Skip to content

Commit

Permalink
Added FileShare commands. (Azure#84)
Browse files Browse the repository at this point in the history
* Added FileShare commands.

* Updated params to match spec. Added param help.

* Added help. Removed --ids support.

* Removed automatic import statements.

* Added back type in help.

* Added tests.

* Updated param names to better reflect AzureFile dependency.

* Added validation to ensure share name and account name are longer than 2 characters. Seems to be an API issue.

* Added warning message if user is updating existing storage.

Co-authored-by: Haroon Feisal <haroonfeisal@microsoft.com>
  • Loading branch information
haroonf and Haroon Feisal committed May 4, 2022
1 parent 53ddc45 commit 711bbd4
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 2 deletions.
117 changes: 117 additions & 0 deletions src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,3 +784,120 @@ 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 == 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 storage 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
42 changes: 42 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 --azure-file-account-key MyAccountKey --azure-file-account-name MyAccountName --azure-file-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
Expand Down
7 changes: 7 additions & 0 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,10 @@
"tenantId": None, # str
"subscriptionId": None # str
}

AzureFileProperties = {
"accountName": None,
"accountKey": None,
"accessMode": None,
"shareName": None
}
8 changes: 8 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ 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('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 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.")
c.argument('system_assigned', help="Boolean indicating whether to assign system-assigned identity.")
Expand Down
6 changes: 6 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
66 changes: 64 additions & 2 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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,
Expand All @@ -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,
Expand Down Expand Up @@ -2201,3 +2202,64 @@ 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, storage_name, resource_group_name):
_validate_subscription_registered(cmd, "Microsoft.App")

try:
return StorageClient.show(cmd, resource_group_name, name, storage_name)
except CLIError as e:
handle_raw_exception(e)


def list_storage(cmd, name, resource_group_name):
_validate_subscription_registered(cmd, "Microsoft.App")

try:
return StorageClient.list(cmd, resource_group_name, name)
except CLIError as e:
handle_raw_exception(e)


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.")

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
storage_def["shareName"] = azure_file_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, name, storage_name, storage_envelope, no_wait)
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")

try:
return StorageClient.delete(cmd, resource_group_name, name, storage_name, no_wait)
except CLIError as e:
handle_raw_exception(e)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {} --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),
])

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),
])

0 comments on commit 711bbd4

Please sign in to comment.