Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AppService] Add az webapp|functionapp config ssl create #11955

Merged
merged 9 commits into from
Feb 13, 2020
Merged
Show file tree
Hide file tree
Changes from 8 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
16 changes: 16 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,14 @@
text: az functionapp config ssl import --resource-group MyResourceGroup --name MyFunctionApp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName
"""

helps['functionapp config ssl create'] = """
type: command
short-summary: Create a Managed Certificate for a hostname in a function app.
examples:
- name: Create a Managed Certificate for $fqdn.
text: az functionapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname $fqdn
"""

helps['functionapp cors'] = """
type: group
short-summary: Manage Cross-Origin Resource Sharing (CORS)
Expand Down Expand Up @@ -1180,6 +1188,14 @@
text: az webapp config ssl import --resource-group MyResourceGroup --name MyWebapp --key-vault MyKeyVault --key-vault-certificate-name MyCertificateName
"""

helps['webapp config ssl create'] = """
type: command
short-summary: Create a Managed Certificate for a hostname in a webapp app.
examples:
- name: Create a Managed Certificate for $fqdn.
text: az webapp config ssl create --resource-group MyResourceGroup --name MyWebapp --hostname $fqdn
"""

helps['webapp config storage-account'] = """
type: group
short-summary: Manage a web app's Azure storage account configurations. (Linux Web Apps and Windows Containers Web Apps Only)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ def load_arguments(self, _):
with self.argument_context(scope + ' config ssl import') as c:
c.argument('key_vault', help='The name or resource ID of the Key Vault')
c.argument('key_vault_certificate_name', help='The name of the certificate in Key Vault')
with self.argument_context(scope + ' config ssl create') as c:
c.argument('hostname', help='The custom domain name')
with self.argument_context(scope + ' config hostname') as c:
c.argument('hostname', completer=get_hostname_completion_list, help="hostname assigned to the site, such as custom domains", id_part='child_name_1')
with self.argument_context(scope + ' deployment user') as c:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def load_command_table(self, _):
g.custom_command('unbind', 'unbind_ssl_cert', validator=validate_app_or_slot_exists_in_rg)
g.custom_command('delete', 'delete_ssl_cert', exception_handler=ex_handler_factory())
g.custom_command('import', 'import_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True)
g.custom_command('create', 'create_managed_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True)

with self.command_group('webapp config backup') as g:
g.custom_command('list', 'list_backups')
Expand Down Expand Up @@ -285,6 +286,7 @@ def load_command_table(self, _):
g.custom_command('unbind', 'unbind_ssl_cert')
g.custom_command('delete', 'delete_ssl_cert')
g.custom_command('import', 'import_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True)
g.custom_command('create', 'create_managed_ssl_cert', exception_handler=ex_handler_factory(), is_preview=True)

with self.command_group('functionapp deployment source') as g:
g.custom_command('config-local-git', 'enable_local_git')
Expand Down
41 changes: 41 additions & 0 deletions src/azure-cli/azure/cli/command_modules/appservice/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2054,6 +2054,35 @@ def import_ssl_cert(cmd, resource_group_name, name, key_vault, key_vault_certifi
certificate_envelope=kv_cert_def)


def create_managed_ssl_cert(cmd, resource_group_name, name, hostname, slot=None):
Certificate = cmd.get_models('Certificate')
hostname = hostname.lower()
client = web_client_factory(cmd.cli_ctx)
webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot)
if not webapp:
slot_text = "Deployment slot {} in ".format(slot)
raise CLIError("{0}app {1} doesn't exist in resource group {2}".format(slot_text, name, resource_group_name))

parsed_plan_id = parse_resource_id(webapp.server_farm_id)
plan_info = client.app_service_plans.get(parsed_plan_id['resource_group'], parsed_plan_id['name'])
if plan_info.sku.tier.upper() == 'FREE' or plan_info.sku.tier.upper() == 'SHARED':
raise CLIError('Managed Certificate is not supported on Free and Shared tier.')

if not _verify_hostname_binding(cmd, resource_group_name, name, hostname, slot):
slot_text = " --slot {}".format(slot) if slot else ""
raise CLIError("Hostname (custom domain) '{0}' is not registered with {1}. "
"Use 'az webapp config hostname add --resource-group {2} "
"--webapp-name {1}{3} --hostname {0}' "
"to register the hostname.".format(hostname, name, resource_group_name, slot_text))

server_farm_id = webapp.server_farm_id
location = webapp.location
easy_cert_def = Certificate(location=location, canonical_name=hostname,
server_farm_id=server_farm_id, password='')
return client.certificates.create_or_update(name=hostname, resource_group_name=resource_group_name,
certificate_envelope=easy_cert_def)


def _check_service_principal_permissions(cmd, resource_group_name, key_vault_name):
from azure.cli.command_modules.keyvault._client_factory import keyvault_client_vaults_factory
from azure.cli.command_modules.role._client_factory import _graph_client_factory
Expand Down Expand Up @@ -3350,3 +3379,15 @@ def _format_key_vault_id(cli_ctx, key_vault, resource_group_name):
namespace='Microsoft.KeyVault',
type='vaults',
name=key_vault)


def _verify_hostname_binding(cmd, resource_group_name, name, hostname, slot=None):
hostname_bindings = _generic_site_operation(cmd.cli_ctx, resource_group_name, name,
'list_host_name_bindings', slot)
verified_hostname_found = False
for hostname_binding in hostname_bindings:
binding_name = hostname_binding.name.split('/')[-1]
if binding_name.lower() == hostname and hostname_binding.host_name_type == 'Verified':
verified_hostname_found = True

return verified_hostname_found
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
validate_container_app_create_options,
restore_deleted_webapp,
list_snapshots,
restore_snapshot)
restore_snapshot,
create_managed_ssl_cert)

# pylint: disable=line-too-long
from vsts_cd_manager.continuous_delivery_manager import ContinuousDeliveryResult
Expand Down Expand Up @@ -403,6 +404,38 @@ def test_valid_linux_create_options(self):
self.assertFalse(validate_container_app_create_options(None, None, test_multi_container_config, None))
self.assertFalse(validate_container_app_create_options(None, None, None, None))

@mock.patch('azure.cli.command_modules.appservice.custom._verify_hostname_binding', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
@mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True)
def test_create_managed_ssl_cert(self, generic_site_op_mock, client_factory_mock, verify_binding_mock):
webapp_name = 'someWebAppName'
rg_name = 'someRgName'
farm_id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.Web/serverfarms/farm1'
host_name = 'www.contoso.com'

client = mock.MagicMock()
client_factory_mock.return_value = client
cmd_mock = _get_test_cmd()
cli_ctx_mock = mock.MagicMock()
cli_ctx_mock.data = {'subscription_id': 'sub1'}
cmd_mock.cli_ctx = cli_ctx_mock
Site, Certificate = cmd_mock.get_models('Site', 'Certificate')
site = Site(name=webapp_name, location='westeurope')
site.server_farm_id = farm_id
generic_site_op_mock.return_value = site

verify_binding_mock.return_value = False
with self.assertRaises(CLIError):
create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None)

verify_binding_mock.return_value = True
create_managed_ssl_cert(cmd_mock, rg_name, webapp_name, host_name, None)

cert_def = Certificate(location='westeurope', canonical_name=host_name,
server_farm_id=farm_id, password='')
client.certificates.create_or_update.assert_called_once_with(name=host_name, resource_group_name=rg_name,
certificate_envelope=cert_def)


class FakedResponse(object): # pylint: disable=too-few-public-methods
def __init__(self, status_code):
Expand Down