From 5a7c0df3a2bcc26a035c49efc47627ec5abce599 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 7 Mar 2022 14:12:22 -0500 Subject: [PATCH 01/22] Added ingress subgroup. --- .../azext_containerapp/_models.py | 2 +- .../azext_containerapp/_params.py | 9 ++ .../azext_containerapp/commands.py | 10 ++ src/containerapp/azext_containerapp/custom.py | 128 ++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index f0d068b1bbc..e7cf5942bf9 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -137,7 +137,7 @@ "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight "customDomains": None, # [CustomDomain] - # "allowInsecure": None + "allowInsecure": None # Boolean } RegistryCredentials = { diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c38c32711c4..95630ce1705 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -105,3 +105,12 @@ def load_arguments(self, _): with self.argument_context('containerapp revision') as c: c.argument('revision_name', type=str, help='Name of the revision') + + with self.argument_context('containerapp ingress') as c: + c.argument('allow_insecure', help='Allow insecure connections for ingress traffic.') + c.argument('type', validator=validate_ingress, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") + c.argument('transport', arg_type=get_enum_type(['auto', 'http', 'http2']), help="The transport protocol used for ingress traffic.") + c.argument('target_port', type=int, validator=validate_target_port, help="The application port used for ingress traffic.") + + with self.argument_context('containerapp ingress traffic') as c: + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 8fd840ccabd..6bf52492ce4 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -62,3 +62,13 @@ def load_command_table(self, _): g.custom_command('list', 'list_revisions', table_transformer=transform_revision_list_output, exception_handler=ex_handler_factory()) g.custom_command('restart', 'restart_revision') g.custom_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) + + with self.command_group('containerapp ingress') as g: + g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) + g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) + g.custom_command('show', 'show_ingress') + + with self.command_group('containerapp ingress traffic') as g: + g.custom_command('set', 'set_ingress_traffic', exception_handler=ex_handler_factory()) + g.custom_command('show', 'show_ingress_traffic') + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 3ef91290a5f..321d1527169 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -953,3 +953,131 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): except CLIError as e: handle_raw_exception(e) +def show_ingress(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"] + except: + raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) + + +def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, allow_insecure=False, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + external_ingress = None + if type is not None: + if type.lower() == "internal": + external_ingress = False + elif type.lower() == "external": + external_ingress = True + + ingress_def = None + if target_port is not None and type is not None: + ingress_def = IngressModel + ingress_def["external"] = external_ingress + ingress_def["targetPort"] = target_port + ingress_def["transport"] = transport + ingress_def["allowInsecure"] = allow_insecure + + containerapp_def["properties"]["configuration"]["ingress"] = ingress_def + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]["configuration"]["ingress"] + except Exception as e: + handle_raw_exception(e) + +def disable_ingress(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + containerapp_def["properties"]["configuration"]["ingress"] = None + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + logger.warning("Ingress has been disabled successfully.") + return + except Exception as e: + handle_raw_exception(e) + +def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["ingress"] + except: + raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") + + if traffic_weights is not None: + _update_traffic_Weights(containerapp_def, traffic_weights) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]["configuration"]["ingress"]["traffic"] + except Exception as e: + handle_raw_exception(e) + +def show_ingress_traffic(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] + except: + raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") + + + From 801c18ddf37c946136b61001f00f4de075493b10 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 7 Mar 2022 14:25:45 -0500 Subject: [PATCH 02/22] Added help for ingress. --- src/containerapp/azext_containerapp/_help.py | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index ac9638014c7..f8916747784 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -244,3 +244,59 @@ text: | az containerapp env list -g MyResourceGroup """ + +# Ingress Commands +helps['containerapp ingress'] = """ + type: group + short-summary: Commands to manage Containerapp ingress. +""" + +helps['containerapp ingress traffic'] = """ + type: subgroup + short-summary: Commands to manage Containerapp ingress traffic. +""" + +helps['containerapp ingress show'] = """ + type: command + short-summary: Show details of a Containerapp ingress. + examples: + - name: Show the details of a Containerapp ingress. + text: | + az containerapp ingress show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress enable'] = """ + type: command + short-summary: Enable Containerapp ingress. + examples: + - name: Enable Containerapp ingress. + text: | + az containerapp ingress enable -n MyContainerapp -g MyResourceGroup --type external --allow-insecure --target-port 80 --transport auto +""" + +helps['containerapp ingress disable'] = """ + type: command + short-summary: Disable Containerapp ingress. + examples: + - name: Disable Containerapp ingress. + text: | + az containerapp ingress disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress traffic set'] = """ + type: command + short-summary: Set Containerapp ingress traffic. + examples: + - name: Set Containerapp ingress traffic. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=100 +""" + +helps['containerapp ingress traffic show'] = """ + type: command + short-summary: Show Containerapp ingress traffic. + examples: + - name: Show Containerapp ingress traffic. + text: | + az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup +""" \ No newline at end of file From f6698c7f2c3172946655992fbc066f2e57315083 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 7 Mar 2022 14:45:37 -0500 Subject: [PATCH 03/22] Fixed ingress traffic help. --- src/containerapp/azext_containerapp/_help.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index f8916747784..a7841c67b88 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -283,6 +283,11 @@ az containerapp ingress disable -n MyContainerapp -g MyResourceGroup """ +helps['containerapp ingress traffic'] = """ + type: group + short-summary: Commands to manage Containerapp ingress traffic. +""" + helps['containerapp ingress traffic set'] = """ type: command short-summary: Set Containerapp ingress traffic. From 96ab873afccc74677266388e3b278793bf49c325 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 7 Mar 2022 16:15:24 -0500 Subject: [PATCH 04/22] Added registry commands. --- src/containerapp/azext_containerapp/_utils.py | 13 ++ .../azext_containerapp/commands.py | 11 ++ src/containerapp/azext_containerapp/custom.py | 159 +++++++++++++++++- 3 files changed, 182 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0ed9d21bf43..bf0c147bba6 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -293,7 +293,20 @@ def _add_or_update_secrets(containerapp_def, add_secrets): if not is_existing: containerapp_def["properties"]["configuration"]["secrets"].append(new_secret) +def _remove_registry_secret(containerapp_def, server, username): + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + if (urlparse(server).hostname is not None): + registry_secret_name = "{server}-{user}".format(server=urlparse(server).hostname.replace('.', ''), user=username.lower()) + else: + registry_secret_name = "{server}-{user}".format(server=server.replace('.', ''), user=username.lower()) + for i in range(0, len(containerapp_def["properties"]["configuration"]["secrets"])): + existing_secret = containerapp_def["properties"]["configuration"]["secrets"][i] + if existing_secret["name"].lower() == registry_secret_name.lower(): + containerapp_def["properties"]["configuration"]["secrets"].pop(i) + break + def _add_or_update_env_vars(existing_env_vars, new_env_vars): for new_env_var in new_env_vars: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 6bf52492ce4..15bae2d1fb1 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -72,3 +72,14 @@ def load_command_table(self, _): g.custom_command('set', 'set_ingress_traffic', exception_handler=ex_handler_factory()) g.custom_command('show', 'show_ingress_traffic') + with self.command_group('containerapp registry') as g: + g.custom_command('add', 'add_registry', exception_handler=ex_handler_factory()) + g.custom_command('show', 'show_registry') + g.custom_command('list', 'list_registry') + g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) + + + # with self.command_group('containerapp secret') as g: + # g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) + # g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) + # g.custom_command('show', 'show_ingress') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 321d1527169..556b64ccedd 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -10,6 +10,8 @@ from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError +from azure.cli.command_modules.appservice.custom import _get_acr_cred +from urllib.parse import urlparse from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient @@ -33,7 +35,7 @@ _generate_log_analytics_if_not_provided, _get_existing_secrets, _convert_object_from_snake_to_camel_case, _object_to_dict, _add_or_update_secrets, _remove_additional_attributes, _remove_readonly_attributes, _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_Weights, - _get_app_from_revision) + _get_app_from_revision, _remove_registry_secret) logger = get_logger(__name__) @@ -1079,5 +1081,160 @@ def show_ingress_traffic(cmd, name, resource_group_name): except: raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") +def show_registry(cmd, name, resource_group_name, server): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + for r in registries_def: + if r['server'].lower() == server.lower(): + return r + raise CLIError("The containerapp {} does not have specified registry assigned.".format(name)) + +def list_registry(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + +def add_registry(cmd, name, resource_group_name, server, username=None, password=None, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + registries_def = None + registry = None + + if "registries" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["registries"] = [] + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + if not username or not password: + # If registry is Azure Container Registry, we can try inferring credentials + if '.azurecr.io' not in server: + raise RequiredArgumentMissingError('Registry username and password are required if you are not using Azure Container Registry.') + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + + try: + username, password = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') + + # Check if updating existing registry + updating_existing_registry = False + for r in registries_def: + if r['server'].lower() == server.lower(): + updating_existing_registry = True + if username: + r["username"] = username + if password: + r["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + r["username"], + r["server"], + password, + update_existing_secret=True) + + # If not updating existing registry, add as new registry + if not updating_existing_registry: + registry = RegistryCredentialsModel + registry["server"] = server + registry["username"] = username + registry["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + username, + server, + password, + update_existing_secret=True) + # Should this be false? ^ + + registries_def.append(registry) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + + return r["properties"]["configuration"]["registries"] + except Exception as e: + handle_raw_exception(e) + +def delete_registry(cmd, name, resource_group_name, server, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + registries_def = None + registry = None + + try: + containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + for i in range(0, len(registries_def)): + r = registries_def[i] + if r['server'].lower() == server.lower(): + registries_def.pop(i) + _remove_registry_secret(containerapp_def=containerapp_def, server=server, username=r["username"]) + + if len(containerapp_def["properties"]["configuration"]["registries"]) == 0: + containerapp_def["properties"]["configuration"].pop("registries") + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + logger.warning("Registry successfully removed.") + return r["properties"]["configuration"]["registries"] + # No registries to return, so return nothing + except Exception as e: + return From 90ced323916a4f7da3350bea392ae4460ba9f158 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 7 Mar 2022 17:56:46 -0500 Subject: [PATCH 05/22] Updated registry remove util to clear secrets if none remaining. Added warning when updating existing registry. Added registry help. --- src/containerapp/azext_containerapp/_help.py | 42 +++++++++++++++++++ src/containerapp/azext_containerapp/_utils.py | 4 +- src/containerapp/azext_containerapp/custom.py | 1 + 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a7841c67b88..d9011572001 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -304,4 +304,46 @@ - name: Show Containerapp ingress traffic. text: | az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup +""" + +# Registry Commands +helps['containerapp registry'] = """ + type: group + short-summary: Commands to manage Containerapp registries. +""" + +helps['containerapp registry show'] = """ + type: command + short-summary: Show details of a Containerapp registry. + examples: + - name: Show the details of a Containerapp registry. + text: | + az containerapp registry show -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +helps['containerapp registry list'] = """ + type: command + short-summary: List registries assigned to a Containerapp. + examples: + - name: Show the details of a Containerapp registry. + text: | + az containerapp registry list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp registry add'] = """ + type: command + short-summary: Add a registry to a Containerapp. + examples: + - name: Add a registry to a Containerapp. + text: | + az containerapp registry add -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +helps['containerapp registry delete'] = """ + type: command + short-summary: Delete a registry from a Containerapp. + examples: + - name: Delete a registry from a Containerapp. + text: | + az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index bf0c147bba6..5a49b9a048b 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -306,7 +306,9 @@ def _remove_registry_secret(containerapp_def, server, username): if existing_secret["name"].lower() == registry_secret_name.lower(): containerapp_def["properties"]["configuration"]["secrets"].pop(i) break - + if len(containerapp_def["properties"]["configuration"]["secrets"]) == 0: + containerapp_def["properties"]["configuration"].pop("secrets") + def _add_or_update_env_vars(existing_env_vars, new_env_vars): for new_env_var in new_env_vars: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 556b64ccedd..5a6b94399f2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1161,6 +1161,7 @@ def add_registry(cmd, name, resource_group_name, server, username=None, password updating_existing_registry = False for r in registries_def: if r['server'].lower() == server.lower(): + logger.warning("Updating existing registry.") updating_existing_registry = True if username: r["username"] = username From e48b576a28d2fe143ef7c8e1b0217094bdab17cc Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 12:36:55 -0500 Subject: [PATCH 06/22] Changed registry delete to remove. --- src/containerapp/azext_containerapp/_help.py | 8 ++++---- src/containerapp/azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index d9011572001..a6a7becb730 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -339,11 +339,11 @@ az containerapp registry add -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ -helps['containerapp registry delete'] = """ +helps['containerapp registry remove'] = """ type: command - short-summary: Delete a registry from a Containerapp. + short-summary: Remove a registry from a Containerapp. examples: - - name: Delete a registry from a Containerapp. + - name: Remove a registry from a Containerapp. text: | - az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + az containerapp registry remove -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 15bae2d1fb1..754278f9d38 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -76,7 +76,7 @@ def load_command_table(self, _): g.custom_command('add', 'add_registry', exception_handler=ex_handler_factory()) g.custom_command('show', 'show_registry') g.custom_command('list', 'list_registry') - g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) + g.custom_command('remove', 'remove_registry', exception_handler=ex_handler_factory()) # with self.command_group('containerapp secret') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5a6b94399f2..afe2c4f5fbe 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1196,7 +1196,7 @@ def add_registry(cmd, name, resource_group_name, server, username=None, password except Exception as e: handle_raw_exception(e) -def delete_registry(cmd, name, resource_group_name, server, no_wait=False): +def remove_registry(cmd, name, resource_group_name, server, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None From 4a71d20b46743ff619c41cd99118f7d3069e07fa Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 12:45:45 -0500 Subject: [PATCH 07/22] Added error message if user tries to remove non assigned registry. --- src/containerapp/azext_containerapp/custom.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index afe2c4f5fbe..8a13f64a3bc 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1220,11 +1220,16 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): registries_def = containerapp_def["properties"]["configuration"]["registries"] + wasRemoved = False for i in range(0, len(registries_def)): r = registries_def[i] if r['server'].lower() == server.lower(): registries_def.pop(i) _remove_registry_secret(containerapp_def=containerapp_def, server=server, username=r["username"]) + wasRemoved = True + + if not wasRemoved: + raise CLIError("Containerapp does not have registry server {} assigned.".format(server)) if len(containerapp_def["properties"]["configuration"]["registries"]) == 0: containerapp_def["properties"]["configuration"].pop("registries") From a5fa2ec4f870c7aed1a1dea12b3237aabdc7d0a9 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 14:51:48 -0500 Subject: [PATCH 08/22] Changed registry add back to registry set. --- src/containerapp/azext_containerapp/_help.py | 8 ++++++-- src/containerapp/azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a6a7becb730..6275ee173ad 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -330,13 +330,17 @@ az containerapp registry list -n MyContainerapp -g MyResourceGroup """ -helps['containerapp registry add'] = """ +helps['containerapp registry set'] = """ type: command - short-summary: Add a registry to a Containerapp. + short-summary: Add or update a Containerapp registry. examples: - name: Add a registry to a Containerapp. text: | az containerapp registry add -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + - name: Update a Containerapp registry. + text: | + az containerapp registry add -n MyContainerapp -g MyResourceGroup --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword + """ helps['containerapp registry remove'] = """ diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 754278f9d38..8eced0bc409 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -73,7 +73,7 @@ def load_command_table(self, _): g.custom_command('show', 'show_ingress_traffic') with self.command_group('containerapp registry') as g: - g.custom_command('add', 'add_registry', exception_handler=ex_handler_factory()) + g.custom_command('set', 'set_registry', exception_handler=ex_handler_factory()) g.custom_command('show', 'show_registry') g.custom_command('list', 'list_registry') g.custom_command('remove', 'remove_registry', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 8a13f64a3bc..5a86d05b7e8 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1122,7 +1122,7 @@ def list_registry(cmd, name, resource_group_name): except: raise CLIError("The containerapp {} has no assigned registries.".format(name)) -def add_registry(cmd, name, resource_group_name, server, username=None, password=None, no_wait=False): +def set_registry(cmd, name, resource_group_name, server, username=None, password=None, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None From ac8d4bd29300630bfc46e0a79883beafd2436f47 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 16:22:01 -0500 Subject: [PATCH 09/22] Added secret subgroup commands. --- src/containerapp/azext_containerapp/_utils.py | 14 +-- .../azext_containerapp/commands.py | 10 +- src/containerapp/azext_containerapp/custom.py | 101 +++++++++++++++++- 3 files changed, 113 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 5a49b9a048b..dc181d9e98e 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -294,20 +294,22 @@ def _add_or_update_secrets(containerapp_def, add_secrets): containerapp_def["properties"]["configuration"]["secrets"].append(new_secret) def _remove_registry_secret(containerapp_def, server, username): - if "secrets" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["secrets"] = [] - if (urlparse(server).hostname is not None): registry_secret_name = "{server}-{user}".format(server=urlparse(server).hostname.replace('.', ''), user=username.lower()) else: registry_secret_name = "{server}-{user}".format(server=server.replace('.', ''), user=username.lower()) + + _remove_secret(containerapp_def, secret_name=registry_secret_name) + +def _remove_secret(containerapp_def, secret_name): + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + for i in range(0, len(containerapp_def["properties"]["configuration"]["secrets"])): existing_secret = containerapp_def["properties"]["configuration"]["secrets"][i] - if existing_secret["name"].lower() == registry_secret_name.lower(): + if existing_secret["name"].lower() == secret_name.lower(): containerapp_def["properties"]["configuration"]["secrets"].pop(i) break - if len(containerapp_def["properties"]["configuration"]["secrets"]) == 0: - containerapp_def["properties"]["configuration"].pop("secrets") def _add_or_update_env_vars(existing_env_vars, new_env_vars): for new_env_var in new_env_vars: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 8eced0bc409..17447759f90 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -78,8 +78,8 @@ def load_command_table(self, _): g.custom_command('list', 'list_registry') g.custom_command('remove', 'remove_registry', exception_handler=ex_handler_factory()) - - # with self.command_group('containerapp secret') as g: - # g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) - # g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) - # g.custom_command('show', 'show_ingress') \ No newline at end of file + with self.command_group('containerapp secret') as g: + g.custom_command('list', 'list_secrets') + g.custom_command('show', 'show_secret') + g.custom_command('delete', 'delete_secret', exception_handler=ex_handler_factory()) + g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5a86d05b7e8..2fa8e270125 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -35,7 +35,7 @@ _generate_log_analytics_if_not_provided, _get_existing_secrets, _convert_object_from_snake_to_camel_case, _object_to_dict, _add_or_update_secrets, _remove_additional_attributes, _remove_readonly_attributes, _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_Weights, - _get_app_from_revision, _remove_registry_secret) + _get_app_from_revision, _remove_registry_secret, _remove_secret) logger = get_logger(__name__) @@ -1243,4 +1243,103 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): except Exception as e: return +def list_secrets(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + raise CLIError("The containerapp {} has no assigned secrets.".format(name)) + +def show_secret(cmd, name, resource_group_name, secret_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + r = ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) + for secret in r["value"]: + if secret["name"].lower() == secret_name.lower(): + return secret + raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + +def delete_secret(cmd, name, resource_group_name, secret_name, no_wait = False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + wasRemoved = False + for secret in containerapp_def["properties"]["configuration"]["secrets"]: + if secret["name"].lower() == secret_name.lower(): + _remove_secret(containerapp_def, secret_name=secret["name"]) + wasRemoved = True + break + if not wasRemoved: + raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + logger.warning("Secret successfully removed.") + return r["properties"]["configuration"]["secrets"] + except Exception as e: + handle_raw_exception(e) + +def set_secrets(cmd, name, resource_group_name, secrets=[], yaml=None, no_wait = False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if not yaml and not secrets: + raise RequiredArgumentMissingError('Usage error: --secrets is required if not using --yaml') + + if yaml: + yaml_secrets = load_yaml_file(yaml).split(' ') + try: + parse_secret_flags(yaml_secrets) + except: + raise CLIError("YAML secrets must be a list of secrets in key=value format, delimited by new line.") + for secret in yaml_secrets: + secrets.append(secret.strip()) + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]["configuration"]["secrets"] + except Exception as e: + handle_raw_exception(e) + From 7f9a9df849f14aa7279e309969c7bcbd555888d4 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 16:32:11 -0500 Subject: [PATCH 10/22] Removed yaml support from secret set. --- src/containerapp/azext_containerapp/custom.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2fa8e270125..64dfcf0ae1f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1256,7 +1256,7 @@ def list_secrets(cmd, name, resource_group_name): raise CLIError("The containerapp '{}' does not exist".format(name)) try: - return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) + return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] except: raise CLIError("The containerapp {} has no assigned secrets.".format(name)) @@ -1308,20 +1308,26 @@ def delete_secret(cmd, name, resource_group_name, secret_name, no_wait = False): except Exception as e: handle_raw_exception(e) -def set_secrets(cmd, name, resource_group_name, secrets=[], yaml=None, no_wait = False): +def set_secrets(cmd, name, resource_group_name, secrets, + #secrets=None, + #yaml=None, + no_wait = False): _validate_subscription_registered(cmd, "Microsoft.App") - if not yaml and not secrets: - raise RequiredArgumentMissingError('Usage error: --secrets is required if not using --yaml') - - if yaml: - yaml_secrets = load_yaml_file(yaml).split(' ') - try: - parse_secret_flags(yaml_secrets) - except: - raise CLIError("YAML secrets must be a list of secrets in key=value format, delimited by new line.") - for secret in yaml_secrets: - secrets.append(secret.strip()) + # if not yaml and not secrets: + # raise RequiredArgumentMissingError('Usage error: --secrets is required if not using --yaml') + + # if not secrets: + # secrets = [] + + # if yaml: + # yaml_secrets = load_yaml_file(yaml).split(' ') + # try: + # parse_secret_flags(yaml_secrets) + # except: + # raise CLIError("YAML secrets must be a list of secrets in key=value format, delimited by new line.") + # for secret in yaml_secrets: + # secrets.append(secret.strip()) containerapp_def = None try: From 60ea5964d940d8aa55548b81c9e611546d965dfa Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 16:49:27 -0500 Subject: [PATCH 11/22] Changed secret add to secret set. Updated consistency between secret set and secret delete. Added secret help. Require at least one secret passed with --secrets for secret commands. --- src/containerapp/azext_containerapp/_help.py | 49 ++++++++++++++++++- .../azext_containerapp/_params.py | 4 ++ .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 25 ++++++---- 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 6275ee173ad..7c54dedd494 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -336,10 +336,10 @@ examples: - name: Add a registry to a Containerapp. text: | - az containerapp registry add -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io - name: Update a Containerapp registry. text: | - az containerapp registry add -n MyContainerapp -g MyResourceGroup --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword + az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword """ @@ -350,4 +350,49 @@ - name: Remove a registry from a Containerapp. text: | az containerapp registry remove -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +# Secret Commands +helps['containerapp secret'] = """ + type: group + short-summary: Commands to manage Containerapp secrets. +""" + +helps['containerapp secret show'] = """ + type: command + short-summary: Show details of a Containerapp secret. + examples: + - name: Show the details of a Containerapp secret. + text: | + az containerapp secret show -n MyContainerapp -g MyResourceGroup --secret-name MySecret +""" + +helps['containerapp secret list'] = """ + type: command + short-summary: List the secrets of a Containerapp. + examples: + - name: List the secrets of a Containerapp. + text: | + az containerapp secret list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp secret delete'] = """ + type: command + short-summary: Delete secrets from a Containerapp. + examples: + - name: Delete secrets from a Containerapp. + text: | + az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secrets MySecret MySecret2 +""" + +helps['containerapp secret set'] = """ + type: command + short-summary: Create/update Containerapp secrets. + examples: + - name: Add a secret to a Containerapp. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName=MySecretValue + - name: Update a Containerapp secret. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 95630ce1705..9c049b5c4ad 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -114,3 +114,7 @@ def load_arguments(self, _): with self.argument_context('containerapp ingress traffic') as c: c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + + with self.argument_context('containerapp secret') as c: + c.argument('secrets', nargs='+', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") + diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 17447759f90..4f6fc4e16ea 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -81,5 +81,5 @@ def load_command_table(self, _): with self.command_group('containerapp secret') as g: g.custom_command('list', 'list_secrets') g.custom_command('show', 'show_secret') - g.custom_command('delete', 'delete_secret', exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 64dfcf0ae1f..e313f15e2a4 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1278,7 +1278,7 @@ def show_secret(cmd, name, resource_group_name, secret_name): return secret raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) -def delete_secret(cmd, name, resource_group_name, secret_name, no_wait = False): +def delete_secrets(cmd, name, resource_group_name, secrets, no_wait = False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1292,19 +1292,24 @@ def delete_secret(cmd, name, resource_group_name, secret_name, no_wait = False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - wasRemoved = False - for secret in containerapp_def["properties"]["configuration"]["secrets"]: - if secret["name"].lower() == secret_name.lower(): - _remove_secret(containerapp_def, secret_name=secret["name"]) - wasRemoved = True - break - if not wasRemoved: - raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + for secret_name in secrets: + wasRemoved = False + for secret in containerapp_def["properties"]["configuration"]["secrets"]: + if secret["name"].lower() == secret_name.lower(): + _remove_secret(containerapp_def, secret_name=secret["name"]) + wasRemoved = True + break + if not wasRemoved: + raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) try: r = ContainerAppClient.create_or_update( cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) logger.warning("Secret successfully removed.") - return r["properties"]["configuration"]["secrets"] + try: + return r["properties"]["configuration"]["secrets"] + # No secrets to return + except: + pass except Exception as e: handle_raw_exception(e) From 13a180e39f46fc6463cf4214ddd73ec8eff65294 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 17:13:09 -0500 Subject: [PATCH 12/22] Changed param name for secret delete from --secrets to --secret-names. Updated help. --- src/containerapp/azext_containerapp/_help.py | 2 +- src/containerapp/azext_containerapp/_params.py | 4 +++- src/containerapp/azext_containerapp/custom.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 7c54dedd494..dd8f003dfc9 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -382,7 +382,7 @@ examples: - name: Delete secrets from a Containerapp. text: | - az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secrets MySecret MySecret2 + az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 """ helps['containerapp secret set'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 9c049b5c4ad..700577d472f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -115,6 +115,8 @@ def load_arguments(self, _): with self.argument_context('containerapp ingress traffic') as c: c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") - with self.argument_context('containerapp secret') as c: + with self.argument_context('containerapp secret set') as c: c.argument('secrets', nargs='+', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") + with self.argument_context('containerapp secret delete') as c: + c.argument('secret_names', nargs='+', help="A list of secret(s) for the containerapp. Space-separated secret values names.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index e313f15e2a4..943b8595285 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1278,7 +1278,7 @@ def show_secret(cmd, name, resource_group_name, secret_name): return secret raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) -def delete_secrets(cmd, name, resource_group_name, secrets, no_wait = False): +def delete_secrets(cmd, name, resource_group_name, secret_names, no_wait = False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1292,7 +1292,7 @@ def delete_secrets(cmd, name, resource_group_name, secrets, no_wait = False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - for secret_name in secrets: + for secret_name in secret_names: wasRemoved = False for secret in containerapp_def["properties"]["configuration"]["secrets"]: if secret["name"].lower() == secret_name.lower(): @@ -1304,7 +1304,7 @@ def delete_secrets(cmd, name, resource_group_name, secrets, no_wait = False): try: r = ContainerAppClient.create_or_update( cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) - logger.warning("Secret successfully removed.") + logger.warning("Secret(s) successfully removed.") try: return r["properties"]["configuration"]["secrets"] # No secrets to return From 70be126853d9ffbdae5425a653bf8e2d7b3a7217 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 17:27:07 -0500 Subject: [PATCH 13/22] Changed registry remove to registry delete. --- src/containerapp/azext_containerapp/_help.py | 8 ++++---- src/containerapp/azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index dd8f003dfc9..de611290562 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -343,13 +343,13 @@ """ -helps['containerapp registry remove'] = """ +helps['containerapp registry delete'] = """ type: command - short-summary: Remove a registry from a Containerapp. + short-summary: Delete a registry from a Containerapp. examples: - - name: Remove a registry from a Containerapp. + - name: Delete a registry from a Containerapp. text: | - az containerapp registry remove -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ # Secret Commands diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 4f6fc4e16ea..92d16dfb9e9 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -76,7 +76,7 @@ def load_command_table(self, _): g.custom_command('set', 'set_registry', exception_handler=ex_handler_factory()) g.custom_command('show', 'show_registry') g.custom_command('list', 'list_registry') - g.custom_command('remove', 'remove_registry', exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) with self.command_group('containerapp secret') as g: g.custom_command('list', 'list_secrets') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 943b8595285..2016d6b0dc2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1196,7 +1196,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password except Exception as e: handle_raw_exception(e) -def remove_registry(cmd, name, resource_group_name, server, no_wait=False): +def delete_registry(cmd, name, resource_group_name, server, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None From 479993225df18a6cf986c4d9d3da6337f0ec1f5e Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 8 Mar 2022 17:31:51 -0500 Subject: [PATCH 14/22] Fixed bug in registry delete. --- src/containerapp/azext_containerapp/custom.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2016d6b0dc2..4cd3d364633 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1227,6 +1227,7 @@ def delete_registry(cmd, name, resource_group_name, server, no_wait=False): registries_def.pop(i) _remove_registry_secret(containerapp_def=containerapp_def, server=server, username=r["username"]) wasRemoved = True + break if not wasRemoved: raise CLIError("Containerapp does not have registry server {} assigned.".format(server)) From b2bfd1f1b371a97afd11665f84deee742e5c2fc9 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 9 Mar 2022 17:35:41 -0500 Subject: [PATCH 15/22] Added revision mode set and revision copy. --- src/containerapp/azext_containerapp/_help.py | 18 ++ .../azext_containerapp/_params.py | 5 +- .../azext_containerapp/commands.py | 4 + src/containerapp/azext_containerapp/custom.py | 187 +++++++++++++++++- 4 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index de611290562..bc94375cfc5 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -189,6 +189,24 @@ az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup """ +helps['containerapp revision mode set'] = """ + type: command + short-summary: Set the revision mode of a Containerapp. + examples: + - name: Set the revision mode of a Containerapp. + text: | + az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi +""" + # Environment Commands helps['containerapp env'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 700577d472f..1169cdd29a4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -104,7 +104,10 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the managed Environment.') with self.argument_context('containerapp revision') as c: - c.argument('revision_name', type=str, help='Name of the revision') + c.argument('revision_name', options_list=['--revision'], type=str, help='Name of the revision.') + + with self.argument_context('containerapp revision copy') as c: + c.argument('from_revision', type=str, help='Revision to copy from. Default: latest revision.') with self.argument_context('containerapp ingress') as c: c.argument('allow_insecure', help='Allow insecure connections for ingress traffic.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 92d16dfb9e9..d3dee61eefc 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -62,6 +62,10 @@ def load_command_table(self, _): g.custom_command('list', 'list_revisions', table_transformer=transform_revision_list_output, exception_handler=ex_handler_factory()) g.custom_command('restart', 'restart_revision') g.custom_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) + g.custom_command('copy', 'copy_revision', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp revision mode') as g: + g.custom_command('set', 'set_revision_mode', exception_handler=ex_handler_factory()) with self.command_group('containerapp ingress') as g: g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4cd3d364633..34458163a3c 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -955,6 +955,192 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): except CLIError as e: handle_raw_exception(e) +def copy_revision(cmd, + name, + resource_group_name, + from_revision=None, + #label=None, + yaml=None, + image=None, + image_name=None, + min_replicas=None, + max_replicas=None, + env_vars=None, + cpu=None, + memory=None, + revision_suffix=None, + startup_command=None, + traffic_weights=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if yaml: + if image or min_replicas or max_replicas or\ + env_vars or cpu or memory or \ + startup_command or args or tags: + logger.warning('Additional flags were passed along with --yaml. These flags will be ignored, and the configuration defined in the yaml will be used instead') + return update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait) + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + if not from_revision: + from_revision = containerapp_def["properties"]["latestRevisionName"] + + try: + r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) + except CLIError as e: + # Error handle the case where revision not found? + handle_raw_exception(e) + + containerapp_def["properties"]["template"] = r["properties"]["template"] + + update_map = {} + update_map['ingress'] = traffic_weights + update_map['scale'] = min_replicas or max_replicas + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['configuration'] = update_map['ingress'] + + if tags: + _add_or_update_tags(containerapp_def, tags) + + if revision_suffix is not None: + containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix + + # Containers + if update_map["container"]: + if not image_name: + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + else: + raise ValidationError("Usage error: --image-name is required when adding or updating a container") + + # Check if updating existing container + updating_existing_container = False + for c in containerapp_def["properties"]["template"]["containers"]: + if c["name"].lower() == image_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + if env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + if startup_command is not None: + if isinstance(startup_command, list) and not startup_command: + c["command"] = None + else: + c["command"] = startup_command + if args is not None: + if isinstance(args, list) and not args: + c["args"] = None + else: + c["args"] = args + if cpu is not None or memory is not None: + if "resources" in c and c["resources"]: + if cpu is not None: + c["resources"]["cpu"] = cpu + if memory is not None: + c["resources"]["memory"] = memory + else: + c["resources"] = { + "cpu": cpu, + "memory": memory + } + + # If not updating existing container, add as new container + if not updating_existing_container: + if image is None: + raise ValidationError("Usage error: --image is required when adding a new container") + + resources_def = None + if cpu is not None or memory is not None: + resources_def = ContainerResourcesModel + resources_def["cpu"] = cpu + resources_def["memory"] = memory + + container_def = ContainerModel + container_def["name"] = image_name + container_def["image"] = image + if env_vars is not None: + container_def["env"] = parse_env_var_flags(env_vars) + if startup_command is not None: + if isinstance(startup_command, list) and not startup_command: + container_def["command"] = None + else: + container_def["command"] = startup_command + if args is not None: + if isinstance(args, list) and not args: + container_def["args"] = None + else: + container_def["args"] = args + if resources_def is not None: + container_def["resources"] = resources_def + + containerapp_def["properties"]["template"]["containers"].append(container_def) + + # Scale + if update_map["scale"]: + if "scale" not in containerapp_def["properties"]["template"]: + containerapp_def["properties"]["template"]["scale"] = {} + if min_replicas is not None: + containerapp_def["properties"]["template"]["scale"]["minReplicas"] = min_replicas + if max_replicas is not None: + containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas + + # Configuration + if update_map["ingress"]: + if "ingress" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["ingress"] = {} + + if traffic_weights is not None: + _update_traffic_Weights(containerapp_def, traffic_weights) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp update in progress. Please monitor the update using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + + return r + except Exception as e: + handle_raw_exception(e) + +def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = mode.lower() + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]["configuration"]["activeRevisionsMode"] + except Exception as e: + handle_raw_exception(e) + def show_ingress(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -972,7 +1158,6 @@ def show_ingress(cmd, name, resource_group_name): except: raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) - def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, allow_insecure=False, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") From df96dab17aef197cf61eeb2be027b60d9e3d4ef3 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 10 Mar 2022 13:12:58 -0500 Subject: [PATCH 16/22] Added dapr enable and dapr disable. Need to test more. --- src/containerapp/azext_containerapp/_help.py | 26 ++++++++- .../azext_containerapp/_params.py | 5 ++ .../azext_containerapp/commands.py | 4 ++ src/containerapp/azext_containerapp/custom.py | 57 +++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index bc94375cfc5..f13e6ca6cd4 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -413,4 +413,28 @@ - name: Update a Containerapp secret. text: | az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue -""" \ No newline at end of file +""" + +# Dapr Commands +helps['containerapp dapr'] = """ + type: group + short-summary: Commands to manage Containerapp dapr. +""" + +helps['containerapp dapr enable'] = """ + type: command + short-summary: Enable dapr for a Containerapp. + examples: + - name: Enable dapr for a Containerapp. + text: | + az containerapp dapr enable -n MyContainerapp -g MyResourceGroup --dapr-app-id my-app-id --dapr-app-port 8080 +""" + +helps['containerapp dapr disable'] = """ + type: command + short-summary: Disable dapr for a Containerapp. + examples: + - name: Disable dapr for a Containerapp. + text: | + az containerapp dapr disable -n MyContainerapp -g MyResourceGroup +""" diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 1169cdd29a4..bcf34d6aa1d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -123,3 +123,8 @@ def load_arguments(self, _): with self.argument_context('containerapp secret delete') as c: c.argument('secret_names', nargs='+', help="A list of secret(s) for the containerapp. Space-separated secret values names.") + + with self.argument_context('containerapp dapr') as c: + c.argument('dapr_app_id', help="The dapr app id.") + c.argument('dapr_app_port', help="The port of your app.") + c.argument('dapr_app_protocol', help="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index d3dee61eefc..ea42b316b57 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -87,3 +87,7 @@ def load_command_table(self, _): g.custom_command('show', 'show_secret') g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp dapr') as g: + g.custom_command('enable', 'enable_dapr', exception_handler=ex_handler_factory()) + g.custom_command('disable', 'disable_dapr', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 34458163a3c..f545d2691c2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1539,4 +1539,61 @@ def set_secrets(cmd, name, resource_group_name, secrets, except Exception as e: handle_raw_exception(e) +def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port=None, dapr_app_protocol=None): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + if 'dapr' not in containerapp_def['properties']: + containerapp_def['properties']['dapr'] = {} + + if dapr_app_id: + containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id + + if dapr_app_port: + containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port + + if dapr_app_protocol: + containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol + + containerapp_def['properties']['dapr']['enabled'] = True + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]['dapr'] + except Exception as e: + handle_raw_exception(e) + +def disable_dapr(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + containerapp_def['properties']['dapr']['enabled'] = False + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + return r["properties"]['dapr'] + except Exception as e: + handle_raw_exception(e) From a0d5163130ef11d2c92f2976eb772cdf10031d0a Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 14 Mar 2022 15:03:18 -0400 Subject: [PATCH 17/22] Added list, show, set dapr component. Added dapr enable, disable. --- .../azext_containerapp/_clients.py | 116 ++++++++++++++++++ .../azext_containerapp/_models.py | 22 ++++ src/containerapp/azext_containerapp/_utils.py | 17 +++ .../azext_containerapp/commands.py | 4 + src/containerapp/azext_containerapp/custom.py | 81 +++++++++++- 5 files changed, 236 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 4d525bee181..6e257e3dc8a 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -523,3 +523,119 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) env_list.append(formatted) return env_list + +class DaprComponentClient(): + @classmethod + def create_or_update(cls, cmd, resource_group_name, environemnt_name, name, dapr_component_envelope, no_wait=False): + #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environemnt_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(dapr_component_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + return poll(cmd, request_url, "inprogress") + + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?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, "DELETE", request_url) + + if no_wait: + return # API doesn't return JSON (it returns no content) + elif r.status_code in [200, 201, 202, 204]: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + if r.status_code == 202: + from azure.cli.core.azclierror import ResourceNotFoundError + try: + poll(cmd, request_url, "cancelled") + except ResourceNotFoundError: + pass + logger.warning('Containerapp successfully deleted') + return + + @classmethod + def show(cls, cmd, resource_group_name, environment_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + @classmethod + def list(cls, cmd, resource_group_name, environment_name, formatter=lambda x: x): + app_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents?api-version={}".format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + app_list.append(formatted) + + while j.get("nextLink") is not None: + request_url = j["nextLink"] + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + app_list.append(formatted) + + return app_list + diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index e7cf5942bf9..4bb8915a0e4 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -180,3 +180,25 @@ }, "tags": None } + +DaprComponent = { + "properties": { + "componentType": None, #String + "version": None, + "ignoreErrors": None, + "initTimeout": None, + "secrets": None, + "metadata": None, + "scopes": None + } +} + +DaprMetadata = { + "key": None, #str + "value": None, #str + "secret_ref": None #str +} + + + + diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index dc181d9e98e..f1905f0269c 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -397,6 +397,23 @@ def _remove_readonly_attributes(containerapp_def): elif unneeded_property in containerapp_def['properties']: del containerapp_def['properties'][unneeded_property] +def _remove_dapr_readonly_attributes(daprcomponent_def): + unneeded_properties = [ + "id", + "name", + "type", + "systemData", + "provisioningState", + "latestRevisionName", + "latestRevisionFqdn", + "customDomainVerificationId", + "outboundIpAddresses", + "fqdn" + ] + + for unneeded_property in unneeded_properties: + if unneeded_property in daprcomponent_def: + del daprcomponent_def[unneeded_property] def update_nested_dictionary(orig_dict, new_dict): # Recursively update a nested dictionary. If the value is a list, replace the old list with new list diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index ea42b316b57..2687ba1ca8a 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -91,3 +91,7 @@ def load_command_table(self, _): with self.command_group('containerapp dapr') as g: g.custom_command('enable', 'enable_dapr', exception_handler=ex_handler_factory()) g.custom_command('disable', 'disable_dapr', exception_handler=ex_handler_factory()) + g.custom_command('list', 'list_dapr_components') + g.custom_command('show', 'show_dapr_component') + g.custom_command('set', 'create_or_update_dapr_component') + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f545d2691c2..f16230602c2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -14,7 +14,7 @@ from urllib.parse import urlparse from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient, ContainerAppClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient, DaprComponentClient from ._sdk_models import * from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, @@ -35,7 +35,7 @@ _generate_log_analytics_if_not_provided, _get_existing_secrets, _convert_object_from_snake_to_camel_case, _object_to_dict, _add_or_update_secrets, _remove_additional_attributes, _remove_readonly_attributes, _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_Weights, - _get_app_from_revision, _remove_registry_secret, _remove_secret) + _get_app_from_revision, _remove_registry_secret, _remove_secret, _remove_dapr_readonly_attributes) logger = get_logger(__name__) @@ -1539,7 +1539,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, except Exception as e: handle_raw_exception(e) -def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port=None, dapr_app_protocol=None): +def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port=None, dapr_app_protocol=None, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1574,7 +1574,7 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= except Exception as e: handle_raw_exception(e) -def disable_dapr(cmd, name, resource_group_name): +def disable_dapr(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1597,3 +1597,76 @@ def disable_dapr(cmd, name, resource_group_name): except Exception as e: handle_raw_exception(e) +def list_dapr_components(cmd, resource_group_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + return DaprComponentClient.list(cmd, resource_group_name, environment_name) + +def show_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + return DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) + +def create_or_update_dapr_component(cmd, resource_group_name, environment_name, name, yaml): + + yaml_containerapp = load_yaml_file(yaml) + if type(yaml_containerapp) != dict: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + # Deserialize the yaml into a DaprComponent object. Need this since we're not using SDK + daprcomponent_def = None + try: + deserializer = create_deserializer() + + daprcomponent_def = deserializer('DaprComponent', yaml_containerapp) + except DeserializationError as ex: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + #daprcomponent_def = _object_to_dict(daprcomponent_def) + daprcomponent_def = _convert_object_from_snake_to_camel_case(_object_to_dict(daprcomponent_def)) + + # Remove "additionalProperties" and read-only attributes that are introduced in the deserialization. Need this since we're not using SDK + _remove_additional_attributes(daprcomponent_def) + _remove_dapr_readonly_attributes(daprcomponent_def) + + if not daprcomponent_def["ignoreErrors"]: + daprcomponent_def["ignoreErrors"] = False + + dapr_component_envelope = {} + + dapr_component_envelope["properties"] = daprcomponent_def + + # r = DaprComponentClient.create_or_update(cmd, resource_group_name, environment_name, name, dapr_component_envelope) + # return r + + try: + r = DaprComponentClient.create_or_update(cmd, resource_group_name, environment_name, name, dapr_component_envelope) + return r + except Exception as e: + handle_raw_exception(e) + + # secrets + # metadata + # scopes (containerapps) + +def delete_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} + + +# DaprComponent = { +# "properties": { +# "componentType": None, #String +# "version": None, +# "ignoreErrors": None, +# "initTimeout": None, +# "secrets": None, +# "metadata": None, +# "scopes": None +# } +# } + +# DaprMetadata = { +# "key": None, #str +# "value": None, #str +# "secret_ref": None #str +# } From b41e34f5daeb0bc053aa8424149077d35bfdd264 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 14 Mar 2022 15:22:13 -0400 Subject: [PATCH 18/22] Added delete dapr delete. --- .../azext_containerapp/_clients.py | 17 ++++--- .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 46 +++++++------------ 3 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 6e257e3dc8a..e86c3c0d545 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -526,7 +526,7 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) class DaprComponentClient(): @classmethod - def create_or_update(cls, cmd, resource_group_name, environemnt_name, name, dapr_component_envelope, no_wait=False): + def create_or_update(cls, cmd, resource_group_name, environment_name, name, dapr_component_envelope, no_wait=False): #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager @@ -537,7 +537,7 @@ def create_or_update(cls, cmd, resource_group_name, environemnt_name, name, dapr management_hostname.strip('/'), sub_id, resource_group_name, - environemnt_name, + environment_name, name, api_version) @@ -546,11 +546,12 @@ def create_or_update(cls, cmd, resource_group_name, environemnt_name, name, dapr if no_wait: return r.json() elif r.status_code == 201: - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" request_url = url_fmt.format( management_hostname.strip('/'), sub_id, resource_group_name, + environment_name, name, api_version) return poll(cmd, request_url, "inprogress") @@ -558,15 +559,16 @@ def create_or_update(cls, cmd, resource_group_name, environemnt_name, name, dapr return r.json() @classmethod - def delete(cls, cmd, resource_group_name, name, no_wait=False): + def delete(cls, cmd, resource_group_name, environment_name, name, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" request_url = url_fmt.format( management_hostname.strip('/'), sub_id, resource_group_name, + environment_name, name, api_version) @@ -575,11 +577,12 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): if no_wait: return # API doesn't return JSON (it returns no content) elif r.status_code in [200, 201, 202, 204]: - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" request_url = url_fmt.format( management_hostname.strip('/'), sub_id, resource_group_name, + environment_name, name, api_version) @@ -589,7 +592,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): poll(cmd, request_url, "cancelled") except ResourceNotFoundError: pass - logger.warning('Containerapp successfully deleted') + logger.warning('Dapr component successfully deleted') return @classmethod diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 2687ba1ca8a..34e8afdbc26 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -94,4 +94,5 @@ def load_command_table(self, _): g.custom_command('list', 'list_dapr_components') g.custom_command('show', 'show_dapr_component') g.custom_command('set', 'create_or_update_dapr_component') + g.custom_command('delete', 'delete_dapr_component') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f16230602c2..cfb51abda9e 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1607,7 +1607,8 @@ def show_dapr_component(cmd, resource_group_name, dapr_component_name, environme return DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) -def create_or_update_dapr_component(cmd, resource_group_name, environment_name, name, yaml): +def create_or_update_dapr_component(cmd, resource_group_name, environment_name, dapr_component_name, yaml): + _validate_subscription_registered(cmd, "Microsoft.App") yaml_containerapp = load_yaml_file(yaml) if type(yaml_containerapp) != dict: @@ -1636,37 +1637,24 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, dapr_component_envelope["properties"] = daprcomponent_def - # r = DaprComponentClient.create_or_update(cmd, resource_group_name, environment_name, name, dapr_component_envelope) - # return r - try: - r = DaprComponentClient.create_or_update(cmd, resource_group_name, environment_name, name, dapr_component_envelope) + r = DaprComponentClient.create_or_update(cmd, resource_group_name, environment_name, dapr_component_envelope, name=dapr_component_name) return r except Exception as e: handle_raw_exception(e) - # secrets - # metadata - # scopes (containerapps) - def delete_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): - delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} - - -# DaprComponent = { -# "properties": { -# "componentType": None, #String -# "version": None, -# "ignoreErrors": None, -# "initTimeout": None, -# "secrets": None, -# "metadata": None, -# "scopes": None -# } -# } - -# DaprMetadata = { -# "key": None, #str -# "value": None, #str -# "secret_ref": None #str -# } + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) + except: + raise CLIError("Dapr component not found.") + + try: + r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) + logger.warning("Dapr componenet successfully deleted.") + return r + except Exception as e: + handle_raw_exception(e) + From 8c48ec1dbcbd38acbfa01b2598317c8e6a44d700 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 14 Mar 2022 15:32:36 -0400 Subject: [PATCH 19/22] Added helps and param text. --- src/containerapp/azext_containerapp/_help.py | 36 +++++++++++++++++++ .../azext_containerapp/_params.py | 2 ++ 2 files changed, 38 insertions(+) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index f13e6ca6cd4..fdf3734c0ab 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -438,3 +438,39 @@ text: | az containerapp dapr disable -n MyContainerapp -g MyResourceGroup """ + +helps['containerapp dapr list'] = """ + type: command + short-summary: List dapr components for a Containerapp environment. + examples: + - name: List dapr components for a Containerapp environment. + text: | + az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment +""" + +helps['containerapp dapr show'] = """ + type: command + short-summary: Show the details of a dapr component. + examples: + - name: Show the details of a dapr component. + text: | + az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" + +helps['containerapp dapr set'] = """ + type: command + short-summary: Create or update a dapr component. + examples: + - name: Create a dapr component. + text: | + az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName +""" + +helps['containerapp dapr delete'] = """ + type: command + short-summary: Delete a dapr componenet from a Containerapp. + examples: + - name: Delete a dapr componenet from a Containerapp. + text: | + az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index bcf34d6aa1d..74726962916 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -128,3 +128,5 @@ def load_arguments(self, _): c.argument('dapr_app_id', help="The dapr app id.") c.argument('dapr_app_port', help="The port of your app.") c.argument('dapr_app_protocol', help="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") + c.argument('dapr_component_name', help="The dapr component name.") + c.argument('environment_name', help="The dapr component environment name.") From 7d2cc0ecdb32addb3ea9386b573d73683e1e3114 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 14 Mar 2022 18:36:50 -0400 Subject: [PATCH 20/22] Changed dapr delete to dapr remove to match with dapr set. --- src/containerapp/azext_containerapp/_help.py | 6 +++--- src/containerapp/azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 0482d931643..3a91fb32aca 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -527,11 +527,11 @@ az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName """ -helps['containerapp dapr delete'] = """ +helps['containerapp dapr remove'] = """ type: command - short-summary: Delete a dapr componenet from a Containerapp. + short-summary: Remove a dapr componenet from a Containerapp environment. examples: - - name: Delete a dapr componenet from a Containerapp. + - name: Remove a dapr componenet from a Containerapp environment. text: | az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index c05f5ef6137..95e165d7e63 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -106,5 +106,5 @@ def load_command_table(self, _): g.custom_command('list', 'list_dapr_components') g.custom_command('show', 'show_dapr_component') g.custom_command('set', 'create_or_update_dapr_component') - g.custom_command('delete', 'delete_dapr_component') + g.custom_command('remove', 'remove_dapr_component') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f88e9d2c45b..13d42b6be6b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2053,7 +2053,7 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, except Exception as e: handle_raw_exception(e) -def delete_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): +def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): _validate_subscription_registered(cmd, "Microsoft.App") try: From d55484b074c2086bc1a9b857625a4e914fc46534 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 14 Mar 2022 18:48:08 -0400 Subject: [PATCH 21/22] Commented out managed identity for whl file. --- src/containerapp/azext_containerapp/commands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 95e165d7e63..7bcf193e363 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -57,10 +57,10 @@ def load_command_table(self, _): g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) - with self.command_group('containerapp identity') as g: - g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('show', 'show_managed_identity') + # with self.command_group('containerapp identity') as g: + # g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('show', 'show_managed_identity') with self.command_group('containerapp github-action') as g: From 29f15bb8eeb5136bcf770c75ac72e603b846303f Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 14 Mar 2022 18:49:08 -0400 Subject: [PATCH 22/22] Uncommented. --- src/containerapp/azext_containerapp/commands.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 7bcf193e363..95e165d7e63 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -57,10 +57,10 @@ def load_command_table(self, _): g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) - # with self.command_group('containerapp identity') as g: - # g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('show', 'show_managed_identity') + with self.command_group('containerapp identity') as g: + g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('show', 'show_managed_identity') with self.command_group('containerapp github-action') as g: