From 708f3106657aadb4e41c7afb092038db2ead2b2c Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 14:45:53 -0400 Subject: [PATCH 1/9] Added FileShare commands. --- .../azext_containerapp/_clients.py | 118 ++++++++++++++++++ .../azext_containerapp/_models.py | 7 ++ .../azext_containerapp/_params.py | 6 + .../azext_containerapp/commands.py | 6 + src/containerapp/azext_containerapp/custom.py | 48 ++++++- 5 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index d217e12be5e..1a5c4cf35b5 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -784,3 +784,121 @@ def list(cls, cmd, resource_group_name, environment_name, formatter=lambda x: x) app_list.append(formatted) return app_list + + +class StorageClient(): + @classmethod + def create_or_update(cls, cmd, resource_group_name, env_name, name, storage_envelope, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = STABLE_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/storages/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + env_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(storage_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/storages/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + env_name, + name, + api_version) + return poll(cmd, request_url, "waiting") + + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, env_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = STABLE_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/storages/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + env_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/managedEnvironments/{}/storages/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + env_name, + name, + api_version) + + if r.status_code == 202: + from azure.cli.core.azclierror import ResourceNotFoundError + try: + poll(cmd, request_url, "scheduledfordelete") + except ResourceNotFoundError: + pass + logger.warning('Containerapp environment successfully deleted') + return + + @classmethod + def show(cls, cmd, resource_group_name, env_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = STABLE_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/storages/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + env_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, env_name, formatter=lambda x: x): + env_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = STABLE_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/storages?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + env_name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for env in j["value"]: + formatted = formatter(env) + env_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 env in j["value"]: + formatted = formatter(env) + env_list.append(formatted) + + return env_list diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 6a474f89267..4ad081170be 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -231,3 +231,10 @@ "tenantId": None, # str "subscriptionId": None # str } + +AzureFileProperties = { + "accountName": None, + "accountKey": None, + "accessMode": None, + "shareName": None +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index fa33873602f..dad479a66f9 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long, too-many-statements, consider-using-f-string +from wsgiref.validate import validator from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, @@ -147,6 +148,11 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Container Apps Environment.') + with self.argument_context('containerapp env storage') as c: + c.argument('managed_env', options_list=['--environment'], help="Name or resource ID of the container app's environment.", validator = None) + c.argument('name', id_part=None) + c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"])) + with self.argument_context('containerapp identity') as c: c.argument('user_assigned', nargs='+', help="Space-separated user identities.") c.argument('system_assigned', help="Boolean indicating whether to assign system-assigned identity.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 158616ca0b2..d7013b98bad 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -73,6 +73,12 @@ def load_command_table(self, _): g.custom_command('set', 'create_or_update_dapr_component') g.custom_command('remove', 'remove_dapr_component') + with self.command_group('containerapp env storage') as g: + g.custom_show_command('show', 'show_storage') + g.custom_command('list', 'list_storage') + g.custom_command('set', 'create_or_update_storage', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('remove', 'remove_storage', 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()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 8160b7b7d10..d8044368b60 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long, consider-using-f-string, logging-format-interpolation, inconsistent-return-statements, broad-except, bare-except, too-many-statements, too-many-locals, too-many-boolean-expressions, too-many-branches, too-many-nested-blocks, pointless-statement, expression-not-assigned, unbalanced-tuple-unpacking +from os import access import threading import sys import time @@ -25,7 +26,7 @@ from msrest.exceptions import DeserializationError from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient, StorageClient from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, @@ -2201,3 +2202,48 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en return ContainerAppClient.create_or_update(cmd, resource_group_name, name, containerapp_def) except Exception as e: handle_raw_exception(e) + + +def show_storage(cmd, name, managed_env, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + return StorageClient.show(cmd, resource_group_name, managed_env, name) + except CLIError as e: + handle_raw_exception(e) + +def list_storage(cmd, managed_env, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + return StorageClient.list(cmd, resource_group_name, managed_env) + except CLIError as e: + handle_raw_exception(e) + +def create_or_update_storage(cmd, name, resource_group_name, managed_env, account_name, share_name, account_key, access_mode, type="AzureFile", no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + from ._models import AzureFileProperties as AzureFilePropertiesModel + if type.lower() != "azurefile": + raise ValidationError("Only AzureFile type is supported at this time.") + + storage_def = AzureFilePropertiesModel + storage_def["accountKey"] = account_key + storage_def["accountName"] = account_name + storage_def["shareName"] = share_name + storage_def["accessMode"] = access_mode + storage_envelope = {} + storage_envelope["properties"] = {} + storage_envelope["properties"]["azureFile"] = storage_def + + try: + return StorageClient.create_or_update(cmd, resource_group_name, managed_env, name, storage_envelope, no_wait) + except CLIError as e: + handle_raw_exception(e) + +def remove_storage(cmd, name, managed_env, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + return StorageClient.delete(cmd, resource_group_name, managed_env, name, no_wait) + except CLIError as e: + handle_raw_exception(e) From 2966fec3b992288d7b791116a1cbca712901e446 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 15:10:45 -0400 Subject: [PATCH 2/9] Updated params to match spec. Added param help. --- src/containerapp/azext_containerapp/_clients.py | 5 ++--- src/containerapp/azext_containerapp/_params.py | 9 ++++++--- src/containerapp/azext_containerapp/custom.py | 16 ++++++++-------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 1a5c4cf35b5..dfc41773ea3 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -845,14 +845,13 @@ def delete(cls, cmd, resource_group_name, env_name, name, no_wait=False): env_name, name, api_version) - - if r.status_code == 202: + if r.status_code == 200: # 200 successful delete, 204 means storage not found from azure.cli.core.azclierror import ResourceNotFoundError try: poll(cmd, request_url, "scheduledfordelete") except ResourceNotFoundError: pass - logger.warning('Containerapp environment successfully deleted') + logger.warning('Containerapp environment storage successfully deleted') return @classmethod diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index dad479a66f9..fcd17b85ac5 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -149,9 +149,12 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the Container Apps Environment.') with self.argument_context('containerapp env storage') as c: - c.argument('managed_env', options_list=['--environment'], help="Name or resource ID of the container app's environment.", validator = None) - c.argument('name', id_part=None) - c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"])) + c.argument('storage_name', help="Name of the storage.") + c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"]), help="Access mode for the storage.") + c.argument('account_key', help="Key of the storage account.") + c.argument('share_name', help="Name of the share on the storage.") + c.argument('account_name', help="Name of the storage account.") + c.ignore('type') with self.argument_context('containerapp identity') as c: c.argument('user_assigned', nargs='+', help="Space-separated user identities.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d8044368b60..6cfc2f6af5d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2204,23 +2204,23 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en handle_raw_exception(e) -def show_storage(cmd, name, managed_env, resource_group_name): +def show_storage(cmd, name, storage_name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") try: - return StorageClient.show(cmd, resource_group_name, managed_env, name) + return StorageClient.show(cmd, resource_group_name, name, storage_name) except CLIError as e: handle_raw_exception(e) -def list_storage(cmd, managed_env, resource_group_name): +def list_storage(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") try: - return StorageClient.list(cmd, resource_group_name, managed_env) + return StorageClient.list(cmd, resource_group_name, name) except CLIError as e: handle_raw_exception(e) -def create_or_update_storage(cmd, name, resource_group_name, managed_env, account_name, share_name, account_key, access_mode, type="AzureFile", no_wait=False): +def create_or_update_storage(cmd, storage_name, resource_group_name, name, account_name, share_name, account_key, access_mode, type="AzureFile", no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") from ._models import AzureFileProperties as AzureFilePropertiesModel if type.lower() != "azurefile": @@ -2236,14 +2236,14 @@ def create_or_update_storage(cmd, name, resource_group_name, managed_env, accoun storage_envelope["properties"]["azureFile"] = storage_def try: - return StorageClient.create_or_update(cmd, resource_group_name, managed_env, name, storage_envelope, no_wait) + return StorageClient.create_or_update(cmd, resource_group_name, name, storage_name, storage_envelope, no_wait) except CLIError as e: handle_raw_exception(e) -def remove_storage(cmd, name, managed_env, resource_group_name, no_wait=False): +def remove_storage(cmd, storage_name, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") try: - return StorageClient.delete(cmd, resource_group_name, managed_env, name, no_wait) + return StorageClient.delete(cmd, resource_group_name, name, storage_name, no_wait) except CLIError as e: handle_raw_exception(e) From 7d584e2b3d2d9862561ad37fabc747feae2b3716 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 15:18:24 -0400 Subject: [PATCH 3/9] Added help. Removed --ids support. --- src/containerapp/azext_containerapp/_help.py | 42 +++++++++++++++++++ .../azext_containerapp/_params.py | 1 + 2 files changed, 43 insertions(+) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index ab4cde705eb..9ca4c44c297 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -356,6 +356,48 @@ az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment """ +helps['containerapp env storage'] = """ + type: group + short-summary: Commands to manage storage for the Container Apps environment. +""" + +helps['containerapp env storage list'] = """ + type: command + short-summary: List the storages for an environment. + examples: + - name: List the storages for an environment. + text: | + az containerapp env storage list -g MyResourceGroup -n MyEnvironment +""" + +helps['containerapp env storage show'] = """ + type: command + short-summary: Show the details of a storage. + examples: + - name: Show the details of a storage. + text: | + az containerapp env storage show -g MyResourceGroup --storage-name MyStorageName -n MyEnvironment +""" + +helps['containerapp env storage set'] = """ + type: command + short-summary: Create or update a storage. + examples: + - name: Create a storage. + text: | + az containerapp env storage set -g MyResourceGroup -n MyEnv --storage-name MyStorageName --access-mode ReadOnly --account-key MyAccountKey --account-name MyAccountName --share-name MyShareName +""" + +helps['containerapp env storage remove'] = """ + type: command + short-summary: Remove a storage from an environment. + examples: + - name: Remove a storage from a Container Apps environment. + text: | + az containerapp env storage remove -g MyResourceGroup --storage-name MyStorageName -n MyEnvironment +""" + + # Identity Commands helps['containerapp identity'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index fcd17b85ac5..9874042e1c2 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -149,6 +149,7 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the Container Apps Environment.') with self.argument_context('containerapp env storage') as c: + c.argument('name', id_part=None) c.argument('storage_name', help="Name of the storage.") c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"]), help="Access mode for the storage.") c.argument('account_key', help="Key of the storage account.") From 44ecce98b7d2afffdcba05c09fa0c0df0de75523 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 15:30:28 -0400 Subject: [PATCH 4/9] Removed automatic import statements. --- src/containerapp/azext_containerapp/_params.py | 1 - src/containerapp/azext_containerapp/custom.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 9874042e1c2..dfe441c0d88 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -4,7 +4,6 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long, too-many-statements, consider-using-f-string -from wsgiref.validate import validator from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 6cfc2f6af5d..048486599c2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,7 +4,6 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long, consider-using-f-string, logging-format-interpolation, inconsistent-return-statements, broad-except, bare-except, too-many-statements, too-many-locals, too-many-boolean-expressions, too-many-branches, too-many-nested-blocks, pointless-statement, expression-not-assigned, unbalanced-tuple-unpacking -from os import access import threading import sys import time @@ -2226,6 +2225,7 @@ def create_or_update_storage(cmd, storage_name, resource_group_name, name, accou if type.lower() != "azurefile": raise ValidationError("Only AzureFile type is supported at this time.") + # We will replace this with sdk at some point storage_def = AzureFilePropertiesModel storage_def["accountKey"] = account_key storage_def["accountName"] = account_name From 8a4c22eb56f43a8099ee3506f7b364d82464516c Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 15:37:47 -0400 Subject: [PATCH 5/9] Added back type in help. --- src/containerapp/azext_containerapp/_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index dfe441c0d88..0a233e7788c 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -154,7 +154,7 @@ def load_arguments(self, _): c.argument('account_key', help="Key of the storage account.") c.argument('share_name', help="Name of the share on the storage.") c.argument('account_name', help="Name of the storage account.") - c.ignore('type') + c.argument('type', help="Type of the storage.") with self.argument_context('containerapp identity') as c: c.argument('user_assigned', nargs='+', help="Space-separated user identities.") From 02eec4d2740ee842a4068119f5cc7d3283181d0a Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 16:56:49 -0400 Subject: [PATCH 6/9] Added tests. --- .../azext_containerapp/_models.py | 2 +- src/containerapp/azext_containerapp/custom.py | 10 +++-- .../latest/test_containerapp_commands.py | 41 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 4ad081170be..02e5bcb916a 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -237,4 +237,4 @@ "accountKey": None, "accessMode": None, "shareName": None -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 048486599c2..dd056535de3 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -45,7 +45,8 @@ RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, SourceControl as SourceControlModel, - ManagedServiceIdentity as ManagedServiceIdentityModel) + ManagedServiceIdentity as ManagedServiceIdentityModel, + AzureFileProperties as AzureFilePropertiesModel) from ._utils import (_validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed, parse_secret_flags, store_as_secret_and_return_secret_ref, parse_env_var_flags, _generate_log_analytics_if_not_provided, _get_existing_secrets, _convert_object_from_snake_to_camel_case, @@ -2211,6 +2212,7 @@ def show_storage(cmd, name, storage_name, resource_group_name): except CLIError as e: handle_raw_exception(e) + def list_storage(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -2219,9 +2221,10 @@ def list_storage(cmd, name, resource_group_name): except CLIError as e: handle_raw_exception(e) -def create_or_update_storage(cmd, storage_name, resource_group_name, name, account_name, share_name, account_key, access_mode, type="AzureFile", no_wait=False): + +def create_or_update_storage(cmd, storage_name, resource_group_name, name, account_name, share_name, account_key, access_mode, type="AzureFile", no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") - from ._models import AzureFileProperties as AzureFilePropertiesModel + if type.lower() != "azurefile": raise ValidationError("Only AzureFile type is supported at this time.") @@ -2240,6 +2243,7 @@ def create_or_update_storage(cmd, storage_name, resource_group_name, name, accou except CLIError as e: handle_raw_exception(e) + def remove_storage(cmd, storage_name, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py index 3f57809fbfa..fadbb92a1d0 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py @@ -147,3 +147,44 @@ def test_containerapp_identity_user(self, resource_group): self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ JMESPathCheck('type', 'None'), ]) + + +@live_only() +class ContainerappEnvStorageTests(ScenarioTest): + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="eastus2") + def test_containerapp_env_storage(self, resource_group): + env_name = self.create_random_name(prefix='containerapp-env', length=24) + storage_name = self.create_random_name(prefix='storage', length=24) + shares_name = self.create_random_name(prefix='share', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + self.cmd('storage account create -g {} -n {} --kind StorageV2 --sku Standard_ZRS --enable-large-file-share'.format(resource_group, storage_name)) + self.cmd('storage share-rm create -g {} -n {} --storage-account {} --access-tier "TransactionOptimized" --quota 1024'.format(resource_group, shares_name, storage_name)) + + storage_keys = self.cmd('az storage account keys list -g {} -n {}'.format(resource_group, storage_name)).get_output_in_json()[0] + + self.cmd('containerapp env storage set -g {} -n {} --storage-name {} --account-name {} --account-key {} --access-mode ReadOnly --share-name {}'.format(resource_group, env_name, storage_name, storage_name, storage_keys["value"], shares_name), checks=[ + JMESPathCheck('name', storage_name), + ]) + + self.cmd('containerapp env storage show -g {} -n {} --storage-name {}'.format(resource_group, env_name, storage_name), checks=[ + JMESPathCheck('name', storage_name), + ]) + + self.cmd('containerapp env storage list -g {} -n {}'.format(resource_group, env_name), checks=[ + JMESPathCheck('[0].name', storage_name), + ]) + + self.cmd('containerapp env storage remove -g {} -n {} --storage-name {} --yes'.format(resource_group, env_name, storage_name)) + + self.cmd('containerapp env storage list -g {} -n {}'.format(resource_group, env_name), checks=[ + JMESPathCheck('length(@)', 0), + ]) From 7b3bc87aae7c0d291714cbcf059912be286ce911 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 17:31:36 -0400 Subject: [PATCH 7/9] Updated param names to better reflect AzureFile dependency. --- src/containerapp/azext_containerapp/_help.py | 2 +- src/containerapp/azext_containerapp/_params.py | 9 ++++----- src/containerapp/azext_containerapp/custom.py | 12 ++++-------- .../tests/latest/test_containerapp_commands.py | 2 +- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 9ca4c44c297..054250a09e4 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -385,7 +385,7 @@ examples: - name: Create a storage. text: | - az containerapp env storage set -g MyResourceGroup -n MyEnv --storage-name MyStorageName --access-mode ReadOnly --account-key MyAccountKey --account-name MyAccountName --share-name MyShareName + az containerapp env storage set -g MyResourceGroup -n MyEnv --storage-name MyStorageName --access-mode ReadOnly --azure-file-account-key MyAccountKey --azure-file-account-name MyAccountName --azure-file-share-name MyShareName """ helps['containerapp env storage remove'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 0a233e7788c..86aa24736db 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -150,11 +150,10 @@ def load_arguments(self, _): with self.argument_context('containerapp env storage') as c: c.argument('name', id_part=None) c.argument('storage_name', help="Name of the storage.") - c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"]), help="Access mode for the storage.") - c.argument('account_key', help="Key of the storage account.") - c.argument('share_name', help="Name of the share on the storage.") - c.argument('account_name', help="Name of the storage account.") - c.argument('type', help="Type of the storage.") + c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"]), help="Access mode for the AzureFile storage.") + c.argument('azure_file_account_key', help="Key of the AzureFile storage account.") + c.argument('azure_file_share_name', help="Name of the share on the AzureFile storage.") + c.argument('azure_file_account_name', help="Name of the AzureFile storage account.") with self.argument_context('containerapp identity') as c: c.argument('user_assigned', nargs='+', help="Space-separated user identities.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index dd056535de3..754be1e5230 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2222,17 +2222,13 @@ def list_storage(cmd, name, resource_group_name): handle_raw_exception(e) -def create_or_update_storage(cmd, storage_name, resource_group_name, name, account_name, share_name, account_key, access_mode, type="AzureFile", no_wait=False): # pylint: disable=redefined-builtin +def create_or_update_storage(cmd, storage_name, resource_group_name, name, azure_file_account_name, azure_file_share_name, azure_file_account_key, access_mode, no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") - if type.lower() != "azurefile": - raise ValidationError("Only AzureFile type is supported at this time.") - - # We will replace this with sdk at some point storage_def = AzureFilePropertiesModel - storage_def["accountKey"] = account_key - storage_def["accountName"] = account_name - storage_def["shareName"] = share_name + storage_def["accountKey"] = azure_file_account_key + storage_def["accountName"] = azure_file_account_name + storage_def["shareName"] = azure_file_share_name storage_def["accessMode"] = access_mode storage_envelope = {} storage_envelope["properties"] = {} diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py index fadbb92a1d0..97e52aabde8 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py @@ -171,7 +171,7 @@ def test_containerapp_env_storage(self, resource_group): storage_keys = self.cmd('az storage account keys list -g {} -n {}'.format(resource_group, storage_name)).get_output_in_json()[0] - self.cmd('containerapp env storage set -g {} -n {} --storage-name {} --account-name {} --account-key {} --access-mode ReadOnly --share-name {}'.format(resource_group, env_name, storage_name, storage_name, storage_keys["value"], shares_name), checks=[ + self.cmd('containerapp env storage set -g {} -n {} --storage-name {} --azure-file-account-name {} --azure-file-account-key {} --access-mode ReadOnly --azure-file-share-name {}'.format(resource_group, env_name, storage_name, storage_name, storage_keys["value"], shares_name), checks=[ JMESPathCheck('name', storage_name), ]) From d1582c71df9dd9d12f597ecf77887f185ebda2fc Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 3 May 2022 17:57:16 -0400 Subject: [PATCH 8/9] Added validation to ensure share name and account name are longer than 2 characters. Seems to be an API issue. --- src/containerapp/azext_containerapp/custom.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 754be1e5230..7d500263472 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2225,6 +2225,12 @@ def list_storage(cmd, name, resource_group_name): def create_or_update_storage(cmd, storage_name, resource_group_name, name, azure_file_account_name, azure_file_share_name, azure_file_account_key, access_mode, no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") + if len(azure_file_share_name) < 3: + raise ValidationError("File share name must be longer than 2 characters.") + + if len(azure_file_account_name) < 3: + raise ValidationError("Account name must be longer than 2 characters.") + storage_def = AzureFilePropertiesModel storage_def["accountKey"] = azure_file_account_key storage_def["accountName"] = azure_file_account_name From e992b72e570acac2e90ae905bc7907864e97c273 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 4 May 2022 12:56:38 -0400 Subject: [PATCH 9/9] Added warning message if user is updating existing storage. --- 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 7d500263472..fdd2897af1f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2231,6 +2231,16 @@ def create_or_update_storage(cmd, storage_name, resource_group_name, name, azure if len(azure_file_account_name) < 3: raise ValidationError("Account name must be longer than 2 characters.") + r = None + + try: + r = StorageClient.show(cmd, resource_group_name, name, storage_name) + except: + pass + + if r: + logger.warning("Only AzureFile account keys can be updated. In order to change the AzureFile share name or account name, please delete this storage and create a new one.") + storage_def = AzureFilePropertiesModel storage_def["accountKey"] = azure_file_account_key storage_def["accountName"] = azure_file_account_name