diff --git a/src/appservice-kube/HISTORY.rst b/src/appservice-kube/HISTORY.rst index 3b56fc6da4d..28d9b20a665 100644 --- a/src/appservice-kube/HISTORY.rst +++ b/src/appservice-kube/HISTORY.rst @@ -7,3 +7,7 @@ Release History 0.1.0 ++++++ * Initial public preview release. + +0.1.1 +++++++ +* Fix ssl binding for web apps in kubernetes environments diff --git a/src/appservice-kube/azext_appservice_kube/_help.py b/src/appservice-kube/azext_appservice_kube/_help.py index 3fa301c209d..fb2dd46ea0a 100644 --- a/src/appservice-kube/azext_appservice_kube/_help.py +++ b/src/appservice-kube/azext_appservice_kube/_help.py @@ -112,3 +112,17 @@ az appservice kube wait -g MyResourceGroup -n MyKubeEnvironment \\ --created --interval 60 """ + +helps['webapp config ssl bind'] = """ +type: command +short-summary: Bind an SSL certificate to a web app. +examples: + - name: Bind an SSL certificate to a web app. (autogenerated) + text: az webapp config ssl bind --certificate-thumbprint {certificate-thumbprint} --name MyWebapp --resource-group MyResourceGroup --ssl-type SNI + crafted: true +""" + +helps['webapp config ssl unbind'] = """ +type: command +short-summary: Unbind an SSL certificate from a web app. +""" diff --git a/src/appservice-kube/azext_appservice_kube/_params.py b/src/appservice-kube/azext_appservice_kube/_params.py index 072c46d9dea..1c072675853 100644 --- a/src/appservice-kube/azext_appservice_kube/_params.py +++ b/src/appservice-kube/azext_appservice_kube/_params.py @@ -134,6 +134,13 @@ def load_arguments(self, _): validator=validate_timeout_value) c.argument('is_kube', help='the app is a kubernetes app') + with self.argument_context(scope + ' config ssl bind') as c: + c.argument('ssl_type', help='The ssl cert type', arg_type=get_enum_type(['SNI', 'IP'])) + c.argument('certificate_thumbprint', help='The ssl cert thumbprint') + + with self.argument_context(scope + ' config ssl unbind') as c: + c.argument('certificate_thumbprint', help='The ssl cert thumbprint') + with self.argument_context('appservice') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) c.argument('location', arg_type=get_location_type(self.cli_ctx)) diff --git a/src/appservice-kube/azext_appservice_kube/commands.py b/src/appservice-kube/azext_appservice_kube/commands.py index cc5b065a754..cc549823efd 100644 --- a/src/appservice-kube/azext_appservice_kube/commands.py +++ b/src/appservice-kube/azext_appservice_kube/commands.py @@ -71,6 +71,10 @@ def load_command_table(self, _): g.custom_command('scale', 'scale_webapp') g.custom_command('restart', 'restart_webapp') + with self.command_group('webapp config ssl') as g: + g.custom_command('bind', 'bind_ssl_cert') + g.custom_command('unbind', 'unbind_ssl_cert') + with self.command_group('webapp deployment source') as g: g.custom_command('config-zip', 'enable_zip_deploy_webapp') diff --git a/src/appservice-kube/azext_appservice_kube/custom.py b/src/appservice-kube/azext_appservice_kube/custom.py index 41d9d165441..5657aa6559d 100644 --- a/src/appservice-kube/azext_appservice_kube/custom.py +++ b/src/appservice-kube/azext_appservice_kube/custom.py @@ -42,7 +42,8 @@ _configure_default_logging, assign_identity, delete_app_settings, - update_app_settings) + update_app_settings, + list_hostnames) from azure.cli.command_modules.appservice.utils import retryable_method from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.commands import LongRunningOperation @@ -1675,3 +1676,102 @@ def _fill_ftp_publishing_url(cmd, webapp, resource_group_name, name, slot=None): pass return webapp + + +def _update_host_name_ssl_state(cmd, resource_group_name, webapp_name, webapp, + host_name, ssl_state, thumbprint, slot=None): + from azure.mgmt.web.models import HostNameSslState + + webapp.host_name_ssl_states = [HostNameSslState(name=host_name, + ssl_state=ssl_state, + thumbprint=thumbprint, + to_update=True)] + + webapp_dict = webapp.serialize() + + if webapp.extended_location is not None: + webapp_dict["extendedLocation"]["type"] = "customLocation" + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = "2020-12-01" + sub_id = get_subscription_id(cmd.cli_ctx) + if slot is None: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + webapp_name, + api_version) + else: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}/slots/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + webapp_name, + slot, + api_version) + + return send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(webapp_dict)) + + +def _match_host_names_from_cert(hostnames_from_cert, hostnames_in_webapp): + # the goal is to match '*.foo.com' with host name like 'admin.foo.com', 'logs.foo.com', etc + matched = set() + for hostname in hostnames_from_cert: + if hostname.startswith('*'): + for h in hostnames_in_webapp: + if hostname[hostname.find('.'):] == h[h.find('.'):]: + matched.add(h) + elif hostname in hostnames_in_webapp: + matched.add(hostname) + return matched + + +def _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, ssl_type, slot=None): + client = web_client_factory(cmd.cli_ctx, api_version="2021-01-01") + webapp = client.web_apps.get(resource_group_name, name) + if not webapp: + raise ResourceNotFoundError("'{}' app doesn't exist".format(name)) + + cert_resource_group_name = parse_resource_id(webapp.server_farm_id)['resource_group'] + webapp_certs = client.certificates.list_by_resource_group(cert_resource_group_name) + + found_cert = None + for webapp_cert in webapp_certs: + if webapp_cert.thumbprint == certificate_thumbprint: + found_cert = webapp_cert + if not found_cert: + webapp_certs = client.certificates.list_by_resource_group(resource_group_name) + for webapp_cert in webapp_certs: + if webapp_cert.thumbprint == certificate_thumbprint: + found_cert = webapp_cert + if found_cert: + if len(found_cert.host_names) == 1 and not found_cert.host_names[0].startswith('*'): + return _update_host_name_ssl_state(cmd, resource_group_name, name, webapp, + found_cert.host_names[0], ssl_type, + certificate_thumbprint, slot) + + query_result = list_hostnames(cmd, resource_group_name, name, slot) + hostnames_in_webapp = [x.name.split('/')[-1] for x in query_result] + to_update = _match_host_names_from_cert(found_cert.host_names, hostnames_in_webapp) + for h in to_update: + _update_host_name_ssl_state(cmd, resource_group_name, name, webapp, + h, ssl_type, certificate_thumbprint, slot) + + return show_webapp(cmd, resource_group_name, name, slot) + + raise ResourceNotFoundError("Certificate for thumbprint '{}' not found.".format(certificate_thumbprint)) + + +def bind_ssl_cert(cmd, resource_group_name, name, certificate_thumbprint, ssl_type, slot=None): + SslState = cmd.get_models('SslState') + return _update_ssl_binding(cmd, resource_group_name, name, certificate_thumbprint, + SslState.sni_enabled if ssl_type == 'SNI' else SslState.ip_based_enabled, slot) + + +def unbind_ssl_cert(cmd, resource_group_name, name, certificate_thumbprint, slot=None): + SslState = cmd.get_models('SslState') + return _update_ssl_binding(cmd, resource_group_name, name, + certificate_thumbprint, SslState.disabled, slot) diff --git a/src/appservice-kube/setup.py b/src/appservice-kube/setup.py index b857d57665d..46983e2bac1 100644 --- a/src/appservice-kube/setup.py +++ b/src/appservice-kube/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.0' +VERSION = '0.1.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers