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
Next Next commit
adding managed certs support
  • Loading branch information
lil131 committed Feb 1, 2023
commit 68c1792766c5b8854302438d6fcfcc10539d2178
89 changes: 88 additions & 1 deletion src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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={}"
Expand Down Expand Up @@ -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 = []
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
35 changes: 31 additions & 4 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,14 +500,23 @@
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.
examples:
- 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.
lil131 marked this conversation as resolved.
Show resolved Hide resolved
lil131 marked this conversation as resolved.
Show resolved Hide resolved
text: |
az containerapp env certificate list -g MyResourceGroup --name MyEnvironment --certificate MyCertificateId
- name: List certificates by certificate name.
Expand All @@ -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'] = """
Expand All @@ -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
"""
Expand Down Expand Up @@ -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'] = """
Expand Down
8 changes: 8 additions & 0 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,11 @@
"accessMode": None,
"shareName": None
}

ManagedCertificateEnvelop = {
"location": None, # str
"properties": {
"subjectName": None, # str
"validationMethod": None # str
}
}
11 changes: 11 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
zhoxing-ms marked this conversation as resolved.
Show resolved Hide resolved
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.')
lil131 marked this conversation as resolved.
Show resolved Hide resolved
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.')
Expand All @@ -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.')
Expand Down Expand Up @@ -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))
Expand Down
47 changes: 45 additions & 2 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

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