Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/appservice-kube/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions src/appservice-kube/azext_appservice_kube/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
7 changes: 7 additions & 0 deletions src/appservice-kube/azext_appservice_kube/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions src/appservice-kube/azext_appservice_kube/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
102 changes: 101 additions & 1 deletion src/appservice-kube/azext_appservice_kube/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion src/appservice-kube/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down