Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added FileShare commands. #84

Merged
merged 9 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
runefa marked this conversation as resolved.
Show resolved Hide resolved
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),
])