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

support certificate from keyvault #7259

Merged
merged 9 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
njuCZ marked this conversation as resolved.
Show resolved Hide resolved

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

njuCZ marked this conversation as resolved.
Show resolved Hide resolved

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")
njuCZ marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not call parent construct_payload directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


### 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
Loading