From 68c1792766c5b8854302438d6fcfcc10539d2178 Mon Sep 17 00:00:00 2001 From: linluliu Date: Wed, 1 Feb 2023 00:44:15 -0800 Subject: [PATCH 01/11] adding managed certs support --- .../azext_containerapp/_clients.py | 89 ++++++++- .../azext_containerapp/_constants.py | 7 + src/containerapp/azext_containerapp/_help.py | 35 +++- .../azext_containerapp/_models.py | 8 + .../azext_containerapp/_params.py | 11 ++ src/containerapp/azext_containerapp/_utils.py | 47 ++++- .../azext_containerapp/commands.py | 2 + src/containerapp/azext_containerapp/custom.py | 180 ++++++++++++++---- 8 files changed, 339 insertions(+), 40 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index e40fa59ddb3..4b852d41444 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -16,6 +16,8 @@ PREVIEW_API_VERSION = "2022-06-01-preview" CURRENT_API_VERSION = PREVIEW_API_VERSION +LATEST_API_VERSION = "2022-10-01" +MANAGED_CERTS_API_VERSION = '2022-11-01-preview' POLLING_TIMEOUT = 600 # how many seconds before exiting POLLING_SECONDS = 2 # how many seconds between requests @@ -102,7 +104,7 @@ def create_or_update(cls, cmd, resource_group_name, name, container_app_envelope def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = CURRENT_API_VERSION + api_version = LATEST_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" @@ -617,6 +619,23 @@ def show_certificate(cls, cmd, resource_group_name, name, certificate_name): r = send_raw_request(cmd.cli_ctx, "GET", request_url, body=None) return r.json() + @classmethod + def show_managed_certificate(cls, cmd, resource_group_name, name, certificate_name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = MANAGED_CERTS_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/managedCertificates/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + certificate_name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url, body=None) + return r.json() + @classmethod def list_certificates(cls, cmd, resource_group_name, name, formatter=lambda x: x): certs_list = [] @@ -638,6 +657,28 @@ def list_certificates(cls, cmd, resource_group_name, name, formatter=lambda x: x formatted = formatter(cert) certs_list.append(formatted) return certs_list + + @classmethod + def list_managed_certificates(cls, cmd, resource_group_name, name, formatter=lambda x: x): + certs_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = MANAGED_CERTS_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/managedCertificates?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url, body=None) + j = r.json() + for cert in j["value"]: + formatted = formatter(cert) + certs_list.append(formatted) + return certs_list @classmethod def create_or_update_certificate(cls, cmd, resource_group_name, name, certificate_name, certificate): @@ -656,6 +697,36 @@ def create_or_update_certificate(cls, cmd, resource_group_name, name, certificat r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(certificate)) return r.json() + @classmethod + def create_or_update_managed_certificate(cls, cmd, resource_group_name, name, certificate_name, certificate_envelop, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = MANAGED_CERTS_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/managedCertificates/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + certificate_name, + api_version) + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(certificate_envelop)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/managedCertificates/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + certificate_name, + api_version) + return poll(cmd, request_url, "inprogress") + + return r.json() + @classmethod def delete_certificate(cls, cmd, resource_group_name, name, certificate_name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager @@ -672,6 +743,22 @@ def delete_certificate(cls, cmd, resource_group_name, name, certificate_name): return send_raw_request(cmd.cli_ctx, "DELETE", request_url, body=None) + @classmethod + def delete_managed_certificate(cls, cmd, resource_group_name, name, certificate_name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = MANAGED_CERTS_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/managedCertificates/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + certificate_name, + api_version) + + return send_raw_request(cmd.cli_ctx, "DELETE", request_url, body=None) + @classmethod def check_name_availability(cls, cmd, resource_group_name, name, name_availability_request): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_constants.py b/src/containerapp/azext_containerapp/_constants.py index 38d57684f45..3f74f9b07b6 100644 --- a/src/containerapp/azext_containerapp/_constants.py +++ b/src/containerapp/azext_containerapp/_constants.py @@ -14,6 +14,13 @@ LOG_ANALYTICS_RP = "Microsoft.OperationalInsights" CONTAINER_APPS_RP = "Microsoft.App" +MANAGED_CERTIFICATE_RT = "managedCertificates" +PRIVATE_CERTIFICATE_RT = "certificates" + +PENDING_STATUS = "Pending" +SUCCEEDED_STATUS = "Succeeded" +UPDATING_STATUS = "Updating" + MICROSOFT_SECRET_SETTING_NAME = "microsoft-provider-authentication-secret" FACEBOOK_SECRET_SETTING_NAME = "facebook-provider-authentication-secret" GITHUB_SECRET_SETTING_NAME = "github-provider-authentication-secret" diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index bfe092fa3e5..ce1249abf7e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -500,6 +500,15 @@ short-summary: Commands to manage certificates for the Container Apps environment. """ +helps['containerapp env certificate create'] = """ + type: command + short-summary: Create a managed certificate. + examples: + - name: Create a managed certificate. + text: | + az containerapp env certificate create -g MyResourceGroup --name MyEnvironment --certificate-name MyCertificate --hostname MyHostname +""" + helps['containerapp env certificate list'] = """ type: command short-summary: List certificates for an environment. @@ -507,7 +516,7 @@ - name: List certificates for an environment. text: | az containerapp env certificate list -g MyResourceGroup --name MyEnvironment - - name: List certificates by certificate id. + - name: List certificate by certificate id. text: | az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --certificate MyCertificateId - name: List certificates by certificate name. @@ -516,6 +525,12 @@ - name: List certificates by certificate thumbprint. text: | az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --thumbprint MyCertificateThumbprint + - name: List managed certificates for an environment. + text: | + az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --managed_certificates_only + - name: List private key certificates for an environment. + text: | + az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --private-key-certificates-only """ helps['containerapp env certificate upload'] = """ @@ -540,7 +555,7 @@ - name: Delete a certificate from the Container Apps environment by certificate id text: | az containerapp env certificate delete -g MyResourceGroup --name MyEnvironment --certificate MyCertificateId - - name: Delete a certificate from the Container Apps environment by certificate thumbprint + - name: Delete all certificates that have a matching thumbprint from the Container Apps environment text: | az containerapp env certificate delete -g MyResourceGroup --name MyEnvironment --thumbprint MyCertificateThumbprint """ @@ -884,13 +899,25 @@ short-summary: Commands to manage hostnames of a container app. """ +helps['containerapp hostname add'] = """ + type: command + short-summary: Add the hostname to a container app without binding. + examples: + - name: Add hostname without binding. + text: | + az containerapp hostname add -n MyContainerapp -g MyResourceGroup --hostname MyHostname --location MyLocation +""" + helps['containerapp hostname bind'] = """ type: command - short-summary: Add or update the hostname and binding with an existing certificate. + short-summary: Add or update the hostname and binding with a certificate. examples: - - name: Add or update hostname and binding. + - name: Add or update hostname and binding with a provided certificate. text: | az containerapp hostname bind -n MyContainerapp -g MyResourceGroup --hostname MyHostname --certificate MyCertificateId + - name: Look for or create a managed certificate and bind with the hostname if no certificate or thumbprint is provided. + text: | + az containerapp hostname bind -n MyContainerapp -g MyResourceGroup --hostname MyHostname """ helps['containerapp hostname delete'] = """ diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 15c685b33b7..3214e9d833a 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -277,3 +277,11 @@ "accessMode": None, "shareName": None } + +ManagedCertificateEnvelop = { + "location": None, # str + "properties": { + "subjectName": None, # str + "validationMethod": None # str + } +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 70251a27329..b147a7de88e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -176,6 +176,12 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Container Apps Environment.') + with self.argument_context('containerapp env certificate create') as c: + c.argument('hostname', options_list=['--hostname'], help='The custom domain name.') + c.argument('certificate_name', options_list=['--certificate-name', '-c'], help='Name of the managed certificate which should be unique within the Container Apps environment.') + c.argument('location', options_list=['--location'], help='Location of the managed certificate which can be different from the location of the Container Apps environment.') + c.argument('validation_method', options_list=['--validation-method', '-v'], help='Validation method of custom domain ownership.') + with self.argument_context('containerapp env certificate upload') as c: c.argument('certificate_file', options_list=['--certificate-file', '-f'], help='The filepath of the .pfx or .pem file') c.argument('certificate_name', options_list=['--certificate-name', '-c'], help='Name of the certificate which should be unique within the Container Apps environment.') @@ -186,6 +192,8 @@ def load_arguments(self, _): c.argument('name', id_part=None) c.argument('certificate', options_list=['--certificate', '-c'], help='Name or resource id of the certificate.') c.argument('thumbprint', options_list=['--thumbprint', '-t'], help='Thumbprint of the certificate.') + c.argument('managed_certificates_only', options_list=['--managed-certificates-only', '-m'], help='List managed certificates only.') + c.argument('private_key_certificates_only', options_list=['--private-key-certificates-only', '-p'], help='List private-key certificates only.') with self.argument_context('containerapp env certificate delete') as c: c.argument('certificate', options_list=['--certificate', '-c'], help='Name or resource id of the certificate.') @@ -367,6 +375,9 @@ def load_arguments(self, _): c.argument('certificate', options_list=['--certificate', '-c'], help='Name or resource id of the certificate.') c.argument('environment', options_list=['--environment', '-e'], help='Name or resource id of the Container App environment.') + with self.argument_context('containerapp hostname add') as c: + c.argument('location', arg_type=get_location_type(self.cli_ctx)) + with self.argument_context('containerapp hostname list') as c: c.argument('name', id_part=None) c.argument('location', arg_type=get_location_type(self.cli_ctx)) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index c70de62a93d..dc00f8a2fd9 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -29,8 +29,8 @@ from ._client_factory import handle_raw_exception, providers_client_factory, cf_resource_groups, log_analytics_client_factory, log_analytics_shared_key_client_factory from ._constants import (MAXIMUM_CONTAINER_APP_NAME_LENGTH, SHORT_POLLING_INTERVAL_SECS, LONG_POLLING_INTERVAL_SECS, LOG_ANALYTICS_RP, CONTAINER_APPS_RP, CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE, ACR_IMAGE_SUFFIX, - LOGS_STRING) -from ._models import (ContainerAppCustomDomainEnvelope as ContainerAppCustomDomainEnvelopeModel) + LOGS_STRING, PENDING_STATUS, SUCCEEDED_STATUS, UPDATING_STATUS) +from ._models import (ContainerAppCustomDomainEnvelope as ContainerAppCustomDomainEnvelopeModel, ManagedCertificateEnvelop as ManagedCertificateEnvelopModel) logger = get_logger(__name__) @@ -1097,6 +1097,15 @@ def generate_randomized_cert_name(thumbprint, prefix, initial="rg"): return cert_name.lower() +def generate_randomized_managed_cert_name(hostname, env_name): + from random import randint + cert_name = "mc-{}-{}-{:04}".format(env_name[:14], hostname[:16].lower(), randint(0, 9999)) + for c in cert_name: + if not (c.isalnum() or c == '-'): + cert_name = cert_name.replace(c, '-') + return cert_name.lower() + + def _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server): from azure.cli.core.util import ConfiguredDefaultSetter with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): @@ -1367,6 +1376,28 @@ def check_cert_name_availability(cmd, resource_group_name, name, cert_name): return r +def prepare_managed_certificate_envelop(cmd, name, resource_group_name, hostname, validation_method, location=None): + certificate_envelop = ManagedCertificateEnvelopModel + certificate_envelop["location"] = location + certificate_envelop["properties"]["subjectName"] = hostname + certificate_envelop["properties"]["validationMethod"] = validation_method + if not location: + try: + managed_env = ManagedEnvironmentClient.show(cmd, resource_group_name, name) + certificate_envelop["location"] = managed_env["location"] + except Exception as e: + handle_raw_exception(e) + return certificate_envelop + +def check_managed_cert_name_availability(cmd, resource_group_name, name, cert_name): + try: + certs = ManagedEnvironmentClient.list_managed_certificates(cmd, resource_group_name, name) + r = any(cert["name"] == cert_name and cert["properties"]["provisioningState"] in [PENDING_STATUS, SUCCEEDED_STATUS, UPDATING_STATUS] for cert in certs) + except CLIError as e: + handle_raw_exception(e) + return not r + + def validate_hostname(cmd, resource_group_name, name, hostname): passed = False message = None @@ -1577,3 +1608,15 @@ def _azure_monitor_quickstart(cmd, name, resource_group_name, storage_account, l logger.warning("Azure Monitor diagnastic settings created successfully.") except Exception as ex: handle_raw_exception(ex) + + +def certificate_location_matches(certificate_object, location=None): + return certificate_object["location"] == location or not location + + +def certificate_thumbprint_matches(certificate_object, thumbprint=None): + return certificate_object["properties"]["thumbprint"] == thumbprint or not thumbprint + + +def certificate_matches(certificate_object, location=None, thumbprint=None): + return certificate_location_matches(certificate_object, location) and certificate_thumbprint_matches(certificate_object, thumbprint) \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index f50332c28b1..2c494ab0585 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -77,6 +77,7 @@ def load_command_table(self, _): g.custom_command('remove', 'remove_dapr_component') with self.command_group('containerapp env certificate') as g: + g.custom_command('create', 'create_managed_certificate') g.custom_command('list', 'list_certificates') g.custom_command('upload', 'upload_certificate') g.custom_command('delete', 'delete_certificate', confirmation=True, exception_handler=ex_handler_factory()) @@ -179,6 +180,7 @@ def load_command_table(self, _): g.custom_command('upload', 'upload_ssl', exception_handler=ex_handler_factory()) with self.command_group('containerapp hostname') as g: + g.custom_command('add', 'add_hostname', exception_handler=ex_handler_factory()) g.custom_command('bind', 'bind_hostname', exception_handler=ex_handler_factory()) g.custom_command('list', 'list_hostname') g.custom_command('delete', 'delete_hostname', confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 139c22c797d..69a49d25061 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -69,13 +69,15 @@ validate_hostname, patch_new_custom_domain, get_custom_domains, _validate_revision_name, set_managed_identity, create_acrpull_role_assignment, is_registry_msi_system, clean_null_values, _populate_secret_values, validate_environment_location, safe_set, parse_metadata_flags, parse_auth_flags, _azure_monitor_quickstart, - set_ip_restrictions) + set_ip_restrictions, certificate_location_matches, certificate_matches, generate_randomized_managed_cert_name, + check_managed_cert_name_availability, prepare_managed_certificate_envelop) from ._validators import validate_create, validate_revision_suffix from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) from ._constants import (MAXIMUM_SECRET_LENGTH, MICROSOFT_SECRET_SETTING_NAME, FACEBOOK_SECRET_SETTING_NAME, GITHUB_SECRET_SETTING_NAME, GOOGLE_SECRET_SETTING_NAME, TWITTER_SECRET_SETTING_NAME, APPLE_SECRET_SETTING_NAME, CONTAINER_APPS_RP, - NAME_INVALID, NAME_ALREADY_EXISTS, ACR_IMAGE_SUFFIX, HELLO_WORLD_IMAGE, LOG_TYPE_SYSTEM, LOG_TYPE_CONSOLE) + NAME_INVALID, NAME_ALREADY_EXISTS, ACR_IMAGE_SUFFIX, HELLO_WORLD_IMAGE, LOG_TYPE_SYSTEM, LOG_TYPE_CONSOLE, + MANAGED_CERTIFICATE_RT, PRIVATE_CERTIFICATE_RT, PENDING_STATUS, SUCCEEDED_STATUS) logger = get_logger(__name__) @@ -2735,32 +2737,72 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, managed_env=managed_env, image=image, env_vars=env_vars, ingress=ingress, target_port=target_port, registry_server=registry_server, registry_user=registry_user, registry_pass=registry_pass) -def list_certificates(cmd, name, resource_group_name, location=None, certificate=None, thumbprint=None): - _validate_subscription_registered(cmd, CONTAINER_APPS_RP) - - def location_match(c): - return c["location"] == location or not location +def create_managed_certificate(cmd, name, resource_group_name, hostname, validation_method, certificate_name=None, location=None): + if certificate_name and not check_managed_cert_name_availability(cmd, resource_group_name, name, certificate_name): + raise ValidationError(f"Certificate name '{certificate_name}' is not available.") + cert_name = certificate_name + while not cert_name: + cert_name = generate_randomized_managed_cert_name(hostname, resource_group_name) + if not check_managed_cert_name_availability(cmd, resource_group_name, name, certificate_name): + cert_name = None + certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, hostname, validation_method, location) + try: + r = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, True) + return r + except Exception as e: + handle_raw_exception(e) - def thumbprint_match(c): - return c["properties"]["thumbprint"] == thumbprint or not thumbprint - def both_match(c): - return location_match(c) and thumbprint_match(c) +def list_certificates(cmd, name, resource_group_name, location=None, certificate=None, thumbprint=None, managed_certificates_only=False, private_key_certificates_only=False): + _validate_subscription_registered(cmd, CONTAINER_APPS_RP) + if managed_certificates_only and private_key_certificates_only: + raise MutuallyExclusiveArgumentError("Use either '--managed-certificates-only' or '--private-key-certificates-only'.") + if managed_certificates_only and thumbprint: + raise MutuallyExclusiveArgumentError("'--thumbprint' not supported for managed certificates.") + + if certificate and is_valid_resource_id(certificate): + certificate_name = parse_resource_id(certificate)["resource_name"] + certificate_type = parse_resource_id(certificate)["resource_type"] + else: + certificate_name = certificate + certificate_type = PRIVATE_CERTIFICATE_RT if private_key_certificates_only or thumbprint else (MANAGED_CERTIFICATE_RT if managed_certificates_only else None) + + if certificate_type == MANAGED_CERTIFICATE_RT: + return get_managed_certificates(cmd, name, resource_group_name, certificate_name, location) + elif certificate_type == PRIVATE_CERTIFICATE_RT: + return get_private_certificates(cmd, name, resource_group_name, certificate_name, thumbprint, location) + else: + managed_certs = get_managed_certificates(cmd, name, resource_group_name, certificate_name, location) + private_certs = get_private_certificates(cmd, name, resource_group_name, certificate_name, thumbprint, location) + return managed_certs + private_certs + - if certificate: - if is_valid_resource_id(certificate): - certificate_name = parse_resource_id(certificate)["resource_name"] - else: - certificate_name = certificate +def get_private_certificates(cmd, name, resource_group_name, certificate_name=None, thumbprint=None, location=None): + if certificate_name: try: r = ManagedEnvironmentClient.show_certificate(cmd, resource_group_name, name, certificate_name) - return [r] if both_match(r) else [] + return [r] if certificate_matches(r, location, thumbprint) else [] + except: # TODO: need to handle non-404 errors + return [] + else: + try: + r = ManagedEnvironmentClient.list_certificates(cmd, resource_group_name, name) + return list(filter(lambda c: certificate_matches(c, location, thumbprint), r)) except Exception as e: handle_raw_exception(e) + + +def get_managed_certificates(cmd, name, resource_group_name, certificate_name=None, location=None): + if certificate_name: + try: + r = ManagedEnvironmentClient.show_managed_certificate(cmd, resource_group_name, name, certificate_name) + return [r] if certificate_location_matches(r, location) else [] + except: + return [] else: try: - r = ManagedEnvironmentClient.list_certificates(cmd, resource_group_name, name) - return list(filter(both_match, r)) + r = ManagedEnvironmentClient.list_managed_certificates(cmd, resource_group_name, name) + return list(filter(lambda c: certificate_location_matches(c, location), r)) except Exception as e: handle_raw_exception(e) @@ -2819,11 +2861,39 @@ def delete_certificate(cmd, resource_group_name, name, location=None, certificat if not certificate and not thumbprint: raise RequiredArgumentMissingError('Please specify at least one of parameters: --certificate and --thumbprint') - certs = list_certificates(cmd, name, resource_group_name, location, certificate, thumbprint) - for cert in certs: + + cert_type = None + cert_name = certificate + if certificate and is_valid_resource_id(certificate): + cert_type = parse_resource_id(certificate)["resource_type"] + cert_name = parse_resource_id(certificate)["resource_name"] + if thumbprint: + cert_type = PRIVATE_CERTIFICATE_RT + + if cert_type == PRIVATE_CERTIFICATE_RT: + certs = list_certificates(cmd, name, resource_group_name, location, certificate, thumbprint) + for cert in certs: + try: + ManagedEnvironmentClient.delete_certificate(cmd, resource_group_name, name, cert["name"]) + logger.warning(f'Successfully deleted certificate: {cert["name"]}') + except Exception as e: + handle_raw_exception(e) + elif cert_type == MANAGED_CERTIFICATE_RT: + try: + ManagedEnvironmentClient.delete_managed_certificate(cmd, resource_group_name, name, cert_name) + logger.warning('Successfully deleted certificate: {}'.format(cert_name)) + except Exception as e: + handle_raw_exception(e) + else: + managed_certs = list(filter(lambda c: c["name"] == cert_name, get_managed_certificates(cmd, name, resource_group_name, None, location))) + private_certs = list(filter(lambda c: c["name"] == cert_name, get_private_certificates(cmd, name, resource_group_name, None, location))) + if len(managed_certs) == 0 and len(private_certs) == 0: + raise ResourceNotFoundError(f"The certificate '{cert_name}' does not exist in Container app environment '{name}'.") + if len(managed_certs) > 0 and len(private_certs) > 0: + raise RequiredArgumentMissingError(f"Found more than one certificates with name '{cert_name}':\n'{managed_certs[0]['id']}',\n'{private_certs[0]['id']}'.\nPlease specify the certificate id using --certificate.") try: - ManagedEnvironmentClient.delete_certificate(cmd, resource_group_name, name, cert["name"]) - logger.warning('Successfully deleted certificate: {}'.format(cert["name"])) + ManagedEnvironmentClient.delete_managed_certificate(cmd, resource_group_name, name, cert_name) + logger.warning(f'Successfully deleted certificate: {cert_name}') except Exception as e: handle_raw_exception(e) @@ -2838,31 +2908,32 @@ def upload_ssl(cmd, resource_group_name, name, environment, certificate_file, ho custom_domains = get_custom_domains(cmd, resource_group_name, name, location, environment) new_custom_domains = list(filter(lambda c: c["name"] != hostname, custom_domains)) + env_name = _get_name(environment) + logger.warning(f'Uploading certificate to {env_name}.') if is_valid_resource_id(environment): - cert = upload_certificate(cmd, _get_name(environment), parse_resource_id(environment)["resource_group"], certificate_file, certificate_name, certificate_password, location) + cert = upload_certificate(cmd, env_name, parse_resource_id(environment)["resource_group"], certificate_file, certificate_name, certificate_password, location) else: - cert = upload_certificate(cmd, _get_name(environment), resource_group_name, certificate_file, certificate_name, certificate_password, location) + cert = upload_certificate(cmd, env_name, resource_group_name, certificate_file, certificate_name, certificate_password, location) cert_id = cert["id"] new_domain = ContainerAppCustomDomainModel new_domain["name"] = hostname new_domain["certificateId"] = cert_id new_custom_domains.append(new_domain) - + logger.warning(f'Adding hostname {hostname} and binding to {name}.') return patch_new_custom_domain(cmd, resource_group_name, name, new_custom_domains) def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, certificate=None, location=None, environment=None): _validate_subscription_registered(cmd, CONTAINER_APPS_RP) - if not thumbprint and not certificate: - raise RequiredArgumentMissingError('Please specify at least one of parameters: --certificate and --thumbprint') if not environment and not certificate: raise RequiredArgumentMissingError('Please specify at least one of parameters: --certificate and --environment') if certificate and not is_valid_resource_id(certificate) and not environment: raise RequiredArgumentMissingError('Please specify the parameter: --environment') - - passed, message = validate_hostname(cmd, resource_group_name, name, hostname) + + standardized_hostname = hostname.lower() + passed, message = validate_hostname(cmd, resource_group_name, name, standardized_hostname) if not passed: raise ValidationError(message or 'Please configure the DNS records before adding the hostname.') @@ -2878,18 +2949,61 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer env_name = _get_name(environment) if not cert_id: certs = list_certificates(cmd, env_name, resource_group_name, location, cert_name, thumbprint) + if len(certs) == 0: + if thumbprint: + raise ResourceNotFoundError(f"The certificate with thumbprint '{thumbprint}' was not found.") + else: + raise ResourceNotFoundError(f"The certificate '{cert_name}' was not found.") cert_id = certs[0]["id"] - + + # look for or create a managed certificate if no certificate info provided + if not thumbprint and not certificate: + managed_certs = get_managed_certificates(cmd, env_name, resource_group_name, None, None) + managed_cert = [cert for cert in managed_certs if cert["properties"]["subjectName"].lower() == standardized_hostname] + if len(managed_cert): + if managed_cert[0]["properties"]["provisioningState"] in [SUCCEEDED_STATUS, PENDING_STATUS]: + cert_id = managed_cert[0]["id"] + cert_name = managed_cert[0]["name"] + else: + cert_name = None + while not cert_name: + random_name = generate_randomized_managed_cert_name(standardized_hostname, env_name) + available = check_managed_cert_name_availability(cmd, resource_group_name, env_name, cert_name) + if available: + cert_name = random_name + logger.warning(f"Creating a managed certificate '{cert_name}' for {standardized_hostname}.\nIt may take up to 10 minutes to create and issue a managed certificate.") + certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, standardized_hostname, 'TXT', location) + try: + managed_cert = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, False) + except Exception as e: + handle_raw_exception(e) + cert_id = managed_cert["id"] + logger.warning(f"Binding managed certificate '{cert_name}' to {standardized_hostname}") + custom_domains = get_custom_domains(cmd, resource_group_name, name, location, environment) - new_custom_domains = list(filter(lambda c: safe_get(c, "name", default=[]) != hostname, custom_domains)) + new_custom_domains = list(filter(lambda c: safe_get(c, "name", default=[]) != standardized_hostname, custom_domains)) new_domain = ContainerAppCustomDomainModel - new_domain["name"] = hostname + new_domain["name"] = standardized_hostname new_domain["certificateId"] = cert_id new_custom_domains.append(new_domain) return patch_new_custom_domain(cmd, resource_group_name, name, new_custom_domains) +def add_hostname(cmd, resource_group_name, name, hostname, location=None): + _validate_subscription_registered(cmd, CONTAINER_APPS_RP) + standardized_hostname = hostname.lower() + custom_domains = get_custom_domains(cmd, resource_group_name, name, location, None) + existing_hostname = list(filter(lambda c: safe_get(c, "name", default=[]) == standardized_hostname, custom_domains)) + if len(existing_hostname) > 0: + raise InvalidArgumentValueError("'{standardized_hostname}' already exists in container app '{name}'.") + new_domain = ContainerAppCustomDomainModel + new_domain["name"] = standardized_hostname + new_domain["bindingType"] = "Disabled" + custom_domains.append(new_domain) + return patch_new_custom_domain(cmd, resource_group_name, name, custom_domains) + + def list_hostname(cmd, resource_group_name, name, location=None): _validate_subscription_registered(cmd, CONTAINER_APPS_RP) From d6028244ffbdab0f09580bc6433abd501b3f8d34 Mon Sep 17 00:00:00 2001 From: linluliu Date: Wed, 1 Feb 2023 00:44:49 -0800 Subject: [PATCH 02/11] update History.rst --- src/containerapp/HISTORY.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 0ce871a675d..14e4eea212e 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -2,6 +2,15 @@ Release History =============== +0.3.22 +++++++ +* Added 'az containerapp env certificate create' to create managed certificate in a container app environment +* Added 'az containerapp hostname add' to add hostname to a container app without binding +* 'az containerapp env certificate delete': add support for managed certificate deletion +* 'az containerapp env certificate list': add optional parameters --managed-certificates-only and --private-key-certificates-only to list certificates by type +* 'az containerapp hostname bind': change --thumbprint to an optional parameter to support managed certificate bindings +* 'az containerapp ssl upload': log messages to indicate which step is in progress + 0.3.21 ++++++ * Fix the PermissionError caused for the Temporary files while running `az containerapp up` command on Windows From 3fbb77b11707952951a39d6c97029cc73c72a3be Mon Sep 17 00:00:00 2001 From: linluliu Date: Thu, 2 Feb 2023 00:39:35 -0800 Subject: [PATCH 03/11] fix for TXT validation flow --- .../azext_containerapp/_clients.py | 41 +++++++++++++------ src/containerapp/azext_containerapp/custom.py | 25 ++++++----- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 4b852d41444..865e64cadd0 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -20,6 +20,8 @@ MANAGED_CERTS_API_VERSION = '2022-11-01-preview' POLLING_TIMEOUT = 600 # how many seconds before exiting POLLING_SECONDS = 2 # how many seconds between requests +POLLING_TIMEOUT_FOR_MANAGED_CERTIFICATE = 1500 # how many seconds before exiting +POLLING_INTERVAL_FOR_MANAGED_CERTIFICATE = 4 # how many seconds between requests class PollingAnimation(): @@ -698,7 +700,7 @@ def create_or_update_certificate(cls, cmd, resource_group_name, name, certificat return r.json() @classmethod - def create_or_update_managed_certificate(cls, cmd, resource_group_name, name, certificate_name, certificate_envelop, no_wait=False): + def create_or_update_managed_certificate(cls, cmd, resource_group_name, name, certificate_name, certificate_envelop, no_wait=False, is_TXT=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = MANAGED_CERTS_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) @@ -712,19 +714,34 @@ def create_or_update_managed_certificate(cls, cmd, resource_group_name, name, ce api_version) r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(certificate_envelop)) - if no_wait: + if no_wait and not is_TXT: return r.json() elif r.status_code == 201: - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/managedCertificates/{}?api-version={}" - request_url = url_fmt.format( - management_hostname.strip('/'), - sub_id, - resource_group_name, - name, - certificate_name, - api_version) - return poll(cmd, request_url, "inprogress") - + try: + start = time.time() + end = time.time() + POLLING_TIMEOUT_FOR_MANAGED_CERTIFICATE + animation = PollingAnimation() + animation.tick() + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + message_logged = False + while r.status_code in [200, 201] and start < end: + time.sleep(POLLING_INTERVAL_FOR_MANAGED_CERTIFICATE) + animation.tick() + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + r2 = r.json() + if is_TXT and not message_logged and "properties" in r2 and "validationToken" in r2["properties"]: + logger.warning('\nPlease copy the token below for TXT record and enter it with your domain provider:\n{}\n'.format(r2["properties"]["validationToken"])) + message_logged = True + if no_wait: + break + if "properties" not in r2 or "provisioningState" not in r2["properties"] or r2["properties"]["provisioningState"].lower() in ["succeeded", "failed", "canceled"]: + break + start = time.time() + animation.flush() + return r.json() + except Exception as e: # pylint: disable=broad-except + animation.flush() + raise e return r.json() @classmethod diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 69a49d25061..0a7433b8a79 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -23,7 +23,7 @@ from azure.cli.core.util import open_page_in_browser from azure.cli.command_modules.appservice.utils import _normalize_location from knack.log import get_logger -from knack.prompting import prompt_y_n +from knack.prompting import prompt_y_n, prompt from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError @@ -2747,7 +2747,7 @@ def create_managed_certificate(cmd, name, resource_group_name, hostname, validat cert_name = None certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, hostname, validation_method, location) try: - r = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, True) + r = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, True, validation_method == 'TXT') return r except Exception as e: handle_raw_exception(e) @@ -2960,10 +2960,9 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer if not thumbprint and not certificate: managed_certs = get_managed_certificates(cmd, env_name, resource_group_name, None, None) managed_cert = [cert for cert in managed_certs if cert["properties"]["subjectName"].lower() == standardized_hostname] - if len(managed_cert): - if managed_cert[0]["properties"]["provisioningState"] in [SUCCEEDED_STATUS, PENDING_STATUS]: - cert_id = managed_cert[0]["id"] - cert_name = managed_cert[0]["name"] + if len(managed_cert) > 0 and managed_cert[0]["properties"]["provisioningState"] in [SUCCEEDED_STATUS, PENDING_STATUS]: + cert_id = managed_cert[0]["id"] + cert_name = managed_cert[0]["name"] else: cert_name = None while not cert_name: @@ -2971,14 +2970,20 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer available = check_managed_cert_name_availability(cmd, resource_group_name, env_name, cert_name) if available: cert_name = random_name - logger.warning(f"Creating a managed certificate '{cert_name}' for {standardized_hostname}.\nIt may take up to 10 minutes to create and issue a managed certificate.") - certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, standardized_hostname, 'TXT', location) + logger.warning(f"Creating managed certificate '{cert_name}' for {standardized_hostname}.\nIt may take up to 20 minutes to create and issue a managed certificate.") + + validation_method = None + while validation_method not in ["TXT", "CNAME", "HTTP"]: + validation_method = prompt('\nPlease choose one of the following domain validation methods: TXT, CNAME, HTTP\nYour answer: ') + + certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, standardized_hostname, validation_method, location) try: - managed_cert = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, False) + managed_cert = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, False, validation_method == 'TXT') except Exception as e: handle_raw_exception(e) cert_id = managed_cert["id"] - logger.warning(f"Binding managed certificate '{cert_name}' to {standardized_hostname}") + + logger.warning(f"\nBinding managed certificate '{cert_name}' to {standardized_hostname}\n") custom_domains = get_custom_domains(cmd, resource_group_name, name, location, environment) new_custom_domains = list(filter(lambda c: safe_get(c, "name", default=[]) != standardized_hostname, custom_domains)) From 5af9f615de822b6c70864250ce9e959dbea5a509 Mon Sep 17 00:00:00 2001 From: linluliu Date: Thu, 2 Feb 2023 01:30:31 -0800 Subject: [PATCH 04/11] fix linter & style errors --- .../azext_containerapp/_clients.py | 6 +-- src/containerapp/azext_containerapp/_help.py | 4 +- .../azext_containerapp/_models.py | 8 +-- .../azext_containerapp/_params.py | 1 + src/containerapp/azext_containerapp/_utils.py | 5 +- .../azext_containerapp/_validators.py | 2 +- src/containerapp/azext_containerapp/custom.py | 50 +++++++++---------- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 865e64cadd0..40164f2158f 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -659,7 +659,7 @@ def list_certificates(cls, cmd, resource_group_name, name, formatter=lambda x: x formatted = formatter(cert) certs_list.append(formatted) return certs_list - + @classmethod def list_managed_certificates(cls, cmd, resource_group_name, name, formatter=lambda x: x): certs_list = [] @@ -713,7 +713,7 @@ def create_or_update_managed_certificate(cls, cmd, resource_group_name, name, ce certificate_name, api_version) r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(certificate_envelop)) - + if no_wait and not is_TXT: return r.json() elif r.status_code == 201: @@ -730,7 +730,7 @@ def create_or_update_managed_certificate(cls, cmd, resource_group_name, name, ce r = send_raw_request(cmd.cli_ctx, "GET", request_url) r2 = r.json() if is_TXT and not message_logged and "properties" in r2 and "validationToken" in r2["properties"]: - logger.warning('\nPlease copy the token below for TXT record and enter it with your domain provider:\n{}\n'.format(r2["properties"]["validationToken"])) + logger.warning('\nPlease copy the token below for TXT record and enter it with your domain provider:\n%s\n', r2["properties"]["validationToken"]) message_logged = True if no_wait: break diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index ce1249abf7e..6f292c2b0d6 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -506,7 +506,7 @@ examples: - name: Create a managed certificate. text: | - az containerapp env certificate create -g MyResourceGroup --name MyEnvironment --certificate-name MyCertificate --hostname MyHostname + az containerapp env certificate create -g MyResourceGroup --name MyEnvironment --certificate-name MyCertificate --hostname MyHostname --validation-method CNAME """ helps['containerapp env certificate list'] = """ @@ -527,7 +527,7 @@ az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --thumbprint MyCertificateThumbprint - name: List managed certificates for an environment. text: | - az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --managed_certificates_only + az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --managed-certificates-only - name: List private key certificates for an environment. text: | az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --private-key-certificates-only diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 3214e9d833a..f163b0c3ea4 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -279,9 +279,9 @@ } ManagedCertificateEnvelop = { - "location": None, # str + "location": None, # str "properties": { - "subjectName": None, # str - "validationMethod": None # str + "subjectName": None, # str + "validationMethod": None # str } -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index b147a7de88e..bed31f29052 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -376,6 +376,7 @@ def load_arguments(self, _): c.argument('environment', options_list=['--environment', '-e'], help='Name or resource id of the Container App environment.') with self.argument_context('containerapp hostname add') as c: + c.argument('hostname', help='The custom domain name.') c.argument('location', arg_type=get_location_type(self.cli_ctx)) with self.argument_context('containerapp hostname list') as c: diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index dc00f8a2fd9..a6195a8ba0d 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -1389,6 +1389,7 @@ def prepare_managed_certificate_envelop(cmd, name, resource_group_name, hostname handle_raw_exception(e) return certificate_envelop + def check_managed_cert_name_availability(cmd, resource_group_name, name, cert_name): try: certs = ManagedEnvironmentClient.list_managed_certificates(cmd, resource_group_name, name) @@ -1611,7 +1612,7 @@ def _azure_monitor_quickstart(cmd, name, resource_group_name, storage_account, l def certificate_location_matches(certificate_object, location=None): - return certificate_object["location"] == location or not location + return certificate_object["location"] == location or not location def certificate_thumbprint_matches(certificate_object, thumbprint=None): @@ -1619,4 +1620,4 @@ def certificate_thumbprint_matches(certificate_object, thumbprint=None): def certificate_matches(certificate_object, location=None, thumbprint=None): - return certificate_location_matches(certificate_object, location) and certificate_thumbprint_matches(certificate_object, thumbprint) \ No newline at end of file + return certificate_location_matches(certificate_object, location) and certificate_thumbprint_matches(certificate_object, thumbprint) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 815e339ca10..a5c92c527e5 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -4,11 +4,11 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long +import re from azure.cli.core.azclierror import (ValidationError, ResourceNotFoundError, InvalidArgumentValueError, MutuallyExclusiveArgumentError) from msrestazure.tools import is_valid_resource_id from knack.log import get_logger -import re from ._clients import ContainerAppClient from ._ssh_utils import ping_container_app diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 0a7433b8a79..832cc7a8415 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -23,7 +23,7 @@ from azure.cli.core.util import open_page_in_browser from azure.cli.command_modules.appservice.utils import _normalize_location from knack.log import get_logger -from knack.prompting import prompt_y_n, prompt +from knack.prompting import prompt_y_n, prompt as prompt_str from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError @@ -2032,7 +2032,7 @@ def show_ip_restrictions(cmd, name, resource_group_name): except Exception as e: raise ValidationError("Ingress must be enabled to list ip restrictions. Try running `az containerapp ingress -h` for more info.") from e return safe_get(containerapp_def, "properties", "configuration", "ingress", "ipSecurityRestrictions", default=[]) - except Exception as e: + except: return [] @@ -2759,30 +2759,29 @@ def list_certificates(cmd, name, resource_group_name, location=None, certificate raise MutuallyExclusiveArgumentError("Use either '--managed-certificates-only' or '--private-key-certificates-only'.") if managed_certificates_only and thumbprint: raise MutuallyExclusiveArgumentError("'--thumbprint' not supported for managed certificates.") - + if certificate and is_valid_resource_id(certificate): certificate_name = parse_resource_id(certificate)["resource_name"] certificate_type = parse_resource_id(certificate)["resource_type"] else: certificate_name = certificate certificate_type = PRIVATE_CERTIFICATE_RT if private_key_certificates_only or thumbprint else (MANAGED_CERTIFICATE_RT if managed_certificates_only else None) - + if certificate_type == MANAGED_CERTIFICATE_RT: return get_managed_certificates(cmd, name, resource_group_name, certificate_name, location) - elif certificate_type == PRIVATE_CERTIFICATE_RT: + if certificate_type == PRIVATE_CERTIFICATE_RT: return get_private_certificates(cmd, name, resource_group_name, certificate_name, thumbprint, location) - else: - managed_certs = get_managed_certificates(cmd, name, resource_group_name, certificate_name, location) - private_certs = get_private_certificates(cmd, name, resource_group_name, certificate_name, thumbprint, location) - return managed_certs + private_certs - + managed_certs = get_managed_certificates(cmd, name, resource_group_name, certificate_name, location) + private_certs = get_private_certificates(cmd, name, resource_group_name, certificate_name, thumbprint, location) + return managed_certs + private_certs + def get_private_certificates(cmd, name, resource_group_name, certificate_name=None, thumbprint=None, location=None): if certificate_name: try: r = ManagedEnvironmentClient.show_certificate(cmd, resource_group_name, name, certificate_name) return [r] if certificate_matches(r, location, thumbprint) else [] - except: # TODO: need to handle non-404 errors + except: # TODO: handle non-404 errors return [] else: try: @@ -2869,13 +2868,13 @@ def delete_certificate(cmd, resource_group_name, name, location=None, certificat cert_name = parse_resource_id(certificate)["resource_name"] if thumbprint: cert_type = PRIVATE_CERTIFICATE_RT - + if cert_type == PRIVATE_CERTIFICATE_RT: certs = list_certificates(cmd, name, resource_group_name, location, certificate, thumbprint) for cert in certs: try: ManagedEnvironmentClient.delete_certificate(cmd, resource_group_name, name, cert["name"]) - logger.warning(f'Successfully deleted certificate: {cert["name"]}') + logger.warning('Successfully deleted certificate: %s', cert["name"]) except Exception as e: handle_raw_exception(e) elif cert_type == MANAGED_CERTIFICATE_RT: @@ -2893,7 +2892,7 @@ def delete_certificate(cmd, resource_group_name, name, location=None, certificat raise RequiredArgumentMissingError(f"Found more than one certificates with name '{cert_name}':\n'{managed_certs[0]['id']}',\n'{private_certs[0]['id']}'.\nPlease specify the certificate id using --certificate.") try: ManagedEnvironmentClient.delete_managed_certificate(cmd, resource_group_name, name, cert_name) - logger.warning(f'Successfully deleted certificate: {cert_name}') + logger.warning('Successfully deleted certificate: %s', cert_name) except Exception as e: handle_raw_exception(e) @@ -2909,7 +2908,7 @@ def upload_ssl(cmd, resource_group_name, name, environment, certificate_file, ho new_custom_domains = list(filter(lambda c: c["name"] != hostname, custom_domains)) env_name = _get_name(environment) - logger.warning(f'Uploading certificate to {env_name}.') + logger.warning('Uploading certificate to %s.', env_name) if is_valid_resource_id(environment): cert = upload_certificate(cmd, env_name, parse_resource_id(environment)["resource_group"], certificate_file, certificate_name, certificate_password, location) else: @@ -2920,7 +2919,7 @@ def upload_ssl(cmd, resource_group_name, name, environment, certificate_file, ho new_domain["name"] = hostname new_domain["certificateId"] = cert_id new_custom_domains.append(new_domain) - logger.warning(f'Adding hostname {hostname} and binding to {name}.') + logger.warning('Adding hostname %s and binding to %s.', hostname, name) return patch_new_custom_domain(cmd, resource_group_name, name, new_custom_domains) @@ -2931,7 +2930,7 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer raise RequiredArgumentMissingError('Please specify at least one of parameters: --certificate and --environment') if certificate and not is_valid_resource_id(certificate) and not environment: raise RequiredArgumentMissingError('Please specify the parameter: --environment') - + standardized_hostname = hostname.lower() passed, message = validate_hostname(cmd, resource_group_name, name, standardized_hostname) if not passed: @@ -2952,10 +2951,9 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer if len(certs) == 0: if thumbprint: raise ResourceNotFoundError(f"The certificate with thumbprint '{thumbprint}' was not found.") - else: - raise ResourceNotFoundError(f"The certificate '{cert_name}' was not found.") + raise ResourceNotFoundError(f"The certificate '{cert_name}' was not found.") cert_id = certs[0]["id"] - + # look for or create a managed certificate if no certificate info provided if not thumbprint and not certificate: managed_certs = get_managed_certificates(cmd, env_name, resource_group_name, None, None) @@ -2970,21 +2968,21 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer available = check_managed_cert_name_availability(cmd, resource_group_name, env_name, cert_name) if available: cert_name = random_name - logger.warning(f"Creating managed certificate '{cert_name}' for {standardized_hostname}.\nIt may take up to 20 minutes to create and issue a managed certificate.") + logger.warning("Creating managed certificate '%s' for %s.\nIt may take up to 20 minutes to create and issue a managed certificate.", cert_name, standardized_hostname) validation_method = None while validation_method not in ["TXT", "CNAME", "HTTP"]: - validation_method = prompt('\nPlease choose one of the following domain validation methods: TXT, CNAME, HTTP\nYour answer: ') - + validation_method = prompt_str('\nPlease choose one of the following domain validation methods: TXT, CNAME, HTTP\nYour answer: ') + certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, standardized_hostname, validation_method, location) try: managed_cert = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, False, validation_method == 'TXT') except Exception as e: handle_raw_exception(e) cert_id = managed_cert["id"] - - logger.warning(f"\nBinding managed certificate '{cert_name}' to {standardized_hostname}\n") - + + logger.warning("\nBinding managed certificate '%s' to %s\n", cert_name, standardized_hostname) + custom_domains = get_custom_domains(cmd, resource_group_name, name, location, environment) new_custom_domains = list(filter(lambda c: safe_get(c, "name", default=[]) != standardized_hostname, custom_domains)) new_domain = ContainerAppCustomDomainModel From e5b58159868553379b6494ceee83126a57ef0659 Mon Sep 17 00:00:00 2001 From: linluliu Date: Fri, 3 Feb 2023 23:18:30 -0800 Subject: [PATCH 05/11] fix bugs & update History and test --- src/containerapp/HISTORY.rst | 3 +- .../azext_containerapp/_client_factory.py | 27 +++++++++ .../azext_containerapp/_params.py | 1 + src/containerapp/azext_containerapp/custom.py | 50 +++++++++-------- .../latest/test_containerapp_env_commands.py | 55 ++++++++++++++++++- 5 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 1c5da2d1fbb..9532100385c 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -4,11 +4,12 @@ Release History =============== 0.3.22 ++++++ +* BREAKING CHANGE: 'az containerapp env certificate list' returns [] if certificate not found, instead of raising an error. * Added 'az containerapp env certificate create' to create managed certificate in a container app environment * Added 'az containerapp hostname add' to add hostname to a container app without binding * 'az containerapp env certificate delete': add support for managed certificate deletion * 'az containerapp env certificate list': add optional parameters --managed-certificates-only and --private-key-certificates-only to list certificates by type -* 'az containerapp hostname bind': change --thumbprint to an optional parameter to support managed certificate bindings +* 'az containerapp hostname bind': change --thumbprint to an optional parameter and add optional parameter --validation-method to support managed certificate bindings * 'az containerapp ssl upload': log messages to indicate which step is in progress * Fix the 'TypeError: 'NoneType' object does not support item assignment' error obtained while running the CLI command 'az containerapp dapr enable' diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 4e8ad424138..d0852ce7e62 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -54,6 +54,33 @@ def handle_raw_exception(e): raise e +def handle_non_404_exception(e): + import json + + stringErr = str(e) + + if "{" in stringErr and "}" in stringErr: + jsonError = stringErr[stringErr.index("{"):stringErr.rindex("}") + 1] + jsonError = json.loads(jsonError) + + if 'error' in jsonError: + jsonError = jsonError['error'] + + if 'code' in jsonError and 'message' in jsonError: + code = jsonError['code'] + message = jsonError['message'] + if code != "ResourceNotFound": + raise CLIInternalError('({}) {}'.format(code, message)) + return jsonError + elif "Message" in jsonError: + message = jsonError["Message"] + raise CLIInternalError(message) + elif "message" in jsonError: + message = jsonError["message"] + raise CLIInternalError(message) + raise e + + def providers_client_factory(cli_ctx, subscription_id=None): return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, subscription_id=subscription_id).providers diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index bed31f29052..3e3e7dbdf7f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -374,6 +374,7 @@ def load_arguments(self, _): c.argument('thumbprint', options_list=['--thumbprint', '-t'], help='Thumbprint of the certificate.') c.argument('certificate', options_list=['--certificate', '-c'], help='Name or resource id of the certificate.') c.argument('environment', options_list=['--environment', '-e'], help='Name or resource id of the Container App environment.') + c.argument('validation_method', options_list=['--validation-method', '-v'], help='Validation method of custom domain ownership.') with self.argument_context('containerapp hostname add') as c: c.argument('hostname', help='The custom domain name.') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 48c3469a520..60d5b79aa93 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -28,7 +28,7 @@ from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError -from ._client_factory import handle_raw_exception +from ._client_factory import handle_raw_exception, handle_non_404_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient, StorageClient, AuthClient from ._github_oauth import get_github_access_token from ._models import ( @@ -2781,7 +2781,8 @@ def get_private_certificates(cmd, name, resource_group_name, certificate_name=No try: r = ManagedEnvironmentClient.show_certificate(cmd, resource_group_name, name, certificate_name) return [r] if certificate_matches(r, location, thumbprint) else [] - except: # TODO: handle non-404 errors + except Exception as e: + handle_non_404_exception(e) return [] else: try: @@ -2796,7 +2797,8 @@ def get_managed_certificates(cmd, name, resource_group_name, certificate_name=No try: r = ManagedEnvironmentClient.show_managed_certificate(cmd, resource_group_name, name, certificate_name) return [r] if certificate_location_matches(r, location) else [] - except: + except Exception as e: + handle_non_404_exception(e) return [] else: try: @@ -2871,6 +2873,9 @@ def delete_certificate(cmd, resource_group_name, name, location=None, certificat if cert_type == PRIVATE_CERTIFICATE_RT: certs = list_certificates(cmd, name, resource_group_name, location, certificate, thumbprint) + if len(certs) == 0: + msg = "'{}'".format(cert_name) if cert_name else "with thumbprint '{}'".format(thumbprint) + raise ResourceNotFoundError(f"The certificate {msg} does not exist in Container app environment '{name}'.") for cert in certs: try: ManagedEnvironmentClient.delete_certificate(cmd, resource_group_name, name, cert["name"]) @@ -2885,7 +2890,7 @@ def delete_certificate(cmd, resource_group_name, name, location=None, certificat handle_raw_exception(e) else: managed_certs = list(filter(lambda c: c["name"] == cert_name, get_managed_certificates(cmd, name, resource_group_name, None, location))) - private_certs = list(filter(lambda c: c["name"] == cert_name, get_private_certificates(cmd, name, resource_group_name, None, location))) + private_certs = list(filter(lambda c: c["name"] == cert_name, get_private_certificates(cmd, name, resource_group_name, None, None, location))) if len(managed_certs) == 0 and len(private_certs) == 0: raise ResourceNotFoundError(f"The certificate '{cert_name}' does not exist in Container app environment '{name}'.") if len(managed_certs) > 0 and len(private_certs) > 0: @@ -2923,7 +2928,7 @@ def upload_ssl(cmd, resource_group_name, name, environment, certificate_file, ho return patch_new_custom_domain(cmd, resource_group_name, name, new_custom_domains) -def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, certificate=None, location=None, environment=None): +def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, certificate=None, location=None, environment=None, validation_method=None): _validate_subscription_registered(cmd, CONTAINER_APPS_RP) if not environment and not certificate: @@ -2936,26 +2941,23 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer if not passed: raise ValidationError(message or 'Please configure the DNS records before adding the hostname.') - env_name = None - cert_name = None - cert_id = None + env_name = _get_name(environment) if environment else None + if certificate: if is_valid_resource_id(certificate): cert_id = certificate else: - cert_name = certificate - if environment: - env_name = _get_name(environment) - if not cert_id: - certs = list_certificates(cmd, env_name, resource_group_name, location, cert_name, thumbprint) + certs = list_certificates(cmd, env_name, resource_group_name, location, certificate, thumbprint) + if len(certs) == 0: + msg = "'{}' with thumbprint '{}'".format(certificate, thumbprint) if thumbprint else "'{}'".format(certificate) + raise ResourceNotFoundError(f"The certificate {msg} does not exist in Container app environment '{env_name}'.") + cert_id = certs[0]["id"] + elif thumbprint: + certs = list_certificates(cmd, env_name, resource_group_name, location, certificate, thumbprint) if len(certs) == 0: - if thumbprint: - raise ResourceNotFoundError(f"The certificate with thumbprint '{thumbprint}' was not found.") - raise ResourceNotFoundError(f"The certificate '{cert_name}' was not found.") + raise ResourceNotFoundError(f"The certificate with thumbprint '{thumbprint}' does not exist in Container app environment '{env_name}'.") cert_id = certs[0]["id"] - - # look for or create a managed certificate if no certificate info provided - if not thumbprint and not certificate: + else: # look for or create a managed certificate if no certificate info provided managed_certs = get_managed_certificates(cmd, env_name, resource_group_name, None, None) managed_cert = [cert for cert in managed_certs if cert["properties"]["subjectName"].lower() == standardized_hostname] if len(managed_cert) > 0 and managed_cert[0]["properties"]["provisioningState"] in [SUCCEEDED_STATUS, PENDING_STATUS]: @@ -2970,13 +2972,13 @@ def bind_hostname(cmd, resource_group_name, name, hostname, thumbprint=None, cer cert_name = random_name logger.warning("Creating managed certificate '%s' for %s.\nIt may take up to 20 minutes to create and issue a managed certificate.", cert_name, standardized_hostname) - validation_method = None - while validation_method not in ["TXT", "CNAME", "HTTP"]: - validation_method = prompt_str('\nPlease choose one of the following domain validation methods: TXT, CNAME, HTTP\nYour answer: ') + validation = validation_method + while validation not in ["TXT", "CNAME", "HTTP"]: + validation = prompt_str('\nPlease choose one of the following domain validation methods: TXT, CNAME, HTTP\nYour answer: ') - certificate_envelop = prepare_managed_certificate_envelop(cmd, name, resource_group_name, standardized_hostname, validation_method, location) + certificate_envelop = prepare_managed_certificate_envelop(cmd, env_name, resource_group_name, standardized_hostname, validation_method, location) try: - managed_cert = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, name, cert_name, certificate_envelop, False, validation_method == 'TXT') + managed_cert = ManagedEnvironmentClient.create_or_update_managed_certificate(cmd, resource_group_name, env_name, cert_name, certificate_envelop, False, validation_method == 'TXT') except Exception as e: handle_raw_exception(e) cert_id = managed_cert["id"] diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_env_commands.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_env_commands.py index b2294f59580..b739d20e6b5 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_env_commands.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_env_commands.py @@ -204,7 +204,7 @@ def test_containerapp_env_dapr_components(self, resource_group): def test_containerapp_env_certificate_e2e(self, resource_group): location = os.getenv("CLITestLocation") if not location: - location = 'eastus' + location = 'northcentralusstage' self.cmd('configure --defaults location={}'.format(location)) env_name = self.create_random_name(prefix='containerapp-e2e-env', length=24) @@ -271,9 +271,62 @@ def test_containerapp_env_certificate_e2e(self, resource_group): JMESPathCheck('[0].id', cert_id), JMESPathCheck('[0].properties.thumbprint', cert_thumbprint), ]) + + # create a container app + ca_name = self.create_random_name(prefix='containerapp', length=24) + app = self.cmd('containerapp create -g {} -n {} --environment {} --ingress external --target-port 80'.format(resource_group, ca_name, env_name)).get_output_in_json() + + # create an App service domain and update its DNS records + contacts = os.path.join(TEST_DIR, 'domain-contact.json') + zone_name = "{}.com".format(ca_name) + subdomain_1 = "devtest" + txt_name_1 = "asuid.{}".format(subdomain_1) + hostname_1 = "{}.{}".format(subdomain_1, zone_name) + verification_id = app["properties"]["customDomainVerificationId"] + fqdn = app["properties"]["configuration"]["ingress"]["fqdn"] + self.cmd("appservice domain create -g {} --hostname {} --contact-info=@'{}' --accept-terms".format(resource_group, zone_name, contacts)).get_output_in_json() + self.cmd('network dns record-set txt add-record -g {} -z {} -n {} -v {}'.format(resource_group, zone_name, txt_name_1, verification_id)).get_output_in_json() + self.cmd('network dns record-set cname create -g {} -z {} -n {}'.format(resource_group, zone_name, subdomain_1)).get_output_in_json() + self.cmd('network dns record-set cname set-record -g {} -z {} -n {} -c {}'.format(resource_group, zone_name, subdomain_1, fqdn)).get_output_in_json() + + # add hostname without binding + self.cmd('containerapp hostname add -g {} -n {} --hostname {}'.format(resource_group, ca_name, hostname_1), checks={ + JMESPathCheck('length(@)', 1), + JMESPathCheck('[0].name', hostname_1), + JMESPathCheck('[0].bindingType', "Disabled"), + }) + self.cmd('containerapp hostname add -g {} -n {} --hostname {}'.format(resource_group, ca_name, hostname_1), expect_failure=True) + + # create a managed certificate + self.cmd('containerapp env certificate create -n {} -g {} --hostname {} -v CNAME -c {}'.format(env_name, resource_group, hostname_1, cert_name), checks=[ + JMESPathCheck('type', "Microsoft.App/managedEnvironments/managedCertificates"), + JMESPathCheck('name', cert_name), + JMESPathCheck('properties.subjectName', hostname_1), + ]).get_output_in_json() + + self.cmd('containerapp env certificate create -n {} -g {} --hostname {} -v CNAME'.format(env_name, resource_group, hostname_1), expect_failure=True) + self.cmd('containerapp env certificate list -g {} -n {} -m'.format(resource_group, env_name), checks=[ + JMESPathCheck('length(@)', 1), + ]) + self.cmd('containerapp env certificate list -g {} -n {} -c {}'.format(resource_group, env_name, cert_name), checks=[ + JMESPathCheck('length(@)', 2), + ]) + self.cmd('containerapp env certificate delete -n {} -g {} --certificate {} --yes'.format(env_name, resource_group, cert_name), expect_failure=True) self.cmd('containerapp env certificate delete -n {} -g {} --thumbprint {} --yes'.format(env_name, resource_group, cert_thumbprint)) + self.cmd('containerapp env certificate delete -n {} -g {} --certificate {} --yes'.format(env_name, resource_group, cert_name)) + self.cmd('containerapp env certificate list -g {} -n {}'.format(resource_group, env_name), checks=[ + JMESPathCheck('length(@)', 0), + ]) + + self.cmd('containerapp hostname bind -g {} -n {} --hostname {} --environment {} -v CNAME'.format(resource_group, ca_name, hostname_1, env_name)) + certs = self.cmd('containerapp env certificate list -g {} -n {}'.format(resource_group, env_name), checks=[ + JMESPathCheck('length(@)', 1), + ]).get_output_in_json() + self.cmd('containerapp env certificate delete -n {} -g {} --certificate {} --yes'.format(env_name, resource_group, certs[0]["name"]), expect_failure=True) + self.cmd('containerapp hostname delete -g {} -n {} --hostname {} --yes'.format(resource_group, ca_name, hostname_1)) + self.cmd('containerapp env certificate delete -n {} -g {} --certificate {} --yes'.format(env_name, resource_group, certs[0]["name"])) self.cmd('containerapp env certificate list -g {} -n {}'.format(resource_group, env_name), checks=[ JMESPathCheck('length(@)', 0), ]) From f60248e6b90465120ad62a7bb86fce316bef2a5a Mon Sep 17 00:00:00 2001 From: linluliu Date: Fri, 3 Feb 2023 23:25:21 -0800 Subject: [PATCH 06/11] update version number in setup.py --- src/containerapp/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 16a134707b4..96a31426d53 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -17,7 +17,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.3.21' +VERSION = '0.3.22' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 1aefbb8e1aa0701e8737b8204fb3ff59bdcd752e Mon Sep 17 00:00:00 2001 From: linluliu Date: Mon, 6 Feb 2023 11:43:06 -0800 Subject: [PATCH 07/11] using CURRENT_API_VERSION for update --- src/containerapp/azext_containerapp/_clients.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 40164f2158f..b0b9f25540c 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -16,7 +16,6 @@ PREVIEW_API_VERSION = "2022-06-01-preview" CURRENT_API_VERSION = PREVIEW_API_VERSION -LATEST_API_VERSION = "2022-10-01" MANAGED_CERTS_API_VERSION = '2022-11-01-preview' POLLING_TIMEOUT = 600 # how many seconds before exiting POLLING_SECONDS = 2 # how many seconds between requests @@ -106,7 +105,7 @@ def create_or_update(cls, cmd, resource_group_name, name, container_app_envelope def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = LATEST_API_VERSION + api_version = CURRENT_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" From 1adb3dc2bccbce87de2a24303a18ac2416e14ba0 Mon Sep 17 00:00:00 2001 From: Linlu Liu <43486346+lil131@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:32:30 -0800 Subject: [PATCH 08/11] Update src/containerapp/azext_containerapp/_params.py Co-authored-by: Xing Zhou --- src/containerapp/azext_containerapp/_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 3e3e7dbdf7f..2fac01b8643 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -177,7 +177,7 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the Container Apps Environment.') with self.argument_context('containerapp env certificate create') as c: - c.argument('hostname', options_list=['--hostname'], help='The custom domain name.') + c.argument('hostname', help='The custom domain name.') c.argument('certificate_name', options_list=['--certificate-name', '-c'], help='Name of the managed certificate which should be unique within the Container Apps environment.') c.argument('location', options_list=['--location'], help='Location of the managed certificate which can be different from the location of the Container Apps environment.') c.argument('validation_method', options_list=['--validation-method', '-v'], help='Validation method of custom domain ownership.') From 44eed874c255d0e0c5beb62205c1d9dfaf4cc684 Mon Sep 17 00:00:00 2001 From: Linlu Liu <43486346+lil131@users.noreply.github.com> Date: Tue, 14 Feb 2023 11:32:41 -0800 Subject: [PATCH 09/11] Update src/containerapp/azext_containerapp/_params.py Co-authored-by: Xing Zhou --- src/containerapp/azext_containerapp/_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 2fac01b8643..5f81d2e59d8 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -179,7 +179,7 @@ def load_arguments(self, _): with self.argument_context('containerapp env certificate create') as c: c.argument('hostname', help='The custom domain name.') c.argument('certificate_name', options_list=['--certificate-name', '-c'], help='Name of the managed certificate which should be unique within the Container Apps environment.') - c.argument('location', options_list=['--location'], help='Location of the managed certificate which can be different from the location of the Container Apps environment.') + c.argument('location', get_location_type(self.cli_ctx), help='Location of the managed certificate which can be different from the location of the Container Apps environment.') c.argument('validation_method', options_list=['--validation-method', '-v'], help='Validation method of custom domain ownership.') with self.argument_context('containerapp env certificate upload') as c: From b57973016d48599ca40126a046381741ff885ffb Mon Sep 17 00:00:00 2001 From: linluliu Date: Tue, 14 Feb 2023 13:49:01 -0800 Subject: [PATCH 10/11] Revert "Update src/containerapp/azext_containerapp/_params.py" This reverts commit 1adb3dc2bccbce87de2a24303a18ac2416e14ba0. --- src/containerapp/azext_containerapp/_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 5f81d2e59d8..5018d62ca7a 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -177,7 +177,7 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the Container Apps Environment.') with self.argument_context('containerapp env certificate create') as c: - c.argument('hostname', help='The custom domain name.') + c.argument('hostname', options_list=['--hostname'], help='The custom domain name.') c.argument('certificate_name', options_list=['--certificate-name', '-c'], help='Name of the managed certificate which should be unique within the Container Apps environment.') c.argument('location', get_location_type(self.cli_ctx), help='Location of the managed certificate which can be different from the location of the Container Apps environment.') c.argument('validation_method', options_list=['--validation-method', '-v'], help='Validation method of custom domain ownership.') From 9ea68e82ca5dae43ce37e4805d1d107dec755a1b Mon Sep 17 00:00:00 2001 From: Linlu Liu <43486346+lil131@users.noreply.github.com> Date: Wed, 15 Feb 2023 13:03:13 -0800 Subject: [PATCH 11/11] Update src/containerapp/azext_containerapp/_help.py Co-authored-by: Xing Zhou --- src/containerapp/azext_containerapp/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 6f292c2b0d6..9a2f8ac4c33 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -516,7 +516,7 @@ - name: List certificates for an environment. text: | az containerapp env certificate list -g MyResourceGroup --name MyEnvironment - - name: List certificate by certificate id. + - name: Show a certificate by certificate id. text: | az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --certificate MyCertificateId - name: List certificates by certificate name.