From 7ebf80a960bfc326075e4264eeb55b01e9a518cd Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 7 Mar 2022 16:03:33 -0800 Subject: [PATCH 1/7] Remove --location since location must be same as managed env --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/custom.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ac3b640b40e..c6d27b2d97f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -26,7 +26,7 @@ def load_arguments(self, _): with self.argument_context('containerapp') as c: c.argument('tags', arg_type=tags_type) - c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment', '-e'], help="Name or resource ID of the containerapp's environment.") + c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment'], help="Name or resource ID of the containerapp's environment.") c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bac77b3ab61..1ba0da1c6ef 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -312,15 +312,11 @@ def create_containerapp(cmd, dapr_app_protocol=None, # dapr_components=None, revision_suffix=None, - location=None, startup_command=None, args=None, tags=None, no_wait=False): - location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) - _validate_subscription_registered(cmd, "Microsoft.App") - _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: if image or managed_env or min_replicas or max_replicas or target_port or ingress or\ @@ -350,13 +346,8 @@ def create_containerapp(cmd, if not managed_env_info: raise ValidationError("The environment '{}' does not exist. Specify a valid environment".format(managed_env)) - if not location: - location = managed_env_info["location"] - elif location.lower() != managed_env_info["location"].lower(): - raise ValidationError("The location \"{}\" of the containerapp must be the same as the Managed Environment location \"{}\"".format( - location, - managed_env_info["location"] - )) + location = managed_env_info["location"] + _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") external_ingress = None if ingress is not None: From 8e499ecfc4b711c3c5994cfd4aa18cda1774801c Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:28:52 -0800 Subject: [PATCH 2/7] Add options for flag names: --env-vars and --registry-srever --- src/containerapp/azext_containerapp/_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c6d27b2d97f..d5c9428d0c6 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\". Empty string to clear existing values") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -56,7 +56,7 @@ def load_arguments(self, _): # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, e.g. myregistry.azurecr.io") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server', '--registry-login-server'], help="The url of the registry, e.g. myregistry.azurecr.io") c.argument('registry_pass', type=str, validator=validate_registry_pass, options_list=['--registry-password'], help="The password to log in container image registry server. If stored as a secret, value must start with \'secretref:\' followed by the secret name.") c.argument('registry_user', type=str, validator=validate_registry_user, options_list=['--registry-username'], help="The username to log in container image registry server") c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") From 54d35915ebdcc716a0533d503e663906b99eb029 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:29:50 -0800 Subject: [PATCH 3/7] Empty string to clear env_vars --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 4 ++-- src/containerapp/azext_containerapp/custom.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index d5c9428d0c6..6bec838fb93 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\". Empty string to clear existing values") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 83b707640f5..16cac247433 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -73,8 +73,8 @@ def parse_env_var_flags(env_list, is_update_containerapp=False): key_val = pair.split('=', 1) if len(key_val) != 2: if is_update_containerapp: - raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") - raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\".") + raise ValidationError("Environment variables must be in the format \"=\" \"=secretref:\" ...\".") + raise ValidationError("Environment variables must be in the format \"=\" \"=secretref:\" ...\".") if key_val[0] in env_pairs: raise ValidationError("Duplicate environment variable {env} found, environment variable names must be unique.".format(env = key_val[0])) value = key_val[1].split('secretref:') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1ba0da1c6ef..45c557eb026 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -496,7 +496,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass 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['container'] = image or image_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -523,9 +523,12 @@ def update_containerapp(cmd, if image is not None: c["image"] = image if env_vars is not None: - if "env" not in c or not c["env"]: + if isinstance(env_vars, list) and not env_vars: c["env"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + else: + 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 From b672fe717b6b2dc386916627bc09e46969ed2772 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:51:49 -0800 Subject: [PATCH 4/7] Default revisions_mode to single --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 45c557eb026..bdc83fdf2a6 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -298,7 +298,7 @@ def create_containerapp(cmd, target_port=None, transport="auto", ingress=None, - revisions_mode=None, + revisions_mode="single", secrets=None, env_vars=None, cpu=None, From 6e92da2f46523a95c2c6600574a87c8e4f0cc0ce Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 14:17:14 -0800 Subject: [PATCH 5/7] Infer acr credentials if it is acr and credentials are not provided --- src/containerapp/azext_containerapp/_utils.py | 17 ++++++++++++++++- .../azext_containerapp/_validators.py | 11 ++++++----- src/containerapp/azext_containerapp/custom.py | 13 +++++++++++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 16cac247433..02b436597c8 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,8 +5,8 @@ from distutils.filelist import findall from operator import is_ +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError, RequiredArgumentMissingError) - from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from msrestazure.tools import parse_resource_id @@ -444,3 +444,18 @@ def _get_app_from_revision(revision): revision.pop() revision = "--".join(revision) return revision + + +def _infer_acr_credentials(cmd, registry_server): + # If registry is Azure Container Registry, we can try inferring credentials + if '.azurecr.io' not in registry_server: + raise RequiredArgumentMissingError('Registry url is required if using Azure Container Registry, otherwise Registry username and password are required.') + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up credentials...') + parsed = urlparse(registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + + try: + registry_user, registry_pass = _get_acr_cred(cmd.cli_ctx, registry_name) + return (registry_user, registry_pass) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry {}. Please provide the registry username and password'.format(registry_name)) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 23ed260e360..c95d675cb00 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -52,19 +52,20 @@ def validate_registry_server(namespace): if "create" in namespace.command.lower(): if namespace.registry_server: if not namespace.registry_user or not namespace.registry_pass: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if ".azurecr.io" not in namespace.registry_server: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") def validate_registry_user(namespace): if "create" in namespace.command.lower(): if namespace.registry_user: - if not namespace.registry_server or not namespace.registry_pass: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if not namespace.registry_server or (not namespace.registry_pass and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") def validate_registry_pass(namespace): if "create" in namespace.command.lower(): if namespace.registry_pass: - if not namespace.registry_user or not namespace.registry_server: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if not namespace.registry_server or (not namespace.registry_user and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") def validate_target_port(namespace): if "create" in namespace.command.lower(): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bdc83fdf2a6..5da806288fb 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger @@ -37,7 +37,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, raise_missing_token_suggestion) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials) logger = get_logger(__name__) @@ -370,6 +370,11 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel + + # Infer credentials if not supplied and its azurecr + if not registry_user or not registry_pass: + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + registries_def["server"] = registry_server registries_def["username"] = registry_user @@ -648,6 +653,10 @@ def update_containerapp(cmd, if not registry_server: raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + # Infer credentials if not supplied and its azurecr + if not registry_user or not registry_pass: + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + # Check if updating existing registry updating_existing_registry = False for r in registries_def: From 967f4e4481f7c31758b7e748408a17da0232c313 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 14:49:36 -0800 Subject: [PATCH 6/7] fix help msg --- src/containerapp/azext_containerapp/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 33f196f133e..f4d7713ce93 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -90,7 +90,7 @@ --registry-password mypassword - name: Update a Containerapp using a specified startup command and arguments text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ + az containerapp update -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage \\ --command "/bin/sh" --args "-c", "while true; do echo hello; sleep 10;done" From bf8bc02d184b599f2f23e0bec6c3b04eb9c74906 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 17:25:47 -0800 Subject: [PATCH 7/7] if image is hosted on acr, and no registry server is supplied, infer the registry server --- src/containerapp/azext_containerapp/custom.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5da806288fb..d346cc75f65 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -367,6 +367,11 @@ def create_containerapp(cmd, if secrets is not None: secrets_def = parse_secret_flags(secrets) + # If ACR image and registry_server is not supplied, infer it + if image and '.azurecr.io' in image: + if not registry_server: + registry_server = image.split('/')[0] + registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel @@ -496,6 +501,11 @@ def update_containerapp(cmd, if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) + # If ACR image and registry_server is not supplied, infer it + if image and '.azurecr.io' in image: + if not registry_server: + registry_server = image.split('/')[0] + update_map = {} update_map['secrets'] = secrets is not None update_map['ingress'] = ingress or target_port or transport or traffic_weights