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

Containerapp 0.3.22 Release #5843

Merged
merged 13 commits into from
Feb 16, 2023
Prev Previous commit
Next Next commit
fix bugs & update History and test
  • Loading branch information
lil131 committed Feb 4, 2023
commit e5b58159868553379b6494ceee83126a57ef0659
3 changes: 2 additions & 1 deletion src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
zhoxing-ms marked this conversation as resolved.
Show resolved Hide resolved
* 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'

Expand Down
27 changes: 27 additions & 0 deletions src/containerapp/azext_containerapp/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down
50 changes: 26 additions & 24 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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"])
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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]:
Expand All @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
])
Expand Down