Skip to content

Commit

Permalink
support certificate from keyvault (#7259)
Browse files Browse the repository at this point in the history
  • Loading branch information
njuCZ committed Mar 6, 2024
1 parent 90e2a23 commit c4baba3
Show file tree
Hide file tree
Showing 14 changed files with 46,735 additions and 8,558 deletions.
2 changes: 2 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ upcoming
* 'az containerapp env telemetry app-insights set': Support update environment app insights configuration with --connection-string, --enable-open-telemetry-traces and --enable-open-telemetry-logs
* 'az containerapp env telemetry app-insights delete': Support delete environment app insights configuration
* 'az containerapp update/up': Explicitly set container name to container app name for source to cloud builds.
* 'az containerapp env create/update': Add support for environment custom domain from azure key vault using managed identity
* 'az containerapp env certificate upload': Add support for environment certificate from azure key vault using managed identity

0.3.48
++++++
Expand Down
15 changes: 15 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,21 @@
az containerapp env certificate create -g MyResourceGroup --name MyEnvironment --certificate-name MyCertificate --hostname MyHostname --validation-method CNAME
"""

helps['containerapp env certificate upload'] = """
type: command
short-summary: Add or update a certificate.
examples:
- name: Add or update a certificate.
text: |
az containerapp env certificate upload -g MyResourceGroup --name MyEnvironment --certificate-file MyFilepath
- name: Add or update a certificate with a user-provided certificate name.
text: |
az containerapp env certificate upload -g MyResourceGroup --name MyEnvironment --certificate-file MyFilepath --certificate-name MyCertificateName
- name: Add or update a certificate from azure key vault using managed identity.
text: |
az containerapp env certificate upload -g MyResourceGroup --name MyEnvironment --akv-url akvSecretUrl --identity system
"""

helps['containerapp env certificate list'] = """
type: command
short-summary: List certificates for an environment.
Expand Down
10 changes: 9 additions & 1 deletion src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,8 @@
"location": None,
"properties": {
"password": None,
"value": None
"value": None,
"certificateKeyVaultProperties": None
}
}

Expand Down Expand Up @@ -579,3 +580,10 @@
"componentType": None
}
}

CustomDomainConfiguration = {
"dnsSuffix": None,
"certificateValue": None,
"certificatePassword": None,
"certificateKeyVaultProperties": None
}
12 changes: 12 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ def load_arguments(self, _):
c.argument('environment_name', options_list=['--environment'], help="The environment name.")
c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None)

with self.argument_context('containerapp env', arg_group='Custom Domain') as c:
c.argument('certificate_identity', options_list=['--custom-domain-certificate-identity', '--certificate-identity'],
help='Resource ID of a managed identity to authenticate with Azure Key Vault, or System to use a system-assigned identity.', is_preview=True)
c.argument('certificate_key_vault_url', options_list=['--custom-domain-certificate-akv-url', '--certificate-akv-url'],
help='The URL pointing to the Azure Key Vault secret that holds the certificate.', is_preview=True)

with self.argument_context('containerapp env create') as c:
c.argument('enable_workload_profiles', arg_type=get_three_state_flag(), options_list=["--enable-workload-profiles", "-w"], help="Boolean indicating if the environment is enabled to have workload profiles")
c.argument('enable_dedicated_gpu', arg_type=get_three_state_flag(), options_list=["--enable-dedicated-gpu"],
Expand All @@ -139,6 +145,12 @@ def load_arguments(self, _):
c.argument('system_assigned', options_list=['--mi-system-assigned'], help='Boolean indicating whether to assign system-assigned identity.', action='store_true')
c.argument('user_assigned', options_list=['--mi-user-assigned'], nargs='+', help='Space-separated user identities to be assigned.')

with self.argument_context('containerapp env certificate upload') as c:
c.argument('certificate_identity', options_list=['--certificate-identity', '--identity'],
help='Resource ID of a managed identity to authenticate with Azure Key Vault, or System to use a system-assigned identity.', is_preview=True)
c.argument('certificate_key_vault_url', options_list=['--certificate-akv-url', '--akv-url'],
help='The URL pointing to the Azure Key Vault secret that holds the certificate.', is_preview=True)

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.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
from knack.log import get_logger

from azure.cli.command_modules.containerapp._utils import certificate_matches, certificate_location_matches, \
load_cert_file, generate_randomized_cert_name
load_cert_file, generate_randomized_cert_name, _ensure_identity_resource_id
from azure.cli.command_modules.containerapp.base_resource import BaseResource
from azure.cli.core.azclierror import MutuallyExclusiveArgumentError, ValidationError
from azure.cli.core.commands import AzCliCommand
from knack.prompting import prompt_y_n
from knack.util import CLIError
from msrestazure.tools import is_valid_resource_id, parse_resource_id
from azure.cli.core.commands.client_factory import get_subscription_id
from copy import deepcopy

from ._constants import PRIVATE_CERTIFICATE_RT, MANAGED_CERTIFICATE_RT, CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE, \
NAME_ALREADY_EXISTS, NAME_INVALID
Expand Down Expand Up @@ -86,7 +88,7 @@ def list(self):
class ContainerappEnvCertificateUploadDecorator(ContainerappEnvCertificateDecorator):
def __init__(self, cmd: AzCliCommand, client: Any, raw_parameters: Dict, models: str):
super().__init__(cmd, client, raw_parameters, models)
self.certificate = ContainerAppCertificateEnvelopeModel
self.certificate = deepcopy(ContainerAppCertificateEnvelopeModel)
self.cert_name = None

def get_argument_certificate_name(self):
Expand Down Expand Up @@ -191,3 +193,37 @@ def list(self):
managed_certs = self.get_managed_certificates(certificate_name)
private_certs = self.get_private_certificates(certificate_name)
return managed_certs + private_certs


class ContainerappEnvCertificatePreviweUploadDecorator(ContainerappEnvCertificateUploadDecorator):
def validate_arguments(self):
# validate arguments
if self.get_argument_certificate_file() and self.get_argument_certificate_key_vault_url():
raise ValidationError("Cannot use --certificate-file/--certificate-password with --certificate-akv-url/--certificate-identity at the same time")
if (not self.get_argument_certificate_file()) and (not self.get_argument_certificate_key_vault_url()):
raise ValidationError("Either --certificate-file/--certificate-password or --certificate-akv-url/--certificate-identity should be set when hostName is set")

def set_up_certificate_from_key_vault(self):
if self.get_argument_certificate_key_vault_url():
identity = self.get_argument_certificate_identity()
if not identity:
identity = "system"
if identity.lower() != "system":
subscription_id = get_subscription_id(self.cmd.cli_ctx)
identity = _ensure_identity_resource_id(subscription_id, self.get_argument_resource_group_name(), identity)
self.certificate["properties"]["certificateKeyVaultProperties"] = {
"keyVaultUrl": self.get_argument_certificate_key_vault_url(),
"identity": identity
}
# used for autogenrate cert name
super().set_argument_thumbprint("cert-kv")

def construct_payload(self):
self.set_up_certificate_from_key_vault()
super().construct_payload()

def get_argument_certificate_identity(self):
return self.get_param("certificate_identity")

def get_argument_certificate_key_vault_url(self):
return self.get_param("certificate_key_vault_url")
95 changes: 92 additions & 3 deletions src/containerapp/azext_containerapp/containerapp_env_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
from azure.cli.command_modules.containerapp._utils import get_default_workload_profiles, safe_set, _ensure_identity_resource_id
from azure.cli.command_modules.containerapp._utils import get_default_workload_profiles, safe_set, _ensure_identity_resource_id, load_cert_file
from knack.log import get_logger

from azure.cli.command_modules.containerapp.containerapp_env_decorator import ContainerAppEnvCreateDecorator, \
ContainerAppEnvUpdateDecorator
from azure.cli.core.azclierror import RequiredArgumentMissingError, ValidationError
from azure.cli.core.commands.client_factory import get_subscription_id
from ._models import ManagedServiceIdentity
from ._models import ManagedServiceIdentity, CustomDomainConfiguration
from ._utils import safe_get
from ._client_factory import handle_non_404_status_code_exception

Expand All @@ -21,7 +21,27 @@ def get_argument_infrastructure_resource_group(self):
return self.get_param("infrastructure_resource_group")

def construct_payload(self):
super().construct_payload()
### copy from the parent construct_payload
self.set_up_app_log_configuration()

self.managed_env_def["location"] = self.get_argument_location()
self.managed_env_def["tags"] = self.get_argument_tags()
self.managed_env_def["properties"]["zoneRedundant"] = self.get_argument_zone_redundant()

self.set_up_workload_profiles()

if self.get_argument_instrumentation_key() is not None:
self.managed_env_def["properties"]["daprAIInstrumentationKey"] = self.get_argument_instrumentation_key()

# Vnet
self.set_up_vnet_configuration()

if self.get_argument_mtls_enabled() is not None:
safe_set(self.managed_env_def, "properties", "peerAuthentication", "mtls", "enabled", value=self.get_argument_mtls_enabled())
### copy end

### overwrite custom_domain_configuration
self.set_up_custom_domain_configuration()

self.set_up_infrastructure_resource_group()
self.set_up_dynamic_json_columns()
Expand All @@ -38,6 +58,13 @@ def validate_arguments(self):
if not self.get_argument_enable_workload_profiles():
raise RequiredArgumentMissingError("Cannot use --infrastructure-resource-group/-i without "
"--enable-workload-profiles/-w")

# validate custom domain configuration
if self.get_argument_hostname():
if self.get_argument_certificate_file() and self.get_argument_certificate_key_vault_url():
raise ValidationError("Cannot use --certificate-file with --certificate-akv-url at the same time")
if (not self.get_argument_certificate_file()) and (not self.get_argument_certificate_key_vault_url()):
raise ValidationError("Either --certificate-file or --certificate-akv-url should be set when --dns-suffix is set")

def set_up_dynamic_json_columns(self):
if self.get_argument_logs_destination() == "log-analytics" and self.get_argument_logs_dynamic_json_columns() is not None:
Expand Down Expand Up @@ -99,6 +126,29 @@ def set_up_workload_profiles(self):
workload_profiles.append(gpu_profile)
self.managed_env_def["properties"]["workloadProfiles"] = workload_profiles

def set_up_custom_domain_configuration(self):
if self.get_argument_hostname():
custom_domain = CustomDomainConfiguration
custom_domain["dnsSuffix"] = self.get_argument_hostname()
if self.get_argument_certificate_file():
blob, _ = load_cert_file(self.get_argument_certificate_file(), self.get_argument_certificate_password())
custom_domain["certificatePassword"] = self.get_argument_certificate_password()
custom_domain["certificateValue"] = blob
if self.get_argument_certificate_key_vault_url():
# default use system identity
identity = self.get_argument_certificate_identity()
if not identity:
identity = "system"
if identity.lower() != "system":
subscription_id = get_subscription_id(self.cmd.cli_ctx)
identity = _ensure_identity_resource_id(subscription_id, self.get_argument_resource_group_name(), identity)

custom_domain["certificateKeyVaultProperties"] = {
"keyVaultUrl": self.get_argument_certificate_key_vault_url(),
"identity": identity
}
self.managed_env_def["properties"]["customDomainConfiguration"] = custom_domain

def get_argument_enable_workload_profiles(self):
return self.get_param("enable_workload_profiles")

Expand All @@ -113,9 +163,22 @@ def get_argument_system_assigned(self):

def get_argument_user_assigned(self):
return self.get_param("user_assigned")

def get_argument_certificate_identity(self):
return self.get_param("certificate_identity")

def get_argument_certificate_key_vault_url(self):
return self.get_param("certificate_key_vault_url")


class ContainerappEnvPreviewUpdateDecorator(ContainerAppEnvUpdateDecorator):
def validate_arguments(self):
super().validate_arguments()

# validate custom domain configuration
if self.get_argument_certificate_file() and self.get_argument_certificate_key_vault_url():
raise ValidationError("Cannot use certificate --certificate-file with --certificate-akv-url at the same time")

def set_up_app_log_configuration(self):
logs_destination = self.get_argument_logs_destination()

Expand All @@ -135,5 +198,31 @@ def set_up_app_log_configuration(self):
if self.get_argument_logs_dynamic_json_columns() is not None:
safe_set(self.managed_env_def, "properties", "appLogsConfiguration", "logAnalyticsConfiguration", "dynamicJsonColumns", value=self.get_argument_logs_dynamic_json_columns())

def set_up_custom_domain_configuration(self):
if self.get_argument_certificate_file():
blob, _ = load_cert_file(self.get_argument_certificate_file(), self.get_argument_certificate_password())
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificateValue", value=blob)
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificatePassword", value=self.get_argument_certificate_password())
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificateKeyVaultProperties", value=None)
if self.get_argument_certificate_key_vault_url():
# default use system identity
identity = self.get_argument_certificate_identity()
if not identity:
identity = "system"
if identity.lower() != "system":
subscription_id = get_subscription_id(self.cmd.cli_ctx)
identity = _ensure_identity_resource_id(subscription_id, self.get_argument_resource_group_name(), identity)
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificateKeyVaultProperties", "identity", value=identity)
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificateKeyVaultProperties", "keyVaultUrl", value=self.get_argument_certificate_key_vault_url())
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificateValue", value="")
safe_set(self.managed_env_def, "properties", "customDomainConfiguration", "certificatePassword", value="")

def get_argument_logs_dynamic_json_columns(self):
return self.get_param("logs_dynamic_json_columns")

def get_argument_certificate_identity(self):
return self.get_param("certificate_identity")

def get_argument_certificate_key_vault_url(self):
return self.get_param("certificate_key_vault_url")

13 changes: 9 additions & 4 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
generate_randomized_cert_name, load_cert_file,
generate_randomized_managed_cert_name,
check_managed_cert_name_availability, prepare_managed_certificate_envelop,
trigger_workflow, set_managed_identity, _ensure_identity_resource_id,
trigger_workflow, _ensure_identity_resource_id,
AppType)

from knack.log import get_logger
Expand All @@ -48,7 +48,7 @@
from msrest.exceptions import DeserializationError

from .containerapp_env_certificate_decorator import ContainerappPreviewEnvCertificateListDecorator, \
ContainerappEnvCertificateUploadDecorator
ContainerappEnvCertificatePreviweUploadDecorator
from .connected_env_decorator import ConnectedEnvironmentDecorator, ConnectedEnvironmentCreateDecorator
from .containerapp_job_decorator import ContainerAppJobPreviewCreateDecorator
from .containerapp_env_decorator import ContainerappEnvPreviewCreateDecorator, ContainerappEnvPreviewUpdateDecorator
Expand Down Expand Up @@ -702,6 +702,8 @@ def create_managed_environment(cmd,
hostname=None,
certificate_file=None,
certificate_password=None,
certificate_identity = None,
certificate_key_vault_url=None,
enable_workload_profiles=True,
mtls_enabled=None,
enable_dedicated_gpu=False,
Expand Down Expand Up @@ -736,6 +738,8 @@ def update_managed_environment(cmd,
hostname=None,
certificate_file=None,
certificate_password=None,
certificate_identity = None,
certificate_key_vault_url=None,
tags=None,
workload_profile_type=None,
workload_profile_name=None,
Expand Down Expand Up @@ -1276,16 +1280,17 @@ def list_certificates(cmd, name, resource_group_name, location=None, certificate
return containerapp_env_certificate_list_decorator.list()


def upload_certificate(cmd, name, resource_group_name, certificate_file, certificate_name=None, certificate_password=None, location=None, prompt=False):
def upload_certificate(cmd, name, resource_group_name, certificate_file=None, certificate_name=None, certificate_password=None, location=None, prompt=False, certificate_identity = None, certificate_key_vault_url=None):
raw_parameters = locals()

containerapp_env_certificate_upload_decorator = ContainerappEnvCertificateUploadDecorator(
containerapp_env_certificate_upload_decorator = ContainerappEnvCertificatePreviweUploadDecorator(
cmd=cmd,
client=ManagedEnvironmentPreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)
containerapp_env_certificate_upload_decorator.validate_subscription_registered(CONTAINER_APPS_RP)
containerapp_env_certificate_upload_decorator.validate_arguments()
containerapp_env_certificate_upload_decorator.construct_payload()

return containerapp_env_certificate_upload_decorator.create_or_update()
Expand Down
Loading

0 comments on commit c4baba3

Please sign in to comment.