diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index f58df889075..8400a3f0baf 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.1.2 +++++++ +* Various fixes for bugs found +* Dapr subgroup +* Managed Identity + 0.1.1 ++++++ * Various fixes for az containerapp create, update diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5a1e597523e..108ee5b004f 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -128,7 +128,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= return r.json() @classmethod - def delete(cls, cmd, resource_group_name, name, no_wait=False): + def delete(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) @@ -142,24 +142,8 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): 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') + if r.status_code == 202: + logger.warning('Containerapp successfully deleted') return @classmethod diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 3a91fb32aca..228343f5dee 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -479,7 +479,7 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage Containerapp dapr. + short-summary: Commands to manage dapr. """ helps['containerapp dapr enable'] = """ diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 14d8e1a8fb3..b356adaa2a8 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -136,8 +136,7 @@ "targetPort": None, "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight - "customDomains": None, # [CustomDomain] - "allowInsecure": None # Boolean + "customDomains": None # [CustomDomain] } RegistryCredentials = { diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ab1ebe848bd..c592ed5363d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -32,7 +32,7 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") - c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the container.") + c.argument('container_name', type=str, options_list=['--container-name'], help="Name of the container.") 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'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") @@ -86,7 +86,7 @@ def load_arguments(self, _): c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log Analytics workspace key to configure your Log Analytics workspace. You can use \"az monitor log-analytics workspace get-shared-keys\" to retrieve the key.') with self.argument_context('containerapp env', arg_group='Dapr') as c: - c.argument('instrumentation_key', options_list=['--instrumentation-key'], help='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') + c.argument('instrumentation_key', options_list=['--dapr-instrumentation-key'], help='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') with self.argument_context('containerapp env', arg_group='Virtual Network') as c: c.argument('infrastructure_subnet_resource_id', type=str, options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 14816a07915..297ce4904ba 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -459,7 +459,7 @@ def _is_valid_weight(weight): return False -def _update_traffic_Weights(containerapp_def, list_weights): +def _update_traffic_weights(containerapp_def, list_weights): if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"] or list_weights and len(list_weights): containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = [] @@ -493,7 +493,7 @@ def _get_app_from_revision(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.') + raise RequiredArgumentMissingError('Registry username and password are required if not using Azure Container Registry.') 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] @@ -503,3 +503,13 @@ def _infer_acr_credentials(cmd, registry_server): 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)) + + +def _registry_exists(containerapp_def, registry_server): + exists = False + if "properties" in containerapp_def and "configuration" in containerapp_def["properties"] and "registries" in containerapp_def["properties"]["configuration"]: + for registry in containerapp_def["properties"]["configuration"]["registries"]: + if "server" in registry and registry["server"] and registry["server"].lower() == registry_server.lower(): + exists = True + break + return exists diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 95e165d7e63..40e422bb532 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -27,10 +27,13 @@ def transform_containerapp_list_output(apps): def transform_revision_output(rev): props = ['name', 'replicas', 'active', 'createdTime'] - result = {k: rev[k] for k in rev if k in props} + result = {k: rev['properties'][k] for k in rev['properties'] if k in props} - if 'latestRevisionFqdn' in rev['template']: - result['fqdn'] = rev['template']['latestRevisionFqdn'] + if 'name' in rev: + result['name'] = rev['name'] + + if 'fqdn' in rev['properties']['template']: + result['fqdn'] = rev['properties']['template']['fqdn'] return result @@ -44,9 +47,8 @@ def load_command_table(self, _): g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) g.custom_command('list', 'list_containerapp', table_transformer=transform_containerapp_list_output) g.custom_command('create', 'create_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 13d42b6be6b..1fcb7c2176b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -43,9 +43,9 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, parse_env_var_flags, _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, + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_weights, _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret, - _ensure_identity_resource_id, _remove_dapr_readonly_attributes) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists) logger = get_logger(__name__) @@ -307,7 +307,7 @@ def create_containerapp(cmd, resource_group_name, yaml=None, image=None, - image_name=None, + container_name=None, managed_env=None, min_replicas=None, max_replicas=None, @@ -384,17 +384,12 @@ 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 # Infer credentials if not supplied and its azurecr - if not registry_user or not registry_pass: + if registry_user is None or registry_pass is None: registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) registries_def["server"] = registry_server @@ -445,7 +440,7 @@ def create_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name if image_name else name + container_def["name"] = container_name if container_name else name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) @@ -497,13 +492,9 @@ def update_containerapp(cmd, resource_group_name, yaml=None, image=None, - image_name=None, + container_name=None, min_replicas=None, max_replicas=None, - ingress=None, - target_port=None, - transport=None, - traffic_weights=None, revisions_mode=None, secrets=None, env_vars=None, @@ -512,11 +503,6 @@ def update_containerapp(cmd, registry_server=None, registry_user=None, registry_pass=None, - dapr_enabled=None, - dapr_app_port=None, - dapr_app_id=None, - dapr_app_protocol=None, - # dapr_components=None, revision_suffix=None, startup_command=None, args=None, @@ -525,9 +511,9 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - if image or min_replicas or max_replicas or target_port or ingress or\ + if image or min_replicas or max_replicas or\ revisions_mode or secrets or env_vars or cpu or memory or registry_server or\ - registry_user or registry_pass or dapr_enabled or dapr_app_port or dapr_app_id or\ + registry_user or registry_pass 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) @@ -541,19 +527,20 @@ 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] + # Doing this while API has bug. If env var is an empty string, API doesn't return "value" even though the "value" should be an empty string + if "properties" in containerapp_def and "template" in containerapp_def["properties"] and "containers" in containerapp_def["properties"]["template"]: + for container in containerapp_def["properties"]["template"]["containers"]: + if "env" in container: + for e in container["env"]: + if "value" not in e: + e["value"] = "" update_map = {} update_map['secrets'] = secrets is not None - 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 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 + update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None + update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -563,16 +550,16 @@ def update_containerapp(cmd, # Containers if update_map["container"]: - if not image_name: + if not container_name: if len(containerapp_def["properties"]["template"]["containers"]) == 1: - image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + container_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(): + if c["name"].lower() == container_name.lower(): updating_existing_container = True if image is not None: @@ -618,7 +605,7 @@ def update_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name + container_def["name"] = container_name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) @@ -646,46 +633,10 @@ def update_containerapp(cmd, if max_replicas is not None: containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - # Dapr - if update_map["dapr"]: - if "dapr" not in containerapp_def["properties"]["template"]: - containerapp_def["properties"]["template"]["dapr"] = {} - if dapr_enabled is not None: - containerapp_def["properties"]["template"]["dapr"]["daprEnabled"] = dapr_enabled - if dapr_app_id is not None: - containerapp_def["properties"]["template"]["dapr"]["appId"] = dapr_app_id - if dapr_app_port is not None: - containerapp_def["properties"]["template"]["dapr"]["appPort"] = dapr_app_port - if dapr_app_protocol is not None: - containerapp_def["properties"]["template"]["dapr"]["appProtocol"] = dapr_app_protocol - # Configuration if revisions_mode is not None: containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode - if update_map["ingress"]: - if "ingress" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["ingress"] = {} - - external_ingress = None - if ingress is not None: - if ingress.lower() == "internal": - external_ingress = False - elif ingress.lower() == "external": - external_ingress = True - - if external_ingress is not None: - containerapp_def["properties"]["configuration"]["ingress"]["external"] = external_ingress - - if target_port is not None: - containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - - if transport is not None: - containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport - - if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) if secrets is not None: @@ -704,7 +655,7 @@ def update_containerapp(cmd, raise ValidationError("Usage error: --registry-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: + if (registry_user is None or registry_pass is None) and not _registry_exists(containerapp_def, registry_server): registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) # Check if updating existing registry @@ -751,39 +702,6 @@ def update_containerapp(cmd, handle_raw_exception(e) -def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_replicas=None, no_wait=False): - 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 "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 - - _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 scale 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 show_containerapp(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -808,11 +726,11 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) -def delete_containerapp(cmd, name, resource_group_name, no_wait=False): +def delete_containerapp(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") try: - return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) except CLIError as e: handle_raw_exception(e) @@ -1221,7 +1139,7 @@ def create_or_update_github_action(cmd, azure_credentials["subscriptionId"] = get_subscription_id(cmd.cli_ctx) # Registry - if not registry_username or not registry_password: + if registry_username is None or registry_password is None: # If registry is Azure Container Registry, we can try inferring credentials if not registry_url or '.azurecr.io' not in registry_url: raise RequiredArgumentMissingError('Registry url is required if using Azure Container Registry, otherwise Registry username and password are required if using Dockerhub') @@ -1366,24 +1284,23 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): 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): + name, + resource_group_name, + from_revision=None, + #label=None, + yaml=None, + image=None, + container_name=None, + min_replicas=None, + max_replicas=None, + env_vars=None, + cpu=None, + memory=None, + revision_suffix=None, + startup_command=None, + args=None, + tags=None, + no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") if not from_revision: @@ -1413,11 +1330,17 @@ def copy_revision(cmd, containerapp_def["properties"]["template"] = r["properties"]["template"] + # Doing this while API has bug. If env var is an empty string, API doesn't return "value" even though the "value" should be an empty string + if "properties" in containerapp_def and "template" in containerapp_def["properties"] and "containers" in containerapp_def["properties"]["template"]: + for container in containerapp_def["properties"]["template"]["containers"]: + if "env" in container: + for e in container["env"]: + if "value" not in e: + e["value"] = "" + 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'] + update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -1427,16 +1350,16 @@ def copy_revision(cmd, # Containers if update_map["container"]: - if not image_name: + if not container_name: if len(containerapp_def["properties"]["template"]["containers"]) == 1: - image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + container_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(): + if c["name"].lower() == container_name.lower(): updating_existing_container = True if image is not None: @@ -1479,7 +1402,7 @@ def copy_revision(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name + container_def["name"] = container_name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) @@ -1507,14 +1430,6 @@ def copy_revision(cmd, 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: @@ -1648,7 +1563,7 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait 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) + _update_traffic_weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index fa1b93b7448..96524e9ab67 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.1' +VERSION = '0.1.2' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index 95f5c9fd470..81c893cbd02 100644 --- a/src/index.json +++ b/src/index.json @@ -11180,6 +11180,46 @@ "version": "0.1.1" }, "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" + }, + { + "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.2-py2.py3-none-any.whl", + "filename": "containerapp-0.1.2-py2.py3-none-any.whl", + "metadata": { + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "extensions": { + "python.details": { + "contacts": [ + { + "email": "azpycli@microsoft.com", + "name": "Microsoft Corporation", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azure-cli-extensions" + } + } + }, + "extras": [], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "containerapp", + "run_requires": [ + { + "requires": [ + "azure-cli-core" + ] + } + ], + "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", + "version": "0.1.2" + }, + "sha256Digest": "b1d4cc823f761cfb5469f8d53a9fa04bdc1493c3c5d5f3a90333876287e7b2f8" } ], "cosmosdb-preview": [