Skip to content
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
1 change: 1 addition & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ upcoming
* 'az containerapp create/update': support --max-inactive-revisions
* 'az containerapp env create': support --mi-system-assigned and --mi-user-assigned for environment create commands
* 'az containerapp env identity': support for container app environment assign/remove/show commands
* 'az containerapp env storage set': Support create or update managed environment storage with NFS Azure File.

0.3.46
++++++
Expand Down
2 changes: 2 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
CONNECTED_ENVIRONMENT_RESOURCE_TYPE = "connectedEnvironments"
CUSTOM_LOCATION_RESOURCE_TYPE = "customLocations"
CONNECTED_CLUSTER_TYPE = "connectedClusters"
AZURE_FILE_STORAGE_TYPE = "azureFile"
NFS_AZURE_FILE_STORAGE_TYPE = "nfsAzureFile"

MAXIMUM_SECRET_LENGTH = 20
MAXIMUM_CONTAINER_APP_NAME_LENGTH = 32
Expand Down
12 changes: 12 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,18 @@
az containerapp env workload-profile set -g MyResourceGroup -n MyEnvironment --workload-profile-name my-wlp --workload-profile-type D4 --min-nodes 1 --max-nodes 2
"""

helps['containerapp env storage set'] = """
type: command
short-summary: Create or update a storage.
examples:
- name: Create a azure file 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
- name: Create a nfs azure file storage.
text: |
az containerapp env storage set -g MyResourceGroup -n MyEnv --storage-name MyStorageName --storage-type NfsAzureFile --access-mode ReadOnly --server MyNfsServer.file.core.windows.net --file-share /MyNfsServer/MyShareName
"""

# Compose commands
helps['containerapp compose'] = """
type: group
Expand Down
9 changes: 9 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ def load_arguments(self, _):
c.argument('logs_dynamic_json_columns', options_list=['--logs-dynamic-json-columns', '-j'], arg_type=get_three_state_flag(),
help='Boolean indicating whether to parse json string log into dynamic json columns. Only work for destination log-analytics.', is_preview=True)

with self.argument_context('containerapp env storage') as c:
c.argument('storage_type', arg_type = get_enum_type(['AzureFile', 'NfsAzureFile']), help="Type of the storage. Assumed to be AzureFile if not specified.", is_preview=True)
c.argument('access_mode', id_part=None, arg_type=get_enum_type(["ReadWrite", "ReadOnly"]),
help="Access mode for the AzureFile or nfs AzureFile storage.")
c.argument('azure_file_share_name', options_list=["--azure-file-share-name", "--file-share", "-f"],
help="Name of the share on the AzureFile or nfs AzureFile storage.")
c.argument('server', options_list=["--server", "-s"],
help="Server of the NfsAzureFile storage account.", is_preview=True)

# App Resiliency
with self.argument_context('containerapp resiliency') as c:
c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None)
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 @@ -55,6 +55,12 @@ def load_command_table(self, _):
g.custom_command('remove', 'remove_env_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory())
g.custom_show_command('show', 'show_env_managed_identity')

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', confirmation=True, exception_handler=ex_handler_factory())

with self.command_group('containerapp service', deprecate_info=self.deprecate(redirect='containerapp add-on', expiration='2.59.0', hide=True), is_preview=True) as g:
g.custom_command('list', 'list_all_services')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.commands import AzCliCommand
from azure.cli.command_modules.containerapp.base_resource import BaseResource
from azure.cli.core.azclierror import RequiredArgumentMissingError, ValidationError
from knack.log import get_logger
from typing import Any, Dict

from ._client_factory import handle_raw_exception
from ._models import AzureFileProperties, NfsAzureFileProperties, ManagedEnvironmentStorageProperties
from ._constants import AZURE_FILE_STORAGE_TYPE, NFS_AZURE_FILE_STORAGE_TYPE
logger = get_logger(__name__)


class ContainerappEnvStorageDecorator(BaseResource):

def __init__(self, cmd: AzCliCommand, client: Any, raw_parameters: Dict, models: str):
super().__init__(cmd, client, raw_parameters, models)
self.managed_environment_storage_def = ManagedEnvironmentStorageProperties
self.storage_type = self.get_argument_storage_type()
self.azure_file_account_name = self.get_argument_azure_file_account_name()
self.azure_file_share_name = self.get_argument_azure_file_share_name()
self.azure_file_account_key = self.get_argument_azure_file_account_key()
self.server = self.get_argument_server()
self.access_mode = self.get_argument_access_mode()

def get_argument_storage_name(self):
return self.get_param("storage_name")

def get_argument_storage_type(self):
return self.get_param("storage_type")

def get_argument_azure_file_account_name(self):
return self.get_param("azure_file_account_name")

def get_argument_azure_file_share_name(self):
return self.get_param("azure_file_share_name")

def get_argument_azure_file_account_key(self):
return self.get_param("azure_file_account_key")

def get_argument_server(self):
return self.get_param("server")

def get_argument_access_mode(self):
return self.get_param("access_mode")

def construct_payload(self):
if not self.storage_type or self.storage_type.lower() == AZURE_FILE_STORAGE_TYPE.lower():
storage_def = AzureFileProperties
storage_def["accountKey"] = self.azure_file_account_key
storage_def["accountName"] = self.azure_file_account_name
storage_def["shareName"] = self.azure_file_share_name
storage_def["accessMode"] = self.access_mode

self.managed_environment_storage_def["properties"] = {}
self.managed_environment_storage_def["properties"]["azureFile"] = storage_def
elif self.storage_type.lower() == NFS_AZURE_FILE_STORAGE_TYPE.lower():
storage_def = NfsAzureFileProperties
storage_def["server"] = self.server
storage_def["shareName"] = self.azure_file_share_name
storage_def["accessMode"] = self.access_mode
self.managed_environment_storage_def["properties"] = {}
self.managed_environment_storage_def["properties"]["nfsAzureFile"] = storage_def

def validate_arguments(self):
import json
if not self.storage_type or self.storage_type.lower() == AZURE_FILE_STORAGE_TYPE:
if len(self.azure_file_share_name) == 0 or len(self.azure_file_account_name) == 0 or len(
self.azure_file_account_key) == 0 or len(self.access_mode) == 0:
raise RequiredArgumentMissingError(
"--azure-file-share-name/--file-share/-f, --azure-file-account-key/--storage-account-key/-k, and --access-mode must be provided for AzureFile storage type")
if len(self.azure_file_share_name) < 3:
raise ValidationError("File share name with --azure-file-share-name/--file-share/-f must be longer than 2 characters.")
if len(self.azure_file_account_name) < 3:
raise ValidationError("Account name with --azure-file-account-name/--account-name/-a must be longer than 2 characters.")
elif self.storage_type.lower() == NFS_AZURE_FILE_STORAGE_TYPE:
if len(self.server) == 0 or len(self.access_mode) == 0 or len(self.azure_file_share_name) == 0:
raise RequiredArgumentMissingError(
"--server, --file-share/-f and --access-mode must be provided for NfsAzureFile storage type")
if len(self.server) < 3:
raise ValidationError("Server with --server must be longer than 2 characters.")

try:
r = self.client.show(cmd=self.cmd, resource_group_name=self.get_argument_resource_group_name(),
env_name=self.get_argument_name(), name=self.get_argument_storage_name())
if r:
logger.warning(
'Only AzureFile account keys can be updated. In order to change the AzureFile share name or account'
' name or NfsAzureFile server, please delete this storage and create a new one.')
except Exception as e:
string_err = str(e)
if "ManagedEnvironmentStorageNotFound" in string_err:
pass
else:
handle_raw_exception(e)

def create_or_update(self):
try:
return self.client.create_or_update(cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
env_name=self.get_argument_name(),
name=self.get_argument_storage_name(),
storage_envelope=self.managed_environment_storage_def)
except Exception as e:
handle_raw_exception(e)

def show(self):
try:
return self.client.show(cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
env_name=self.get_argument_name(),
name=self.get_argument_storage_name())
except Exception as e:
handle_raw_exception(e)

def list(self):
try:
return self.client.list(cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
env_name=self.get_argument_name())
except Exception as e:
handle_raw_exception(e)

def delete(self):
try:
return self.client.delete(cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
env_name=self.get_argument_name(),
name=self.get_argument_storage_name())
except Exception as e:
handle_raw_exception(e)
68 changes: 66 additions & 2 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@
)
from .containerapp_auth_decorator import ContainerAppPreviewAuthDecorator
from .containerapp_decorator import ContainerAppPreviewCreateDecorator, ContainerAppPreviewListDecorator, ContainerAppPreviewUpdateDecorator
from .containerapp_env_storage_decorator import ContainerappEnvStorageDecorator
from ._client_factory import handle_raw_exception
from ._clients import (
GitHubActionPreviewClient,
ContainerAppPreviewClient,
AuthPreviewClient,
SubscriptionPreviewClient,
StoragePreviewClient,
ContainerAppsJobPreviewClient,
ContainerAppsResiliencyPreviewClient,
DaprComponentResiliencyPreviewClient,
Expand All @@ -86,7 +88,8 @@
AzureCredentials as AzureCredentialsModel,
SourceControl as SourceControlModel,
ContainerAppCertificateEnvelope as ContainerAppCertificateEnvelopeModel,
AzureFileProperties as AzureFilePropertiesModel)
AzureFileProperties as AzureFilePropertiesModel
)

from ._utils import connected_env_check_cert_name_availability, get_oryx_run_image_tags, patchable_check, get_pack_exec_path, is_docker_running, parse_build_env_vars

Expand All @@ -96,7 +99,8 @@
DEV_KAFKA_IMAGE, DEV_KAFKA_SERVICE_TYPE, DEV_MARIADB_CONTAINER_NAME, DEV_MARIADB_IMAGE, DEV_MARIADB_SERVICE_TYPE, DEV_QDRANT_IMAGE,
DEV_QDRANT_CONTAINER_NAME, DEV_QDRANT_SERVICE_TYPE, DEV_WEAVIATE_IMAGE, DEV_WEAVIATE_CONTAINER_NAME, DEV_WEAVIATE_SERVICE_TYPE,
DEV_MILVUS_IMAGE, DEV_MILVUS_CONTAINER_NAME, DEV_MILVUS_SERVICE_TYPE, DEV_SERVICE_LIST, CONTAINER_APPS_SDK_MODELS, BLOB_STORAGE_TOKEN_STORE_SECRET_SETTING_NAME,
DAPR_SUPPORTED_STATESTORE_DEV_SERVICE_LIST, DAPR_SUPPORTED_PUBSUB_DEV_SERVICE_LIST)
DAPR_SUPPORTED_STATESTORE_DEV_SERVICE_LIST, DAPR_SUPPORTED_PUBSUB_DEV_SERVICE_LIST, AZURE_FILE_STORAGE_TYPE, NFS_AZURE_FILE_STORAGE_TYPE)


logger = get_logger(__name__)

Expand Down Expand Up @@ -771,6 +775,66 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False):
return containerapp_env_decorator.delete()


def show_storage(cmd, name, storage_name, resource_group_name):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

raw_parameters = locals()
containerapp_env_storage_decorator = ContainerappEnvStorageDecorator(
cmd=cmd,
client=StoragePreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)

return containerapp_env_storage_decorator.show()


def list_storage(cmd, name, resource_group_name):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

raw_parameters = locals()
containerapp_env_storage_decorator = ContainerappEnvStorageDecorator(
cmd=cmd,
client=StoragePreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)

return containerapp_env_storage_decorator.list()


def create_or_update_storage(cmd, storage_name, resource_group_name, name, storage_type=None,
azure_file_account_name=None, azure_file_share_name=None, azure_file_account_key=None,
server=None, access_mode=None, no_wait=False): # pylint: disable=redefined-builtin
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

raw_parameters = locals()
containerapp_env_storage_decorator = ContainerappEnvStorageDecorator(
cmd=cmd,
client=StoragePreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)
containerapp_env_storage_decorator.register_provider(CONTAINER_APPS_RP)
containerapp_env_storage_decorator.validate_arguments()
containerapp_env_storage_decorator.construct_payload()
return containerapp_env_storage_decorator.create_or_update()


def remove_storage(cmd, storage_name, name, resource_group_name):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

raw_parameters = locals()
containerapp_env_storage_decorator = ContainerappEnvStorageDecorator(
cmd=cmd,
client=StoragePreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)

return containerapp_env_storage_decorator.delete()


def create_containerappsjob(cmd,
name,
resource_group_name,
Expand Down
Loading