diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 04e86c8ce86..e466001d5ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -230,4 +230,6 @@ /src/confidentialledger/ @kairu-ms @lynshi -/src/quota/ @kairu-ms @ZengTaoxu \ No newline at end of file +/src/quota/ @kairu-ms @ZengTaoxu + +/src/containerapp/ @calvinsID @haroonf @panchagnula diff --git a/linter_exclusions.yml b/linter_exclusions.yml index dc6a952ebf4..6054be4859c 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -300,6 +300,28 @@ codespace plan create: default_sku_name: rule_exclusions: - option_length_too_long +containerapp env create: + parameters: + infrastructure_subnet_resource_id: + rule_exclusions: + - option_length_too_long + instrumentation_key: + rule_exclusions: + - option_length_too_long + platform_reserved_dns_ip: + rule_exclusions: + - option_length_too_long +containerapp github-action add: + parameters: + service_principal_client_id: + rule_exclusions: + - option_length_too_long + service_principal_client_secret: + rule_exclusions: + - option_length_too_long + service_principal_tenant_id: + rule_exclusions: + - option_length_too_long costmanagement export create: parameters: definition_dataset_configuration: diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst new file mode 100644 index 00000000000..b66465a832a --- /dev/null +++ b/src/containerapp/HISTORY.rst @@ -0,0 +1,10 @@ +.. :changelog: + +Release History +=============== + +0.1.0 +++++++ +* Initial release for Container App support with Microsoft.App RP. +* Subgroup commands for dapr, github-action, ingress, registry, revision & secrets +* Various bugfixes for create & update commands diff --git a/src/containerapp/README.rst b/src/containerapp/README.rst new file mode 100644 index 00000000000..629d90415c3 --- /dev/null +++ b/src/containerapp/README.rst @@ -0,0 +1,5 @@ +Microsoft Azure CLI 'containerapp' Extension +========================================== + +This package is for the 'containerapp' extension. +i.e. 'az containerapp' \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/__init__.py b/src/containerapp/azext_containerapp/__init__.py new file mode 100644 index 00000000000..dcff6d86def --- /dev/null +++ b/src/containerapp/azext_containerapp/__init__.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=super-with-arguments + +from azure.cli.core import AzCommandsLoader + +from azext_containerapp._help import helps # pylint: disable=unused-import + + +class ContainerappCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + containerapp_custom = CliCommandType( + operations_tmpl='azext_containerapp.custom#{}', + client_factory=None) + super(ContainerappCommandsLoader, self).__init__(cli_ctx=cli_ctx, + custom_command_type=containerapp_custom) + + def load_command_table(self, args): + from azext_containerapp.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azext_containerapp._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = ContainerappCommandsLoader diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py new file mode 100644 index 00000000000..4e8ad424138 --- /dev/null +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -0,0 +1,75 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long, consider-using-f-string + +from azure.cli.core.commands.client_factory import get_mgmt_service_client +from azure.cli.core.profiles import ResourceType +from azure.cli.core.azclierror import CLIInternalError + + +# pylint: disable=inconsistent-return-statements +def ex_handler_factory(no_throw=False): + def _polish_bad_errors(ex): + import json + try: + content = json.loads(ex.response.content) + if 'message' in content: + detail = content['message'] + elif 'Message' in content: + detail = content['Message'] + + ex = CLIInternalError(detail) + except Exception: # pylint: disable=broad-except + pass + if no_throw: + return ex + raise ex + return _polish_bad_errors + + +def handle_raw_exception(e): + import json + + stringErr = str(e) + + if "{" in stringErr and "}" in stringErr: + jsonError = stringErr[stringErr.index("{"):stringErr.rindex("}") + 1] + jsonError = json.loads(jsonError) + + if 'error' in jsonError: + jsonError = jsonError['error'] + + if 'code' in jsonError and 'message' in jsonError: + code = jsonError['code'] + message = jsonError['message'] + raise CLIInternalError('({}) {}'.format(code, message)) + elif "Message" in jsonError: + message = jsonError["Message"] + raise CLIInternalError(message) + elif "message" in jsonError: + message = jsonError["message"] + raise CLIInternalError(message) + raise e + + +def providers_client_factory(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, subscription_id=subscription_id).providers + + +def cf_resource_groups(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).resource_groups + + +def log_analytics_client_factory(cli_ctx): + from azure.mgmt.loganalytics import LogAnalyticsManagementClient + + return get_mgmt_service_client(cli_ctx, LogAnalyticsManagementClient).workspaces + + +def log_analytics_shared_key_client_factory(cli_ctx): + from azure.mgmt.loganalytics import LogAnalyticsManagementClient + + return get_mgmt_service_client(cli_ctx, LogAnalyticsManagementClient).shared_keys diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py new file mode 100644 index 00000000000..77cf596c8bf --- /dev/null +++ b/src/containerapp/azext_containerapp/_clients.py @@ -0,0 +1,723 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long, super-with-arguments, too-many-instance-attributes, consider-using-f-string, no-else-return, no-self-use + +import json +import time +import sys + +from azure.cli.core.util import send_raw_request +from azure.cli.core.commands.client_factory import get_subscription_id +from knack.log import get_logger + +logger = get_logger(__name__) + +API_VERSION = "2021-03-01" +NEW_API_VERSION = "2022-01-01-preview" +POLLING_TIMEOUT = 60 # how many seconds before exiting +POLLING_SECONDS = 2 # how many seconds between requests + + +class PollingAnimation(): + def __init__(self): + self.tickers = ["/", "|", "\\", "-", "/", "|", "\\", "-"] + self.currTicker = 0 + + def tick(self): + sys.stdout.write('\r') + sys.stdout.write(self.tickers[self.currTicker] + " Running ..") + sys.stdout.flush() + self.currTicker += 1 + self.currTicker = self.currTicker % len(self.tickers) + + def flush(self): + sys.stdout.flush() + sys.stdout.write('\r') + sys.stdout.write("\033[K") + + +def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-return-statements + try: + start = time.time() + end = time.time() + POLLING_TIMEOUT + animation = PollingAnimation() + + animation.tick() + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + + while r.status_code in [200, 201] and start < end: + time.sleep(POLLING_SECONDS) + animation.tick() + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + r2 = r.json() + + if "properties" not in r2 or "provisioningState" not in r2["properties"] or not r2["properties"]["provisioningState"].lower() == poll_if_status: + break + start = time.time() + + animation.flush() + return r.json() + except Exception as e: # pylint: disable=broad-except + animation.flush() + + delete_statuses = ["scheduledfordelete", "cancelled"] + + if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete + raise e + + +class ContainerAppClient(): + @classmethod + def create_or_update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + 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) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(container_app_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + 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) + return poll(cmd, request_url, "inprogress") + + return r.json() + + @classmethod + def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + 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) + + r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(container_app_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + 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) + return poll(cmd, request_url, "inprogress") + + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + 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) + + 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') + + @classmethod + def show(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) + 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) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + @classmethod + def list_by_subscription(cls, cmd, formatter=lambda x: x): + app_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/providers/Microsoft.App/containerApps?api-version={}".format( + management_hostname.strip('/'), + sub_id, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + app_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 app in j["value"]: + formatted = formatter(app) + app_list.append(formatted) + + return app_list + + @classmethod + def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): + app_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + app_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 app in j["value"]: + formatted = formatter(app) + app_list.append(formatted) + + return app_list + + @classmethod + def list_secrets(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) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/listSecrets?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "POST", request_url, body=None) + return r.json() + + @classmethod + def list_revisions(cls, cmd, resource_group_name, name, formatter=lambda x: x): + + revisions_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + revisions_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 app in j["value"]: + formatted = formatter(app) + revisions_list.append(formatted) + + return revisions_list + + @classmethod + def show_revision(cls, cmd, resource_group_name, container_app_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + container_app_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + @classmethod + def restart_revision(cls, cmd, resource_group_name, container_app_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}/restart?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + container_app_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "POST", request_url) + return r.json() + + @classmethod + def activate_revision(cls, cmd, resource_group_name, container_app_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}/activate?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + container_app_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "POST", request_url) + return r.json() + + @classmethod + def deactivate_revision(cls, cmd, resource_group_name, container_app_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}/deactivate?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + container_app_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "POST", request_url) + return r.json() + + +class ManagedEnvironmentClient(): + @classmethod + def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(managed_environment_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + return poll(cmd, request_url, "waiting") + + return r.json() + + @classmethod + def update(cls, cmd, resource_group_name, name, managed_environment_envelope, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(managed_environment_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + return poll(cmd, request_url, "waiting") + + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_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/{}?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, "scheduledfordelete") + except ResourceNotFoundError: + pass + logger.warning('Containerapp environment successfully deleted') + return + + @classmethod + def show(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) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + @classmethod + def list_by_subscription(cls, cmd, formatter=lambda x: x): + env_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/providers/Microsoft.App/managedEnvironments?api-version={}".format( + management_hostname.strip('/'), + sub_id, + 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 + + @classmethod + def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): + env_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_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 + + +class GitHubActionClient(): + @classmethod + def create_or_update(cls, cmd, resource_group_name, name, github_action_envelope, headers, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(github_action_envelope), headers=headers) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + return poll(cmd, request_url, "inprogress") + + return r.json() + + @classmethod + def show(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) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/sourcecontrols/current?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "DELETE", request_url, headers=headers) + + 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/{}/sourcecontrols/current?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 github action successfully deleted') + return + + +class DaprComponentClient(): + @classmethod + def create_or_update(cls, cmd, resource_group_name, environment_name, name, dapr_component_envelope, no_wait=False): + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(dapr_component_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + name, + api_version) + return poll(cmd, request_url, "inprogress") + + return r.json() + + @classmethod + def delete(cls, cmd, resource_group_name, environment_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_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/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_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('Dapr component successfully deleted') + return + + @classmethod + def show(cls, cmd, resource_group_name, environment_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_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, environment_name, formatter=lambda x: x): + app_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = NEW_API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/managedEnvironments/{}/daprComponents?api-version={}".format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + environment_name, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for app in j["value"]: + formatted = formatter(app) + app_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 app in j["value"]: + formatted = formatter(app) + app_list.append(formatted) + + return app_list diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py new file mode 100644 index 00000000000..659d43afc39 --- /dev/null +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=consider-using-f-string + +from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) +from knack.log import get_logger + +logger = get_logger(__name__) + + +''' +Get Github personal access token following Github oauth for command line tools +https://docs.github.com/en/developers/apps/authorizing-oauth-apps#device-flow +''' + + +GITHUB_OAUTH_CLIENT_ID = "8d8e1f6000648c575489" +GITHUB_OAUTH_SCOPES = [ + "admin:repo_hook", + "repo", + "workflow" +] + + +def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument + if scope_list: + for scope in scope_list: + if scope not in GITHUB_OAUTH_SCOPES: + raise ValidationError("Requested github oauth scope is invalid") + scope_list = ' '.join(scope_list) + + authorize_url = 'https://github.com/login/device/code' + authorize_url_data = { + 'scope': scope_list, + 'client_id': GITHUB_OAUTH_CLIENT_ID + } + + import requests + import time + from urllib.parse import parse_qs + + try: + response = requests.post(authorize_url, data=authorize_url_data) + parsed_response = parse_qs(response.content.decode('ascii')) + + device_code = parsed_response['device_code'][0] + user_code = parsed_response['user_code'][0] + verification_uri = parsed_response['verification_uri'][0] + interval = int(parsed_response['interval'][0]) + expires_in_seconds = int(parsed_response['expires_in'][0]) + logger.warning('Please navigate to %s and enter the user code %s to activate and ' + 'retrieve your github personal access token', verification_uri, user_code) + + timeout = time.time() + expires_in_seconds + logger.warning("Waiting up to '%s' minutes for activation", str(expires_in_seconds // 60)) + + confirmation_url = 'https://github.com/login/oauth/access_token' + confirmation_url_data = { + 'client_id': GITHUB_OAUTH_CLIENT_ID, + 'device_code': device_code, + 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' + } + + pending = True + while pending: + time.sleep(interval) + + if time.time() > timeout: + raise UnclassifiedUserFault('Activation did not happen in time. Please try again') + + confirmation_response = requests.post(confirmation_url, data=confirmation_url_data) + parsed_confirmation_response = parse_qs(confirmation_response.content.decode('ascii')) + + if 'error' in parsed_confirmation_response and parsed_confirmation_response['error'][0]: + if parsed_confirmation_response['error'][0] == 'slow_down': + interval += 5 # if slow_down error is received, 5 seconds is added to minimum polling interval + elif parsed_confirmation_response['error'][0] != 'authorization_pending': + pending = False + + if 'access_token' in parsed_confirmation_response and parsed_confirmation_response['access_token'][0]: + return parsed_confirmation_response['access_token'][0] + except Exception as e: + raise CLIInternalError( + 'Error: {}. Please try again, or retrieve personal access token from the Github website'.format(e)) from e + + raise UnclassifiedUserFault('Activation did not happen in time. Please try again') diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py new file mode 100644 index 00000000000..5e75c334d82 --- /dev/null +++ b/src/containerapp/azext_containerapp/_help.py @@ -0,0 +1,487 @@ +# coding=utf-8 +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.help_files import helps # pylint: disable=unused-import + + +helps['containerapp'] = """ + type: group + short-summary: Manage Azure Container Apps. +""" + +helps['containerapp create'] = """ + type: command + short-summary: Create a container app. + examples: + - name: Create a container app and retrieve its fully qualified domain name. + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --ingress external --target-port 80 \\ + --registry-server myregistry.azurecr.io --registry-username myregistry --registry-password $REGISTRY_PASSWORD \\ + --query properties.configuration.ingress.fqdn + - name: Create a container app with resource requirements and replica count limits. + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image nginx --environment MyContainerappEnv \\ + --cpu 0.5 --memory 1.0Gi \\ + --min-replicas 4 --max-replicas 8 + - name: Create a container app with secrets and environment variables. + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image my-app:v1.0 --environment MyContainerappEnv \\ + --secrets mysecret=secretvalue1 anothersecret="secret value 2" \\ + --env-vars GREETING="Hello, world" SECRETENV=secretref:anothersecret + - name: Create a container app using a YAML configuration. Example YAML configuration - https://aka.ms/azure-container-apps-yaml + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --environment MyContainerappEnv \\ + --yaml "path/to/yaml/file.yml" +""" + +helps['containerapp update'] = """ + type: command + short-summary: Update a container app. In multiple revisions mode, create a new revision based on the latest revision. + examples: + - name: Update a container app's container image. + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --image myregistry.azurecr.io/my-app:v2.0 + - name: Update a container app's resource requirements and scale limits. + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --cpu 0.5 --memory 1.0Gi \\ + --min-replicas 4 --max-replicas 8 +""" + +helps['containerapp delete'] = """ + type: command + short-summary: Delete a container app. + examples: + - name: Delete a container app. + text: az containerapp delete -g MyResourceGroup -n MyContainerapp +""" + +helps['containerapp show'] = """ + type: command + short-summary: Show details of a container app. + examples: + - name: Show the details of a container app. + text: | + az containerapp show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp list'] = """ + type: command + short-summary: List container apps. + examples: + - name: List container apps in the current subscription. + text: | + az containerapp list + - name: List container apps by resource group. + text: | + az containerapp list -g MyResourceGroup +""" + +# Revision Commands +helps['containerapp revision'] = """ + type: group + short-summary: Commands to manage revisions. +""" + +helps['containerapp revision show'] = """ + type: command + short-summary: Show details of a revision. + examples: + - name: Show details of a revision. + text: | + az containerapp revision show -n MyContainerapp -g MyResourceGroup \\ + --revision MyContainerappRevision +""" + +helps['containerapp revision list'] = """ + type: command + short-summary: List a container app's revisions. + examples: + - name: List a container app's revisions. + text: | + az containerapp revision list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision restart'] = """ + type: command + short-summary: Restart a revision. + examples: + - name: Restart a revision. + text: | + az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision MyContainerappRevision +""" + +helps['containerapp revision activate'] = """ + type: command + short-summary: Activate a revision. + examples: + - name: Activate a revision. + text: | + az containerapp revision activate -g MyResourceGroup --revision MyContainerappRevision +""" + +helps['containerapp revision deactivate'] = """ + type: command + short-summary: Deactivate a revision. + examples: + - name: Deactivate a revision. + text: | + az containerapp revision deactivate -g MyResourceGroup --revision MyContainerappRevision +""" + +helps['containerapp revision set-mode'] = """ + type: command + short-summary: Set the revision mode of a container app. + examples: + - name: Set a container app to single revision mode. + text: | + az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on the latest revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup \\ + --cpu 0.75 --memory 1.5Gi + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -g MyResourceGroup \\ + --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi + +""" + + +# Environment Commands +helps['containerapp env'] = """ + type: group + short-summary: Commands to manage Container Apps environments. +""" + +helps['containerapp env create'] = """ + type: command + short-summary: Create a Container Apps environment. + examples: + - name: Create an environment with an auto-generated Log Analytics workspace. + text: | + az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ + --location "Canada Central" + - name: Create an environment with an existing Log Analytics workspace. + text: | + az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ + --logs-workspace-id myLogsWorkspaceID \\ + --logs-workspace-key myLogsWorkspaceKey \\ + --location "Canada Central" +""" + + +helps['containerapp env delete'] = """ + type: command + short-summary: Delete a Container Apps environment. + examples: + - name: Delete an environment. + text: az containerapp env delete -n MyContainerappEnvironment -g MyResourceGroup +""" + +helps['containerapp env show'] = """ + type: command + short-summary: Show details of a Container Apps environment. + examples: + - name: Show the details of an environment. + text: | + az containerapp env show -n MyContainerappEnvironment -g MyResourceGroup +""" + +helps['containerapp env list'] = """ + type: command + short-summary: List Container Apps environments by subscription or resource group. + examples: + - name: List environments in the current subscription. + text: | + az containerapp env list + - name: List environments by resource group. + text: | + az containerapp env list -g MyResourceGroup +""" + +helps['containerapp env dapr-component'] = """ + type: group + short-summary: Commands to manage Dapr components for the Container Apps environment. +""" + +helps['containerapp env dapr-component list'] = """ + type: command + short-summary: List Dapr components for an environment. + examples: + - name: List Dapr components for an environment. + text: | + az containerapp env dapr-component list -g MyResourceGroup --name MyEnvironment +""" + +helps['containerapp env dapr-component show'] = """ + type: command + short-summary: Show the details of a Dapr component. + examples: + - name: Show the details of a Dapr component. + text: | + az containerapp env dapr-component show -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment +""" + +helps['containerapp env dapr-component set'] = """ + type: command + short-summary: Create or update a Dapr component. + examples: + - name: Create a Dapr component. + text: | + az containerapp env dapr-component set -g MyResourceGroup --name MyEnv --yaml MyYAMLPath --dapr-component-name MyDaprComponentName +""" + +helps['containerapp env dapr-component remove'] = """ + type: command + short-summary: Remove a Dapr component from an environment. + examples: + - name: Remove a Dapr component from a Container Apps environment. + text: | + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment +""" + +# Ingress Commands +helps['containerapp ingress'] = """ + type: group + short-summary: Commands to manage ingress and traffic-splitting. +""" + +helps['containerapp ingress traffic'] = """ + type: subgroup + short-summary: Commands to manage traffic-splitting. +""" + +helps['containerapp ingress show'] = """ + type: command + short-summary: Show details of a container app's ingress. + examples: + - name: Show the details of a container app's ingress. + text: | + az containerapp ingress show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress enable'] = """ + type: command + short-summary: Enable ingress for a container app. + examples: + - name: Enable ingress for a container app. + text: | + az containerapp ingress enable -n MyContainerapp -g MyResourceGroup \\ + --type external --allow-insecure --target-port 80 --transport auto +""" + +helps['containerapp ingress disable'] = """ + type: command + short-summary: Disable ingress for a container app. + examples: + - name: Disable ingress for a container app. + text: | + az containerapp ingress disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress traffic'] = """ + type: group + short-summary: Commands to manage traffic-splitting. +""" + +helps['containerapp ingress traffic set'] = """ + type: command + short-summary: Configure traffic-splitting for a container app. + examples: + - name: Route 100%% of a container app's traffic to its latest revision. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=100 + - name: Split a container app's traffic between two revisions. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=80 MyRevisionName=20 +""" + +helps['containerapp ingress traffic show'] = """ + type: command + short-summary: Show traffic-splitting configuration for a container app. + examples: + - name: Show a container app's ingress traffic configuration. + text: | + az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup +""" + +# Registry Commands +helps['containerapp registry'] = """ + type: group + short-summary: Commands to manage container registry information. +""" + +helps['containerapp registry show'] = """ + type: command + short-summary: Show details of a container registry. + examples: + - name: Show the details of a container registry. + text: | + az containerapp registry show -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +helps['containerapp registry list'] = """ + type: command + short-summary: List container registries configured in a container app. + examples: + - name: List container registries configured in a container app. + text: | + az containerapp registry list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp registry set'] = """ + type: command + short-summary: Add or update a container registry's details. + examples: + - name: Configure a container app to use a registry. + text: | + az containerapp registry set -n MyContainerapp -g MyResourceGroup \\ + --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword +""" + +helps['containerapp registry remove'] = """ + type: command + short-summary: Remove a container registry's details. + examples: + - name: Remove a registry from a Containerapp. + text: | + az containerapp registry remove -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +# Secret Commands +helps['containerapp secret'] = """ + type: group + short-summary: Commands to manage secrets. +""" + +helps['containerapp secret show'] = """ + type: command + short-summary: Show details of a secret. + examples: + - name: Show the details of a secret. + text: | + az containerapp secret show -n MyContainerapp -g MyResourceGroup --secret-name MySecret +""" + +helps['containerapp secret list'] = """ + type: command + short-summary: List the secrets of a container app. + examples: + - name: List the secrets of a container app. + text: | + az containerapp secret list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp secret remove'] = """ + type: command + short-summary: Remove secrets from a container app. + examples: + - name: Remove secrets from a container app. + text: | + az containerapp secret remove -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 +""" + +helps['containerapp secret set'] = """ + type: command + short-summary: Create/update secrets. + examples: + - name: Add secrets to a container app. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName1=MySecretValue1 MySecretName2=MySecretValue2 + - name: Update a secret. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue +""" + +helps['containerapp github-action'] = """ + type: group + short-summary: Commands to manage GitHub Actions. +""" + +helps['containerapp github-action add'] = """ + type: command + short-summary: Add a Github Actions workflow to a repository to deploy a container app. + examples: + - name: Add GitHub Actions, using Azure Container Registry and personal access token. + text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main + --registry-url myregistryurl.azurecr.io + --service-principal-client-id 00000000-0000-0000-0000-00000000 + --service-principal-tenant-id 00000000-0000-0000-0000-00000000 + --service-principal-client-secret ClientSecret + --token MyAccessToken + - name: Add GitHub Actions, using Azure Container Registry and log in to GitHub flow to retrieve personal access token. + text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main + --registry-url myregistryurl.azurecr.io + --service-principal-client-id 00000000-0000-0000-0000-00000000 + --service-principal-tenant-id 00000000-0000-0000-0000-00000000 + --service-principal-client-secret ClientSecret + --login-with-github + - name: Add GitHub Actions, using Docker Hub and log in to GitHub flow to retrieve personal access token. + text: az containerapp github-action add -g MyResourceGroup -n MyContainerapp --repo-url https://github.com/userid/repo --branch main + --registry-username MyUsername + --registry-password MyPassword + --service-principal-client-id 00000000-0000-0000-0000-00000000 + --service-principal-tenant-id 00000000-0000-0000-0000-00000000 + --service-principal-client-secret ClientSecret + --login-with-github +""" + +helps['containerapp github-action delete'] = """ + type: command + short-summary: Remove a previously configured Container Apps GitHub Actions workflow from a repository. + examples: + - name: Remove GitHub Actions using a personal access token. + text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp + --token MyAccessToken + - name: Remove GitHub Actions using log in to GitHub flow to retrieve personal access token. + text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp + --login-with-github +""" + +helps['containerapp github-action show'] = """ + type: command + short-summary: Show the GitHub Actions configuration on a container app. + examples: + - name: Show the GitHub Actions configuration on a Containerapp. + text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp +""" + +# Dapr Commands +helps['containerapp dapr'] = """ + type: group + short-summary: Commands to manage Dapr. To manage Dapr components, see `az containerapp env dapr-component`. +""" + +helps['containerapp dapr enable'] = """ + type: command + short-summary: Enable Dapr for a container app. + examples: + - name: Enable Dapr for a container app. + text: | + az containerapp dapr enable -n MyContainerapp -g MyResourceGroup --dapr-app-id my-app-id --dapr-app-port 8080 +""" + +helps['containerapp dapr disable'] = """ + type: command + short-summary: Disable Dapr for a container app. + examples: + - name: Disable Dapr for a container app. + text: | + az containerapp dapr disable -n MyContainerapp -g MyResourceGroup +""" diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py new file mode 100644 index 00000000000..d4b26d94b32 --- /dev/null +++ b/src/containerapp/azext_containerapp/_models.py @@ -0,0 +1,232 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=line-too-long, too-many-statements, super-with-arguments + +VnetConfiguration = { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIP": None +} + +ManagedEnvironment = { + "location": None, + "tags": None, + "properties": { + "daprAIInstrumentationKey": None, + "vnetConfiguration": None, # VnetConfiguration + "internalLoadBalancerEnabled": None, + "appLogsConfiguration": None + } +} + +AppLogsConfiguration = { + "destination": None, + "logAnalyticsConfiguration": None +} + +LogAnalyticsConfiguration = { + "customerId": None, + "sharedKey": None +} + +# Containerapp + +Dapr = { + "enabled": False, + "appId": None, + "appProtocol": None, + "appPort": None +} + +EnvironmentVar = { + "name": None, + "value": None, + "secretRef": None +} + +ContainerResources = { + "cpu": None, + "memory": None +} + +VolumeMount = { + "volumeName": None, + "mountPath": None +} + +Container = { + "image": None, + "name": None, + "command": None, + "args": None, + "env": None, # [EnvironmentVar] + "resources": None, # ContainerResources + "volumeMounts": None, # [VolumeMount] +} + +Volume = { + "name": None, + "storageType": "EmptyDir", # AzureFile or EmptyDir + "storageName": None # None for EmptyDir, otherwise name of storage resource +} + +ScaleRuleAuth = { + "secretRef": None, + "triggerParameter": None +} + +QueueScaleRule = { + "queueName": None, + "queueLength": None, + "auth": None # ScaleRuleAuth +} + +CustomScaleRule = { + "type": None, + "metadata": {}, + "auth": None # ScaleRuleAuth +} + +HttpScaleRule = { + "metadata": {}, + "auth": None # ScaleRuleAuth +} + +ScaleRule = { + "name": None, + "azureQueue": None, # QueueScaleRule + "customScaleRule": None, # CustomScaleRule + "httpScaleRule": None, # HttpScaleRule +} + +Secret = { + "name": None, + "value": None +} + +Scale = { + "minReplicas": None, + "maxReplicas": None, + "rules": [] # list of ScaleRule +} + +TrafficWeight = { + "revisionName": None, + "weight": None, + "latestRevision": False +} + +BindingType = { + +} + +CustomDomain = { + "name": None, + "bindingType": None, # BindingType + "certificateId": None +} + +Ingress = { + "fqdn": None, + "external": False, + "targetPort": None, + "transport": None, # 'auto', 'http', 'http2' + "traffic": None, # TrafficWeight + "customDomains": None # [CustomDomain] +} + +RegistryCredentials = { + "server": None, + "username": None, + "passwordSecretRef": None +} + +Template = { + "revisionSuffix": None, + "containers": None, # [Container] + "scale": Scale, + "volumes": None # [Volume] +} + +Configuration = { + "secrets": None, # [Secret] + "activeRevisionsMode": None, # 'multiple' or 'single' + "ingress": None, # Ingress + "dapr": Dapr, + "registries": None # [RegistryCredentials] +} + +UserAssignedIdentity = { + +} + +ManagedServiceIdentity = { + "type": None, # 'None', 'SystemAssigned', 'UserAssigned', 'SystemAssigned,UserAssigned' + "userAssignedIdentities": None # {string: UserAssignedIdentity} +} + +ContainerApp = { + "location": None, + "identity": None, # ManagedServiceIdentity + "properties": { + "managedEnvironmentId": None, + "configuration": None, # Configuration + "template": None # Template + }, + "tags": None +} + +DaprComponent = { + "properties": { + "componentType": None, # String + "version": None, + "ignoreErrors": None, + "initTimeout": None, + "secrets": None, + "metadata": None, + "scopes": None + } +} + +DaprMetadata = { + "key": None, # str + "value": None, # str + "secret_ref": None # str +} + +SourceControl = { + "properties": { + "repoUrl": None, + "branch": None, + "githubActionConfiguration": None # [GitHubActionConfiguration] + } + +} + +GitHubActionConfiguration = { + "registryInfo": None, # [RegistryInfo] + "azureCredentials": None, # [AzureCredentials] + "dockerfilePath": None, # str + "publishType": None, # str + "os": None, # str + "runtimeStack": None, # str + "runtimeVersion": None # str +} + +RegistryInfo = { + "registryUrl": None, # str + "registryUserName": None, # str + "registryPassword": None # str +} + +AzureCredentials = { + "clientId": None, # str + "clientSecret": None, # str + "tenantId": None, # str + "subscriptionId": None # str +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py new file mode 100644 index 00000000000..6e0ee6918d4 --- /dev/null +++ b/src/containerapp/azext_containerapp/_params.py @@ -0,0 +1,178 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long, too-many-statements, consider-using-f-string, option-length-too-long + +from knack.arguments import CLIArgumentType + +from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, + file_type, + get_three_state_flag, get_enum_type, tags_type) +# from azure.cli.core.commands.validators import get_default_location_from_resource_group + +from ._validators import (validate_memory, validate_cpu, validate_managed_env_name_or_id, validate_registry_server, + validate_registry_user, validate_registry_pass, validate_target_port, validate_ingress) + + +def load_arguments(self, _): + + name_type = CLIArgumentType(options_list=['--name', '-n']) + + with self.argument_context('containerapp') as c: + # Base arguments + c.argument('name', name_type, metavar='NAME', id_part='name', help="The name of the Containerapp.") + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('location', arg_type=get_location_type(self.cli_ctx)) + + with self.argument_context('containerapp') as c: + c.argument('tags', arg_type=tags_type) + c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment'], help="Name or resource ID of the container app's environment.") + c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a container app. All other parameters will be ignored. For an example, see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples') + + # Container + with self.argument_context('containerapp', arg_group='Container') as c: + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + c.argument('container_name', type=str, help="Name of the container.") + c.argument('cpu', type=float, validator=validate_cpu, help="Required CPU in cores, e.g. 0.5") + c.argument('memory', type=str, validator=validate_memory, help="Required memory, e.g. 1.0Gi") + c.argument('env_vars', nargs='*', help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values. Prefix value with 'secretref:' to reference a secret.") + c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container that will executed during startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") + c.argument('args', nargs='*', help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\". Empty string to clear existing values") + c.argument('revision_suffix', type=str, help='User friendly suffix that is appended to the revision name') + + # Env vars + with self.argument_context('containerapp', arg_group='Environment variables') as c: + c.argument('set_env_vars', nargs='*', help="A list of environment variable(s) to add to the container. Space-separated values in 'key=value' format. If stored as a secret, value must start with \'secretref:\' followed by the secret name.") + c.argument('remove_env_vars', nargs='*', help="A list of environment variable(s) to remove from container. Space-separated env var name values.") + c.argument('replace_env_vars', nargs='*', help="A list of environment variable(s) to replace from the container. Space-separated values in 'key=value' format. If stored as a secret, value must start with \'secretref:\' followed by the secret name.") + c.argument('remove_all_env_vars', help="Option to remove all environment variable(s) from the container.") + + # Scale + with self.argument_context('containerapp', arg_group='Scale') as c: + c.argument('min_replicas', type=int, help="The minimum number of replicas.") + c.argument('max_replicas', type=int, help="The maximum number of replicas.") + + # Dapr + with self.argument_context('containerapp', arg_group='Dapr') as c: + c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag(), help="Boolean indicating if the Dapr side car is enabled.") + c.argument('dapr_app_port', type=int, help="The port Dapr uses to talk to the application.") + c.argument('dapr_app_id', type=str, help="The Dapr application identifier.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), help="The protocol Dapr uses to talk to the application.") + + # Configuration + with self.argument_context('containerapp', arg_group='Configuration') as c: + c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), help="The active revisions mode for the container app.") + c.argument('registry_server', type=str, validator=validate_registry_server, help="The container registry server hostname, e.g. myregistry.azurecr.io.") + c.argument('registry_pass', type=str, validator=validate_registry_pass, options_list=['--registry-password'], help="The password to log in to container registry. If stored as a secret, value must start with \'secretref:\' followed by the secret name.") + c.argument('registry_user', type=str, validator=validate_registry_user, options_list=['--registry-username'], help="The username to log in to container registry.") + c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the container app. Space-separated values in 'key=value' format.") + + # Ingress + with self.argument_context('containerapp', arg_group='Ingress') as c: + c.argument('ingress', validator=validate_ingress, default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") + c.argument('target_port', type=int, validator=validate_target_port, help="The application port used for ingress traffic.") + c.argument('transport', arg_type=get_enum_type(['auto', 'http', 'http2']), help="The transport protocol used for ingress traffic.") + + with self.argument_context('containerapp create') as c: + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + + with self.argument_context('containerapp scale') as c: + c.argument('min_replicas', type=int, help="The minimum number of replicas.") + c.argument('max_replicas', type=int, help="The maximum number of replicas.") + + with self.argument_context('containerapp env') as c: + c.argument('name', name_type, help='Name of the Container Apps environment.') + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('location', arg_type=get_location_type(self.cli_ctx), help='Location of resource. Examples: Canada Central, North Europe') + c.argument('tags', arg_type=tags_type) + + with self.argument_context('containerapp env', arg_group='Log Analytics') as c: + c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Name or resource ID of the Log Analytics workspace to send diagnostics logs to. You can use \"az monitor log-analytics workspace create\" to create one. Extra billing may apply.') + 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=['--dapr-instrumentation-key'], help='Application Insights 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 and user app containers.') + c.argument('app_subnet_resource_id', type=str, options_list=['--app-subnet-resource-id'], help='Resource ID of a subnet that Container App containers are injected into. This subnet must be in the same VNET as the subnet defined in infrastructureSubnetResourceId.') + c.argument('docker_bridge_cidr', type=str, options_list=['--docker-bridge-cidr'], help='CIDR notation IP range assigned to the Docker bridge. It must not overlap with any Subnet IP ranges or the IP range defined in Platform Reserved CIDR, if defined') + c.argument('platform_reserved_cidr', type=str, options_list=['--platform-reserved-cidr'], help='IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. It must not overlap with any other Subnet IP ranges') + c.argument('platform_reserved_dns_ip', type=str, options_list=['--platform-reserved-dns-ip'], help='An IP address from the IP range defined by Platform Reserved CIDR that will be reserved for the internal DNS server.') + c.argument('internal_only', arg_type=get_three_state_flag(), options_list=['--internal-only'], help='Boolean indicating the environment only has an internal load balancer. These environments do not have a public static IP resource, therefore must provide infrastructureSubnetResourceId and appSubnetResourceId if enabling this property') + + with self.argument_context('containerapp env update') as c: + c.argument('name', name_type, help='Name of the Container Apps environment.') + c.argument('tags', arg_type=tags_type) + + with self.argument_context('containerapp env delete') as c: + c.argument('name', name_type, help='Name of the Container Apps Environment.') + + 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 github-action add') as c: + c.argument('repo_url', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com//') + c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') + c.argument('branch', options_list=['--branch', '-b'], help='The branch of the GitHub repo. Defaults to "main" if not specified.') + c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token') + c.argument('registry_url', help='The container registry server, e.g. myregistry.azurecr.io') + c.argument('registry_username', help='The username of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied') + c.argument('registry_password', help='The password of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied') + c.argument('docker_file_path', help='The dockerfile location, e.g. ./Dockerfile') + c.argument('service_principal_client_id', help='The service principal client ID. ') + c.argument('service_principal_client_secret', help='The service principal client secret.') + c.argument('service_principal_tenant_id', help='The service principal tenant ID.') + + with self.argument_context('containerapp github-action delete') as c: + c.argument('token', help='A Personal Access Token with write access to the specified repository. For more information: https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') + c.argument('login_with_github', help='Interactively log in with Github to retrieve the Personal Access Token') + + with self.argument_context('containerapp revision') as c: + c.argument('revision_name', options_list=['--revision'], type=str, help='Name of the revision.') + + with self.argument_context('containerapp revision copy') as c: + c.argument('from_revision', type=str, help='Revision to copy from. Default: latest revision.') + + with self.argument_context('containerapp ingress') as c: + c.argument('allow_insecure', help='Allow insecure connections for ingress traffic.') + c.argument('type', validator=validate_ingress, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") + c.argument('transport', arg_type=get_enum_type(['auto', 'http', 'http2']), help="The transport protocol used for ingress traffic.") + c.argument('target_port', type=int, validator=validate_target_port, help="The application port used for ingress traffic.") + + with self.argument_context('containerapp ingress traffic') as c: + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + + with self.argument_context('containerapp secret set') as c: + c.argument('secrets', nargs='+', options_list=['--secrets', '-s'], help="A list of secret(s) for the container app. Space-separated values in 'key=value' format.") + + with self.argument_context('containerapp secret show') as c: + c.argument('secret_name', help="The name of the secret to show.") + + with self.argument_context('containerapp secret remove') as c: + c.argument('secret_names', nargs='+', help="A list of secret(s) for the container app. Space-separated secret values names.") + + with self.argument_context('containerapp env dapr-component') as c: + c.argument('dapr_app_id', help="The Dapr app ID.") + c.argument('dapr_app_port', help="The port of your app.") + c.argument('dapr_app_protocol', help="Tell Dapr which protocol your application is using. Allowed values: grpc, http.") + c.argument('dapr_component_name', help="The Dapr component name.") + c.argument('environment_name', options_list=['--name', '-n'], help="The environment name.") + + with self.argument_context('containerapp revision set-mode') as c: + c.argument('mode', arg_type=get_enum_type(['single', 'multiple']), help="The active revisions mode for the container app.") + + with self.argument_context('containerapp registry') as c: + c.argument('server', help="The container registry server, e.g. myregistry.azurecr.io") + c.argument('username', help='The username of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied') + c.argument('password', help='The password of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied') + + with self.argument_context('containerapp registry list') as c: + c.argument('name', id_part=None) + + with self.argument_context('containerapp secret list') as c: + c.argument('name', id_part=None) + + with self.argument_context('containerapp revision list') as c: + c.argument('name', id_part=None) diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py new file mode 100644 index 00000000000..dd93bfce7c2 --- /dev/null +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -0,0 +1,3396 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# coding=utf-8 +# -------------------------------------------------------------------------- +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- +# pylint: disable=line-too-long, super-with-arguments, too-many-instance-attributes + +from msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class AllowedAudiencesValidation(Model): + """The configuration settings of the Allowed Audiences validation flow. + + :param allowed_audiences: The configuration settings of the allowed list + of audiences from which to validate the JWT token. + :type allowed_audiences: list[str] + """ + + _attribute_map = { + 'allowed_audiences': {'key': 'allowedAudiences', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(AllowedAudiencesValidation, self).__init__(**kwargs) + self.allowed_audiences = kwargs.get('allowed_audiences', None) + + +class Apple(Model): + """The configuration settings of the Apple provider. + + :param state: Disabled if the Apple provider should not be + enabled despite the set registration; otherwise, Enabled. + Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the Apple registration. + :type registration: ~commondefinitions.models.AppleRegistration + :param login: The configuration settings of the login flow. + :type login: ~commondefinitions.models.LoginScopes + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'AppleRegistration'}, + 'login': {'key': 'login', 'type': 'LoginScopes'}, + } + + def __init__(self, **kwargs): + super(Apple, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.login = kwargs.get('login', None) + + +class AppleRegistration(Model): + """The configuration settings of the registration for the Apple provider. + + :param client_id: The Client ID of the app used for login. + :type client_id: str + :param client_secret_ref_name: The app secret ref name that contains the + client secret. + :type client_secret_ref_name: str + """ + + _attribute_map = { + 'client_id': {'key': 'clientId', 'type': 'str'}, + 'client_secret_ref_name': {'key': 'clientSecretRefName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AppleRegistration, self).__init__(**kwargs) + self.client_id = kwargs.get('client_id', None) + self.client_secret_ref_name = kwargs.get('client_secret_ref_name', None) + + +class AppLogsConfiguration(Model): + """Configuration of application logs. + + :param destination: Logs destination + :type destination: str + :param log_analytics_configuration: Log Analytics configuration + :type log_analytics_configuration: + ~commondefinitions.models.LogAnalyticsConfiguration + """ + + _attribute_map = { + 'destination': {'key': 'destination', 'type': 'str'}, + 'log_analytics_configuration': {'key': 'logAnalyticsConfiguration', 'type': 'LogAnalyticsConfiguration'}, + } + + def __init__(self, **kwargs): + super(AppLogsConfiguration, self).__init__(**kwargs) + self.destination = kwargs.get('destination', None) + self.log_analytics_configuration = kwargs.get('log_analytics_configuration', None) + + +class AppRegistration(Model): + """The configuration settings of the app registration for providers that have + app ids and app secrets. + + :param app_id: The App ID of the app used for login. + :type app_id: str + :param app_secret_ref_name: The app secret ref name that contains the app + secret. + :type app_secret_ref_name: str + """ + + _attribute_map = { + 'app_id': {'key': 'appId', 'type': 'str'}, + 'app_secret_ref_name': {'key': 'appSecretRefName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AppRegistration, self).__init__(**kwargs) + self.app_id = kwargs.get('app_id', None) + self.app_secret_ref_name = kwargs.get('app_secret_ref_name', None) + + +class Resource(Model): + """Resource. + + Common fields that are returned in the response for all Azure Resource + Manager resources. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + } + + def __init__(self, **kwargs): + super(Resource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.system_data = None + + +class ProxyResource(Resource): + """Proxy Resource. + + The resource model definition for a Azure Resource Manager proxy resource. + It will not have tags and a location. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + } + + # def __init__(self, **kwargs): + # super(ProxyResource, self).__init__(**kwargs) + + +class AuthConfig(ProxyResource): + """Configuration settings for the Azure ContainerApp Authentication / + Authorization feature. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param state: Enabled if the Authentication / Authorization + feature is enabled for the current app; otherwise, Disabled. + Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.EasyAuthState + :param global_validation: The configuration settings that determines the + validation flow of users using ContainerApp Authentication/Authorization. + :type global_validation: ~commondefinitions.models.GlobalValidation + :param identity_providers: The configuration settings of each of the + identity providers used to configure ContainerApp + Authentication/Authorization. + :type identity_providers: ~commondefinitions.models.IdentityProviders + :param login: The configuration settings of the login flow of users using + ContainerApp Authentication/Authorization. + :type login: ~commondefinitions.models.Login + :param http_settings: The configuration settings of the HTTP requests for + authentication and authorization requests made against ContainerApp + Authentication/Authorization. + :type http_settings: ~commondefinitions.models.HttpSettings + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'state': {'key': 'properties.state', 'type': 'str'}, + 'global_validation': {'key': 'properties.globalValidation', 'type': 'GlobalValidation'}, + 'identity_providers': {'key': 'properties.identityProviders', 'type': 'IdentityProviders'}, + 'login': {'key': 'properties.login', 'type': 'Login'}, + 'http_settings': {'key': 'properties.httpSettings', 'type': 'HttpSettings'}, + } + + def __init__(self, **kwargs): + super(AuthConfig, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.global_validation = kwargs.get('global_validation', None) + self.identity_providers = kwargs.get('identity_providers', None) + self.login = kwargs.get('login', None) + self.http_settings = kwargs.get('http_settings', None) + + +class AuthConfigCollection(Model): + """AuthConfig collection ARM resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.AuthConfig] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[AuthConfig]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AuthConfigCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class AvailableOperations(Model): + """Available operations of the service. + + :param value: Collection of available operation details + :type value: list[~commondefinitions.models.OperationDetail] + :param next_link: URL client should use to fetch the next page (per server + side paging). + It's null for now, added for future use. + :type next_link: str + """ + + _attribute_map = { + 'value': {'key': 'value', 'type': '[OperationDetail]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AvailableOperations, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = kwargs.get('next_link', None) + + +class AzureActiveDirectory(Model): + """The configuration settings of the Azure Active directory provider. + + :param state: Disabled if the Azure Active Directory provider + should not be enabled despite the set registration; otherwise, + Enabled. Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the Azure Active + Directory app registration. + :type registration: + ~commondefinitions.models.AzureActiveDirectoryRegistration + :param login: The configuration settings of the Azure Active Directory + login flow. + :type login: ~commondefinitions.models.AzureActiveDirectoryLogin + :param validation: The configuration settings of the Azure Active + Directory token validation flow. + :type validation: ~commondefinitions.models.AzureActiveDirectoryValidation + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'AzureActiveDirectoryRegistration'}, + 'login': {'key': 'login', 'type': 'AzureActiveDirectoryLogin'}, + 'validation': {'key': 'validation', 'type': 'AzureActiveDirectoryValidation'}, + } + + def __init__(self, **kwargs): + super(AzureActiveDirectory, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.login = kwargs.get('login', None) + self.validation = kwargs.get('validation', None) + + +class AzureActiveDirectoryLogin(Model): + """The configuration settings of the Azure Active Directory login flow. + + :param login_parameters: Login parameters to send to the OpenID Connect + authorization endpoint when + a user logs in. Each parameter must be in the form "key=value". + :type login_parameters: list[str] + :param disable_www_authenticate: true if the www-authenticate + provider should be omitted from the request; otherwise, + false. Possible values include: 'True', 'False' + :type disable_www_authenticate: str or + ~commondefinitions.models.DisableWwwAuthenticateMode + """ + + _attribute_map = { + 'login_parameters': {'key': 'loginParameters', 'type': '[str]'}, + 'disable_www_authenticate': {'key': 'disableWwwAuthenticate', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AzureActiveDirectoryLogin, self).__init__(**kwargs) + self.login_parameters = kwargs.get('login_parameters', None) + self.disable_www_authenticate = kwargs.get('disable_www_authenticate', None) + + +class AzureActiveDirectoryRegistration(Model): + """The configuration settings of the Azure Active Directory app registration. + + :param open_id_issuer: The OpenID Connect Issuer URI that represents the + entity which issues access tokens for this application. + When using Azure Active Directory, this value is the URI of the directory + tenant, e.g. https://login.microsoftonline.com/v2.0/{tenant-guid}/. + This URI is a case-sensitive identifier for the token issuer. + More information on OpenID Connect Discovery: + http://openid.net/specs/openid-connect-discovery-1_0.html + :type open_id_issuer: str + :param client_id: The Client ID of this relying party application, known + as the client_id. + This setting is required for enabling OpenID Connection authentication + with Azure Active Directory or + other 3rd party OpenID Connect providers. + More information on OpenID Connect: + http://openid.net/specs/openid-connect-core-1_0.html + :type client_id: str + :param client_secret_ref_name: The app secret ref name that contains the + client secret of the relying party application. + :type client_secret_ref_name: str + :param client_secret_certificate_thumbprint: An alternative to the client + secret, that is the thumbprint of a certificate used for signing purposes. + This property acts as + a replacement for the Client Secret. It is also optional. + :type client_secret_certificate_thumbprint: str + :param client_secret_certificate_subject_alternative_name: An alternative + to the client secret thumbprint, that is the subject alternative name of a + certificate used for signing purposes. This property acts as + a replacement for the Client Secret Certificate Thumbprint. It is also + optional. + :type client_secret_certificate_subject_alternative_name: str + :param client_secret_certificate_issuer: An alternative to the client + secret thumbprint, that is the issuer of a certificate used for signing + purposes. This property acts as + a replacement for the Client Secret Certificate Thumbprint. It is also + optional. + :type client_secret_certificate_issuer: str + """ + + _attribute_map = { + 'open_id_issuer': {'key': 'openIdIssuer', 'type': 'str'}, + 'client_id': {'key': 'clientId', 'type': 'str'}, + 'client_secret_ref_name': {'key': 'clientSecretRefName', 'type': 'str'}, + 'client_secret_certificate_thumbprint': {'key': 'clientSecretCertificateThumbprint', 'type': 'str'}, + 'client_secret_certificate_subject_alternative_name': {'key': 'clientSecretCertificateSubjectAlternativeName', 'type': 'str'}, + 'client_secret_certificate_issuer': {'key': 'clientSecretCertificateIssuer', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AzureActiveDirectoryRegistration, self).__init__(**kwargs) + self.open_id_issuer = kwargs.get('open_id_issuer', None) + self.client_id = kwargs.get('client_id', None) + self.client_secret_ref_name = kwargs.get('client_secret_ref_name', None) + self.client_secret_certificate_thumbprint = kwargs.get('client_secret_certificate_thumbprint', None) + self.client_secret_certificate_subject_alternative_name = kwargs.get('client_secret_certificate_subject_alternative_name', None) + self.client_secret_certificate_issuer = kwargs.get('client_secret_certificate_issuer', None) + + +class AzureActiveDirectoryValidation(Model): + """The configuration settings of the Azure Active Directory token validation + flow. + + :param allowed_audiences: The list of audiences that can make successful + authentication/authorization requests. + :type allowed_audiences: list[str] + """ + + _attribute_map = { + 'allowed_audiences': {'key': 'allowedAudiences', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(AzureActiveDirectoryValidation, self).__init__(**kwargs) + self.allowed_audiences = kwargs.get('allowed_audiences', None) + + +class AzureCredentials(Model): + """Container App credentials. + + :param client_id: Client Id. + :type client_id: str + :param client_secret: Client Secret. + :type client_secret: str + :param tenant_id: Tenant Id. + :type tenant_id: str + :param subscription_id: Subscription Id. + :type subscription_id: str + """ + + _attribute_map = { + 'client_id': {'key': 'clientId', 'type': 'str'}, + 'client_secret': {'key': 'clientSecret', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + 'subscription_id': {'key': 'subscriptionId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AzureCredentials, self).__init__(**kwargs) + self.client_id = kwargs.get('client_id', None) + self.client_secret = kwargs.get('client_secret', None) + self.tenant_id = kwargs.get('tenant_id', None) + self.subscription_id = kwargs.get('subscription_id', None) + + +class AzureEntityResource(Resource): + """Entity Resource. + + The resource model definition for an Azure Resource Manager resource with + an etag. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :ivar etag: Resource Etag. + :vartype etag: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'etag': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'etag': {'key': 'etag', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AzureEntityResource, self).__init__(**kwargs) + self.etag = None + + +class AzureFileProperties(Model): + """Azure File Properties. + + :param account_name: Storage account name for azure file. + :type account_name: str + :param account_key: Storage account key for azure file. + :type account_key: str + :param access_mode: Access mode for storage. Possible values include: + 'ReadOnly', 'ReadWrite' + :type access_mode: str or ~commondefinitions.models.AccessMode + :param share_name: Azure file share name. + :type share_name: str + """ + + _attribute_map = { + 'account_name': {'key': 'accountName', 'type': 'str'}, + 'account_key': {'key': 'accountKey', 'type': 'str'}, + 'access_mode': {'key': 'accessMode', 'type': 'str'}, + 'share_name': {'key': 'shareName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AzureFileProperties, self).__init__(**kwargs) + self.account_name = kwargs.get('account_name', None) + self.account_key = kwargs.get('account_key', None) + self.access_mode = kwargs.get('access_mode', None) + self.share_name = kwargs.get('share_name', None) + + +class AzureStaticWebApp(Model): + """The configuration settings of the Azure Static Web Apps provider. + + :param state: Disabled if the Azure Static Web Apps provider + should not be enabled despite the set registration; otherwise, + Enabled. Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the Azure Static Web + Apps registration. + :type registration: + ~commondefinitions.models.AzureStaticWebAppRegistration + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'AzureStaticWebAppRegistration'}, + } + + def __init__(self, **kwargs): + super(AzureStaticWebApp, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + + +class AzureStaticWebAppRegistration(Model): + """The configuration settings of the registration for the Azure Static Web + Apps provider. + + :param client_id: The Client ID of the app used for login. + :type client_id: str + """ + + _attribute_map = { + 'client_id': {'key': 'clientId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(AzureStaticWebAppRegistration, self).__init__(**kwargs) + self.client_id = kwargs.get('client_id', None) + + +class TrackedResource(Resource): + """Tracked Resource. + + The resource model definition for an Azure Resource Manager tracked top + level resource which has 'tags' and a 'location'. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param tags: Resource tags. + :type tags: dict[str, str] + :param location: Required. The geo-location where the resource lives + :type location: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'location': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'location': {'key': 'location', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(TrackedResource, self).__init__(**kwargs) + self.tags = kwargs.get('tags', None) + self.location = kwargs.get('location', None) + + +class Certificate(TrackedResource): + """Certificate used for Custom Domain bindings of Container Apps in a Managed + Environment. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param tags: Resource tags. + :type tags: dict[str, str] + :param location: Required. The geo-location where the resource lives + :type location: str + :param properties: Certificate resource specific properties + :type properties: ~commondefinitions.models.CertificateProperties + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'location': {'required': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'location': {'key': 'location', 'type': 'str'}, + 'properties': {'key': 'properties', 'type': 'CertificateProperties'}, + } + + def __init__(self, **kwargs): + super(Certificate, self).__init__(**kwargs) + self.properties = kwargs.get('properties', None) + + +class CertificateCollection(Model): + """Collection of Certificates. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.Certificate] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[Certificate]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(CertificateCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class CertificatePatch(Model): + """A certificate to update. + + :param tags: Application-specific metadata in the form of key-value pairs. + :type tags: dict[str, str] + """ + + _attribute_map = { + 'tags': {'key': 'tags', 'type': '{str}'}, + } + + def __init__(self, **kwargs): + super(CertificatePatch, self).__init__(**kwargs) + self.tags = kwargs.get('tags', None) + + +class CertificateProperties(Model): + """Certificate resource specific properties. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param password: Certificate password. + :type password: str + :ivar subject_name: Subject name of the certificate. + :vartype subject_name: str + :param value: PFX or PEM blob + :type value: bytearray + :ivar issuer: Certificate issuer. + :vartype issuer: str + :ivar issue_date: Certificate issue Date. + :vartype issue_date: datetime + :ivar expiration_date: Certificate expiration date. + :vartype expiration_date: datetime + :ivar thumbprint: Certificate thumbprint. + :vartype thumbprint: str + :ivar valid: Is the certificate valid?. + :vartype valid: bool + :ivar public_key_hash: Public key hash. + :vartype public_key_hash: str + """ + + _validation = { + 'subject_name': {'readonly': True}, + 'issuer': {'readonly': True}, + 'issue_date': {'readonly': True}, + 'expiration_date': {'readonly': True}, + 'thumbprint': {'readonly': True}, + 'valid': {'readonly': True}, + 'public_key_hash': {'readonly': True}, + } + + _attribute_map = { + 'password': {'key': 'password', 'type': 'str'}, + 'subject_name': {'key': 'subjectName', 'type': 'str'}, + 'value': {'key': 'value', 'type': 'bytearray'}, + 'issuer': {'key': 'issuer', 'type': 'str'}, + 'issue_date': {'key': 'issueDate', 'type': 'iso-8601'}, + 'expiration_date': {'key': 'expirationDate', 'type': 'iso-8601'}, + 'thumbprint': {'key': 'thumbprint', 'type': 'str'}, + 'valid': {'key': 'valid', 'type': 'bool'}, + 'public_key_hash': {'key': 'publicKeyHash', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(CertificateProperties, self).__init__(**kwargs) + self.password = kwargs.get('password', None) + self.subject_name = None + self.value = kwargs.get('value', None) + self.issuer = None + self.issue_date = None + self.expiration_date = None + self.thumbprint = None + self.valid = None + self.public_key_hash = None + + +class ClientRegistration(Model): + """The configuration settings of the app registration for providers that have + client ids and client secrets. + + :param client_id: The Client ID of the app used for login. + :type client_id: str + :param client_secret_ref_name: The app secret ref name that contains the + client secret. + :type client_secret_ref_name: str + """ + + _attribute_map = { + 'client_id': {'key': 'clientId', 'type': 'str'}, + 'client_secret_ref_name': {'key': 'clientSecretRefName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ClientRegistration, self).__init__(**kwargs) + self.client_id = kwargs.get('client_id', None) + self.client_secret_ref_name = kwargs.get('client_secret_ref_name', None) + + +class Configuration(Model): + """Non versioned Container App configuration properties that define the + mutable settings of a Container app. + + :param secrets: Collection of secrets used by a Container app + :type secrets: list[~commondefinitions.models.Secret] + :param active_revisions_mode: ActiveRevisionsMode controls how active + revisions are handled for the Container app: + Multiple: multiple revisions can be active. If no value if + provided, this is the defaultSingle: Only one revision can be + active at a time. Revision weights can not be used in this + mode. Possible values include: 'multiple', 'single' + :type active_revisions_mode: str or + ~commondefinitions.models.ActiveRevisionsMode + :param ingress: Ingress configurations. + :type ingress: ~commondefinitions.models.Ingress + :param dapr: Dapr configuration for the Container App. + :type dapr: ~commondefinitions.models.Dapr + :param registries: Collection of private container registry credentials + for containers used by the Container app + :type registries: list[~commondefinitions.models.RegistryCredentials] + """ + + _attribute_map = { + 'secrets': {'key': 'secrets', 'type': '[Secret]'}, + 'active_revisions_mode': {'key': 'activeRevisionsMode', 'type': 'str'}, + 'ingress': {'key': 'ingress', 'type': 'Ingress'}, + 'dapr': {'key': 'dapr', 'type': 'Dapr'}, + 'registries': {'key': 'registries', 'type': '[RegistryCredentials]'}, + } + + def __init__(self, **kwargs): + super(Configuration, self).__init__(**kwargs) + self.secrets = kwargs.get('secrets', None) + self.active_revisions_mode = kwargs.get('active_revisions_mode', None) + self.ingress = kwargs.get('ingress', None) + self.dapr = kwargs.get('dapr', None) + self.registries = kwargs.get('registries', None) + + +class Container(Model): + """Container App container definition. + + :param image: Container image tag. + :type image: str + :param name: Custom container name. + :type name: str + :param command: Container start command. + :type command: list[str] + :param args: Container start command arguments. + :type args: list[str] + :param env: Container environment variables. + :type env: list[~commondefinitions.models.EnvironmentVar] + :param resources: Container resource requirements. + :type resources: ~commondefinitions.models.ContainerResources + :param probes: List of probes for the container. + :type probes: list[~commondefinitions.models.ContainerAppProbe] + :param volume_mounts: Container volume mounts. + :type volume_mounts: list[~commondefinitions.models.VolumeMount] + """ + + _attribute_map = { + 'image': {'key': 'image', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'command': {'key': 'command', 'type': '[str]'}, + 'args': {'key': 'args', 'type': '[str]'}, + 'env': {'key': 'env', 'type': '[EnvironmentVar]'}, + 'resources': {'key': 'resources', 'type': 'ContainerResources'}, + 'probes': {'key': 'probes', 'type': '[ContainerAppProbe]'}, + 'volume_mounts': {'key': 'volumeMounts', 'type': '[VolumeMount]'}, + } + + def __init__(self, **kwargs): + super(Container, self).__init__(**kwargs) + self.image = kwargs.get('image', None) + self.name = kwargs.get('name', None) + self.command = kwargs.get('command', None) + self.args = kwargs.get('args', None) + self.env = kwargs.get('env', None) + self.resources = kwargs.get('resources', None) + self.probes = kwargs.get('probes', None) + self.volume_mounts = kwargs.get('volume_mounts', None) + + +class ContainerApp(TrackedResource): + """Container App. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param tags: Resource tags. + :type tags: dict[str, str] + :param location: Required. The geo-location where the resource lives + :type location: str + :param identity: managed identities for the Container App to interact with + other Azure services without maintaining any secrets or credentials in + code. + :type identity: ~commondefinitions.models.ManagedServiceIdentity + :ivar provisioning_state: Provisioning state of the Container App. + Possible values include: 'InProgress', 'Succeeded', 'Failed', 'Canceled' + :vartype provisioning_state: str or + ~commondefinitions.models.ContainerAppProvisioningState + :param managed_environment_id: Resource ID of the Container App's + environment. + :type managed_environment_id: str + :ivar latest_revision_name: Name of the latest revision of the Container + App. + :vartype latest_revision_name: str + :ivar latest_revision_fqdn: Fully Qualified Domain Name of the latest + revision of the Container App. + :vartype latest_revision_fqdn: str + :ivar custom_domain_verification_id: Id used to verify domain name + ownership + :vartype custom_domain_verification_id: str + :param configuration: Non versioned Container App configuration + properties. + :type configuration: ~commondefinitions.models.Configuration + :param template: Container App versioned application definition. + :type template: ~commondefinitions.models.Template + :ivar outbound_ip_addresses: Outbound IP Addresses for container app. + :vartype outbound_ip_addresses: list[str] + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'location': {'required': True}, + 'provisioning_state': {'readonly': True}, + 'latest_revision_name': {'readonly': True}, + 'latest_revision_fqdn': {'readonly': True}, + 'custom_domain_verification_id': {'readonly': True}, + 'outbound_ip_addresses': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'location': {'key': 'location', 'type': 'str'}, + 'identity': {'key': 'identity', 'type': 'ManagedServiceIdentity'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'managed_environment_id': {'key': 'properties.managedEnvironmentId', 'type': 'str'}, + 'latest_revision_name': {'key': 'properties.latestRevisionName', 'type': 'str'}, + 'latest_revision_fqdn': {'key': 'properties.latestRevisionFqdn', 'type': 'str'}, + 'custom_domain_verification_id': {'key': 'properties.customDomainVerificationId', 'type': 'str'}, + 'configuration': {'key': 'properties.configuration', 'type': 'Configuration'}, + 'template': {'key': 'properties.template', 'type': 'Template'}, + 'outbound_ip_addresses': {'key': 'properties.outboundIPAddresses', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(ContainerApp, self).__init__(**kwargs) + self.identity = kwargs.get('identity', None) + self.provisioning_state = None + self.managed_environment_id = kwargs.get('managed_environment_id', None) + self.latest_revision_name = None + self.latest_revision_fqdn = None + self.custom_domain_verification_id = None + self.configuration = kwargs.get('configuration', None) + self.template = kwargs.get('template', None) + self.outbound_ip_addresses = None + + +class ContainerAppCollection(Model): + """Container App collection ARM resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.ContainerApp] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ContainerApp]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ContainerAppCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class ContainerAppPatch(Model): + """Container App Patch. + + :param tags: Application-specific metadata in the form of key-value pairs. + :type tags: dict[str, str] + """ + + _attribute_map = { + 'tags': {'key': 'tags', 'type': '{str}'}, + } + + def __init__(self, **kwargs): + super(ContainerAppPatch, self).__init__(**kwargs) + self.tags = kwargs.get('tags', None) + + +class ContainerAppProbe(Model): + """Probe describes a health check to be performed against a container to + determine whether it is alive or ready to receive traffic. + + :param failure_threshold: Minimum consecutive failures for the probe to be + considered failed after having succeeded. Defaults to 3. Minimum value is + 1. Maximum value is 10. + :type failure_threshold: int + :param http_get: HTTPGet specifies the http request to perform. + :type http_get: ~commondefinitions.models.ContainerAppProbeHttpGet + :param initial_delay_seconds: Number of seconds after the container has + started before liveness probes are initiated. Minimum value is 1. Maximum + value is 60. + :type initial_delay_seconds: int + :param period_seconds: How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. Maximum value is 240. + :type period_seconds: int + :param success_threshold: Minimum consecutive successes for the probe to + be considered successful after having failed. Defaults to 1. Must be 1 for + liveness and startup. Minimum value is 1. Maximum value is 10. + :type success_threshold: int + :param tcp_socket: TCPSocket specifies an action involving a TCP port. TCP + hooks not yet supported. + :type tcp_socket: ~commondefinitions.models.ContainerAppProbeTcpSocket + :param termination_grace_period_seconds: Optional duration in seconds the + pod needs to terminate gracefully upon probe failure. The grace period is + the duration in seconds after the processes running in the pod are sent a + termination signal and the time when the processes are forcibly halted + with a kill signal. Set this value longer than the expected cleanup time + for your process. If this value is nil, the pod's + terminationGracePeriodSeconds will be used. Otherwise, this value + overrides the value provided by the pod spec. Value must be non-negative + integer. The value zero indicates stop immediately via the kill signal (no + opportunity to shut down). This is an alpha field and requires enabling + ProbeTerminationGracePeriod feature gate. Maximum value is 3600 seconds (1 + hour) + :type termination_grace_period_seconds: long + :param timeout_seconds: Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. Maximum value is 240. + :type timeout_seconds: int + :param type: The type of probe. Possible values include: 'liveness', + 'readiness', 'startup' + :type type: str or ~commondefinitions.models.Type + """ + + _attribute_map = { + 'failure_threshold': {'key': 'failureThreshold', 'type': 'int'}, + 'http_get': {'key': 'httpGet', 'type': 'ContainerAppProbeHttpGet'}, + 'initial_delay_seconds': {'key': 'initialDelaySeconds', 'type': 'int'}, + 'period_seconds': {'key': 'periodSeconds', 'type': 'int'}, + 'success_threshold': {'key': 'successThreshold', 'type': 'int'}, + 'tcp_socket': {'key': 'tcpSocket', 'type': 'ContainerAppProbeTcpSocket'}, + 'termination_grace_period_seconds': {'key': 'terminationGracePeriodSeconds', 'type': 'long'}, + 'timeout_seconds': {'key': 'timeoutSeconds', 'type': 'int'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ContainerAppProbe, self).__init__(**kwargs) + self.failure_threshold = kwargs.get('failure_threshold', None) + self.http_get = kwargs.get('http_get', None) + self.initial_delay_seconds = kwargs.get('initial_delay_seconds', None) + self.period_seconds = kwargs.get('period_seconds', None) + self.success_threshold = kwargs.get('success_threshold', None) + self.tcp_socket = kwargs.get('tcp_socket', None) + self.termination_grace_period_seconds = kwargs.get('termination_grace_period_seconds', None) + self.timeout_seconds = kwargs.get('timeout_seconds', None) + self.type = kwargs.get('type', None) + + +class ContainerAppProbeHttpGet(Model): + """HTTPGet specifies the http request to perform. + + All required parameters must be populated in order to send to Azure. + + :param host: Host name to connect to, defaults to the pod IP. You probably + want to set "Host" in httpHeaders instead. + :type host: str + :param http_headers: Custom headers to set in the request. HTTP allows + repeated headers. + :type http_headers: + list[~commondefinitions.models.ContainerAppProbeHttpGetHttpHeadersItem] + :param path: Path to access on the HTTP server. + :type path: str + :param port: Required. Name or number of the port to access on the + container. Number must be in the range 1 to 65535. Name must be an + IANA_SVC_NAME. + :type port: int + :param scheme: Scheme to use for connecting to the host. Defaults to HTTP. + :type scheme: str + """ + + _validation = { + 'port': {'required': True}, + } + + _attribute_map = { + 'host': {'key': 'host', 'type': 'str'}, + 'http_headers': {'key': 'httpHeaders', 'type': '[ContainerAppProbeHttpGetHttpHeadersItem]'}, + 'path': {'key': 'path', 'type': 'str'}, + 'port': {'key': 'port', 'type': 'int'}, + 'scheme': {'key': 'scheme', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ContainerAppProbeHttpGet, self).__init__(**kwargs) + self.host = kwargs.get('host', None) + self.http_headers = kwargs.get('http_headers', None) + self.path = kwargs.get('path', None) + self.port = kwargs.get('port', None) + self.scheme = kwargs.get('scheme', None) + + +class ContainerAppProbeHttpGetHttpHeadersItem(Model): + """HTTPHeader describes a custom header to be used in HTTP probes. + + All required parameters must be populated in order to send to Azure. + + :param name: Required. The header field name + :type name: str + :param value: Required. The header field value + :type value: str + """ + + _validation = { + 'name': {'required': True}, + 'value': {'required': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'value': {'key': 'value', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ContainerAppProbeHttpGetHttpHeadersItem, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.value = kwargs.get('value', None) + + +class ContainerAppProbeTcpSocket(Model): + """TCPSocket specifies an action involving a TCP port. TCP hooks not yet + supported. + + All required parameters must be populated in order to send to Azure. + + :param host: Optional: Host name to connect to, defaults to the pod IP. + :type host: str + :param port: Required. Number or name of the port to access on the + container. Number must be in the range 1 to 65535. Name must be an + IANA_SVC_NAME. + :type port: int + """ + + _validation = { + 'port': {'required': True}, + } + + _attribute_map = { + 'host': {'key': 'host', 'type': 'str'}, + 'port': {'key': 'port', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(ContainerAppProbeTcpSocket, self).__init__(**kwargs) + self.host = kwargs.get('host', None) + self.port = kwargs.get('port', None) + + +class ContainerAppSecret(Model): + """Container App Secret. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar name: Secret Name. + :vartype name: str + :ivar value: Secret Value. + :vartype value: str + """ + + _validation = { + 'name': {'readonly': True}, + 'value': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'value': {'key': 'value', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ContainerAppSecret, self).__init__(**kwargs) + self.name = None + self.value = None + + +class ContainerResources(Model): + """Container App container resource requirements. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param cpu: Required CPU in cores, e.g. 0.5 + :type cpu: float + :param memory: Required memory, e.g. "250Mb" + :type memory: str + :ivar ephemeral_storage: Ephemeral Storage, e.g. "1Gi" + :vartype ephemeral_storage: str + """ + + _validation = { + 'ephemeral_storage': {'readonly': True}, + } + + _attribute_map = { + 'cpu': {'key': 'cpu', 'type': 'float'}, + 'memory': {'key': 'memory', 'type': 'str'}, + 'ephemeral_storage': {'key': 'ephemeralStorage', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ContainerResources, self).__init__(**kwargs) + self.cpu = kwargs.get('cpu', None) + self.memory = kwargs.get('memory', None) + self.ephemeral_storage = None + + +class CustomDomain(Model): + """Custom Domain of a Container App. + + :param name: Hostname. + :type name: str + :param binding_type: Custom Domain binding type. Possible values include: + 'Disabled', 'SniEnabled' + :type binding_type: str or ~commondefinitions.models.BindingType + :param certificate_id: Resource Id of the Certificate to be bound to this + hostname. Must exist in the Managed Environment. + :type certificate_id: str + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'binding_type': {'key': 'bindingType', 'type': 'str'}, + 'certificate_id': {'key': 'certificateId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(CustomDomain, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.binding_type = kwargs.get('binding_type', None) + self.certificate_id = kwargs.get('certificate_id', None) + + +class CustomHostnameAnalysisResult(ProxyResource): + """Custom domain analysis. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :ivar host_name: Host name that was analyzed + :vartype host_name: str + :ivar is_hostname_already_verified: true if hostname is + already verified; otherwise, false. + :vartype is_hostname_already_verified: bool + :ivar custom_domain_verification_test: DNS verification test result. + Possible values include: 'Passed', 'Failed', 'Skipped' + :vartype custom_domain_verification_test: str or + ~commondefinitions.models.DnsVerificationTestResult + :ivar custom_domain_verification_failure_info: Raw failure information if + DNS verification fails. + :vartype custom_domain_verification_failure_info: + ~commondefinitions.models.DefaultErrorResponse + :ivar has_conflict_on_managed_environment: true if there is a + conflict on the Container App's managed environment; otherwise, + false. + :vartype has_conflict_on_managed_environment: bool + :ivar conflicting_container_app_resource_id: Name of the conflicting + Container App on the Managed Environment if it's within the same + subscription. + :vartype conflicting_container_app_resource_id: str + :param c_name_records: CName records visible for this hostname. + :type c_name_records: list[str] + :param txt_records: TXT records visible for this hostname. + :type txt_records: list[str] + :param a_records: A records visible for this hostname. + :type a_records: list[str] + :param alternate_cname_records: Alternate CName records visible for this + hostname. + :type alternate_cname_records: list[str] + :param alternate_txt_records: Alternate TXT records visible for this + hostname. + :type alternate_txt_records: list[str] + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'host_name': {'readonly': True}, + 'is_hostname_already_verified': {'readonly': True}, + 'custom_domain_verification_test': {'readonly': True}, + 'custom_domain_verification_failure_info': {'readonly': True}, + 'has_conflict_on_managed_environment': {'readonly': True}, + 'conflicting_container_app_resource_id': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'host_name': {'key': 'properties.hostName', 'type': 'str'}, + 'is_hostname_already_verified': {'key': 'properties.isHostnameAlreadyVerified', 'type': 'bool'}, + 'custom_domain_verification_test': {'key': 'properties.customDomainVerificationTest', 'type': 'DnsVerificationTestResult'}, + 'custom_domain_verification_failure_info': {'key': 'properties.customDomainVerificationFailureInfo', 'type': 'DefaultErrorResponse'}, + 'has_conflict_on_managed_environment': {'key': 'properties.hasConflictOnManagedEnvironment', 'type': 'bool'}, + 'conflicting_container_app_resource_id': {'key': 'properties.conflictingContainerAppResourceId', 'type': 'str'}, + 'c_name_records': {'key': 'properties.cNameRecords', 'type': '[str]'}, + 'txt_records': {'key': 'properties.txtRecords', 'type': '[str]'}, + 'a_records': {'key': 'properties.aRecords', 'type': '[str]'}, + 'alternate_cname_records': {'key': 'properties.alternateCNameRecords', 'type': '[str]'}, + 'alternate_txt_records': {'key': 'properties.alternateTxtRecords', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(CustomHostnameAnalysisResult, self).__init__(**kwargs) + self.host_name = None + self.is_hostname_already_verified = None + self.custom_domain_verification_test = None + self.custom_domain_verification_failure_info = None + self.has_conflict_on_managed_environment = None + self.conflicting_container_app_resource_id = None + self.c_name_records = kwargs.get('c_name_records', None) + self.txt_records = kwargs.get('txt_records', None) + self.a_records = kwargs.get('a_records', None) + self.alternate_cname_records = kwargs.get('alternate_cname_records', None) + self.alternate_txt_records = kwargs.get('alternate_txt_records', None) + + +class CustomOpenIdConnectProvider(Model): + """The configuration settings of the custom Open ID Connect provider. + + :param state: Disabled if the custom Open ID Connect provider + should not be enabled despite the set registration; otherwise, + Enabled. Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the app registration + for the custom Open ID Connect provider. + :type registration: ~commondefinitions.models.OpenIdConnectRegistration + :param login: The configuration settings of the login flow of the custom + Open ID Connect provider. + :type login: ~commondefinitions.models.OpenIdConnectLogin + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'OpenIdConnectRegistration'}, + 'login': {'key': 'login', 'type': 'OpenIdConnectLogin'}, + } + + def __init__(self, **kwargs): + super(CustomOpenIdConnectProvider, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.login = kwargs.get('login', None) + + +class CustomScaleRule(Model): + """Container App container Custom scaling rule. + + :param type: Type of the custom scale rule + eg: azure-servicebus, redis etc. + :type type: str + :param metadata: Metadata properties to describe custom scale rule. + :type metadata: dict[str, str] + :param auth: Authentication secrets for the custom scale rule. + :type auth: list[~commondefinitions.models.ScaleRuleAuth] + """ + + _attribute_map = { + 'type': {'key': 'type', 'type': 'str'}, + 'metadata': {'key': 'metadata', 'type': '{str}'}, + 'auth': {'key': 'auth', 'type': '[ScaleRuleAuth]'}, + } + + def __init__(self, **kwargs): + super(CustomScaleRule, self).__init__(**kwargs) + self.type = kwargs.get('type', None) + self.metadata = kwargs.get('metadata', None) + self.auth = kwargs.get('auth', None) + + +class Dapr(Model): + """Container App Dapr configuration. + + :param enabled: Boolean indicating if the Dapr side car is enabled + :type enabled: bool + :param app_id: Dapr application identifier + :type app_id: str + :param app_protocol: Tells Dapr which protocol your application is using. + Valid options are http and grpc. Default is http. Possible values include: + 'http', 'grpc' + :type app_protocol: str or ~commondefinitions.models.AppProtocol + :param app_port: Tells Dapr which port your application is listening on + :type app_port: int + """ + + _attribute_map = { + 'enabled': {'key': 'enabled', 'type': 'bool'}, + 'app_id': {'key': 'appId', 'type': 'str'}, + 'app_protocol': {'key': 'appProtocol', 'type': 'str'}, + 'app_port': {'key': 'appPort', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(Dapr, self).__init__(**kwargs) + self.enabled = kwargs.get('enabled', None) + self.app_id = kwargs.get('app_id', None) + self.app_protocol = kwargs.get('app_protocol', None) + self.app_port = kwargs.get('app_port', None) + + +class DaprComponent(ProxyResource): + """Dapr Component. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param component_type: Component type + :type component_type: str + :param version: Component version + :type version: str + :param ignore_errors: Boolean describing if the component errors are + ignores + :type ignore_errors: bool + :param init_timeout: Initialization timeout + :type init_timeout: str + :param secrets: Collection of secrets used by a Dapr component + :type secrets: list[~commondefinitions.models.Secret] + :param metadata: Component metadata + :type metadata: list[~commondefinitions.models.DaprMetadata] + :param scopes: Names of container apps that can use this Dapr component + :type scopes: list[str] + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'component_type': {'key': 'properties.componentType', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'ignore_errors': {'key': 'properties.ignoreErrors', 'type': 'bool'}, + 'init_timeout': {'key': 'properties.initTimeout', 'type': 'str'}, + 'secrets': {'key': 'properties.secrets', 'type': '[Secret]'}, + 'metadata': {'key': 'properties.metadata', 'type': '[DaprMetadata]'}, + 'scopes': {'key': 'properties.scopes', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(DaprComponent, self).__init__(**kwargs) + self.component_type = kwargs.get('component_type', None) + self.version = kwargs.get('version', None) + self.ignore_errors = kwargs.get('ignore_errors', None) + self.init_timeout = kwargs.get('init_timeout', None) + self.secrets = kwargs.get('secrets', None) + self.metadata = kwargs.get('metadata', None) + self.scopes = kwargs.get('scopes', None) + + +class DaprComponentsCollection(Model): + """Dapr Components ARM resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.DaprComponent] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[DaprComponent]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DaprComponentsCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class DaprMetadata(Model): + """Dapr component metadata. + + :param name: Metadata property name. + :type name: str + :param value: Metadata property value. + :type value: str + :param secret_ref: Name of the Dapr Component secret from which to pull + the metadata property value. + :type secret_ref: str + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'value': {'key': 'value', 'type': 'str'}, + 'secret_ref': {'key': 'secretRef', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DaprMetadata, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.value = kwargs.get('value', None) + self.secret_ref = kwargs.get('secret_ref', None) + + +class DefaultErrorResponse(Model): + """App Service error response. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar error: Error model. + :vartype error: ~commondefinitions.models.DefaultErrorResponseError + """ + + _validation = { + 'error': {'readonly': True}, + } + + _attribute_map = { + 'error': {'key': 'error', 'type': 'DefaultErrorResponseError'}, + } + + def __init__(self, **kwargs): + super(DefaultErrorResponse, self).__init__(**kwargs) + self.error = None + + +class DefaultErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'DefaultErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(DefaultErrorResponseException, self).__init__(deserialize, response, 'DefaultErrorResponse', *args) + + +class DefaultErrorResponseError(Model): + """Error model. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar code: Standardized string to programmatically identify the error. + :vartype code: str + :ivar message: Detailed error description and debugging information. + :vartype message: str + :ivar target: Detailed error description and debugging information. + :vartype target: str + :param details: Details or the error + :type details: + list[~commondefinitions.models.DefaultErrorResponseErrorDetailsItem] + :ivar innererror: More information to debug error. + :vartype innererror: str + """ + + _validation = { + 'code': {'readonly': True}, + 'message': {'readonly': True}, + 'target': {'readonly': True}, + 'innererror': {'readonly': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'target': {'key': 'target', 'type': 'str'}, + 'details': {'key': 'details', 'type': '[DefaultErrorResponseErrorDetailsItem]'}, + 'innererror': {'key': 'innererror', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DefaultErrorResponseError, self).__init__(**kwargs) + self.code = None + self.message = None + self.target = None + self.details = kwargs.get('details', None) + self.innererror = None + + +class DefaultErrorResponseErrorDetailsItem(Model): + """Detailed errors. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar code: Standardized string to programmatically identify the error. + :vartype code: str + :ivar message: Detailed error description and debugging information. + :vartype message: str + :ivar target: Detailed error description and debugging information. + :vartype target: str + """ + + _validation = { + 'code': {'readonly': True}, + 'message': {'readonly': True}, + 'target': {'readonly': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'target': {'key': 'target', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(DefaultErrorResponseErrorDetailsItem, self).__init__(**kwargs) + self.code = None + self.message = None + self.target = None + + +class EnvironmentVar(Model): + """Container App container environment variable. + + :param name: Environment variable name. + :type name: str + :param value: Non-secret environment variable value. + :type value: str + :param secret_ref: Name of the Container App secret from which to pull the + environment variable value. + :type secret_ref: str + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'value': {'key': 'value', 'type': 'str'}, + 'secret_ref': {'key': 'secretRef', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(EnvironmentVar, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.value = kwargs.get('value', None) + self.secret_ref = kwargs.get('secret_ref', None) + + +class Facebook(Model): + """The configuration settings of the Facebook provider. + + :param state: Disabled if the Facebook provider should not be + enabled despite the set registration; otherwise, Enabled. + Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the app registration + for the Facebook provider. + :type registration: ~commondefinitions.models.AppRegistration + :param graph_api_version: The version of the Facebook api to be used while + logging in. + :type graph_api_version: str + :param login: The configuration settings of the login flow. + :type login: ~commondefinitions.models.LoginScopes + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'AppRegistration'}, + 'graph_api_version': {'key': 'graphApiVersion', 'type': 'str'}, + 'login': {'key': 'login', 'type': 'LoginScopes'}, + } + + def __init__(self, **kwargs): + super(Facebook, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.graph_api_version = kwargs.get('graph_api_version', None) + self.login = kwargs.get('login', None) + + +class GitHub(Model): + """The configuration settings of the GitHub provider. + + :param state: Disabled if the GitHub provider should not be + enabled despite the set registration; otherwise, Enabled. + Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the app registration + for the GitHub provider. + :type registration: ~commondefinitions.models.ClientRegistration + :param login: The configuration settings of the login flow. + :type login: ~commondefinitions.models.LoginScopes + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'ClientRegistration'}, + 'login': {'key': 'login', 'type': 'LoginScopes'}, + } + + def __init__(self, **kwargs): + super(GitHub, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.login = kwargs.get('login', None) + + +class GithubActionConfiguration(Model): + """Configuration properties that define the mutable settings of a Container + App SourceControl. + + :param registry_info: Registry configurations. + :type registry_info: ~commondefinitions.models.RegistryInfo + :param azure_credentials: AzureCredentials configurations. + :type azure_credentials: ~commondefinitions.models.AzureCredentials + :param dockerfile_path: Docker file path + :type dockerfile_path: str + :param publish_type: Code or Image + :type publish_type: str + :param os: Operation system + :type os: str + :param runtime_stack: Runtime stack + :type runtime_stack: str + :param runtime_version: Runtime Version + :type runtime_version: str + """ + + _attribute_map = { + 'registry_info': {'key': 'registryInfo', 'type': 'RegistryInfo'}, + 'azure_credentials': {'key': 'azureCredentials', 'type': 'AzureCredentials'}, + 'dockerfile_path': {'key': 'dockerfilePath', 'type': 'str'}, + 'publish_type': {'key': 'publishType', 'type': 'str'}, + 'os': {'key': 'os', 'type': 'str'}, + 'runtime_stack': {'key': 'runtimeStack', 'type': 'str'}, + 'runtime_version': {'key': 'runtimeVersion', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(GithubActionConfiguration, self).__init__(**kwargs) + self.registry_info = kwargs.get('registry_info', None) + self.azure_credentials = kwargs.get('azure_credentials', None) + self.dockerfile_path = kwargs.get('dockerfile_path', None) + self.publish_type = kwargs.get('publish_type', None) + self.os = kwargs.get('os', None) + self.runtime_stack = kwargs.get('runtime_stack', None) + self.runtime_version = kwargs.get('runtime_version', None) + + +class GlobalValidation(Model): + """The configuration settings that determines the validation flow of users + using ContainerApp Authentication/Authorization. + + :param unauthenticated_client_action: The action to take when an + unauthenticated client attempts to access the app. Possible values + include: 'RedirectToLoginPage', 'AllowAnonymous', 'Return401', 'Return403' + :type unauthenticated_client_action: str or + ~commondefinitions.models.UnauthenticatedClientAction + :param redirect_to_provider: The default authentication provider to use + when multiple providers are configured. + This setting is only needed if multiple providers are configured and the + unauthenticated client + action is set to "RedirectToLoginPage". + :type redirect_to_provider: str + """ + + _attribute_map = { + 'unauthenticated_client_action': {'key': 'unauthenticatedClientAction', 'type': 'str'}, + 'redirect_to_provider': {'key': 'redirectToProvider', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(GlobalValidation, self).__init__(**kwargs) + self.unauthenticated_client_action = kwargs.get('unauthenticated_client_action', None) + self.redirect_to_provider = kwargs.get('redirect_to_provider', None) + + +class Google(Model): + """The configuration settings of the Google provider. + + :param state: Disabled if the Google provider should not be + enabled despite the set registration; otherwise, Enabled. + Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the app registration + for the Google provider. + :type registration: ~commondefinitions.models.ClientRegistration + :param login: The configuration settings of the login flow. + :type login: ~commondefinitions.models.LoginScopes + :param validation: The configuration settings of the Azure Active + Directory token validation flow. + :type validation: ~commondefinitions.models.AllowedAudiencesValidation + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'ClientRegistration'}, + 'login': {'key': 'login', 'type': 'LoginScopes'}, + 'validation': {'key': 'validation', 'type': 'AllowedAudiencesValidation'}, + } + + def __init__(self, **kwargs): + super(Google, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.login = kwargs.get('login', None) + self.validation = kwargs.get('validation', None) + + +class HttpScaleRule(Model): + """Container App container Custom scaling rule. + + :param metadata: Metadata properties to describe http scale rule. + :type metadata: dict[str, str] + :param auth: Authentication secrets for the custom scale rule. + :type auth: list[~commondefinitions.models.ScaleRuleAuth] + """ + + _attribute_map = { + 'metadata': {'key': 'metadata', 'type': '{str}'}, + 'auth': {'key': 'auth', 'type': '[ScaleRuleAuth]'}, + } + + def __init__(self, **kwargs): + super(HttpScaleRule, self).__init__(**kwargs) + self.metadata = kwargs.get('metadata', None) + self.auth = kwargs.get('auth', None) + + +class HttpSettings(Model): + """The configuration settings of the HTTP requests for authentication and + authorization requests made against ContainerApp + Authentication/Authorization. + + :param require_https: false if the + authentication/authorization responses not having the HTTPS scheme are + permissible; otherwise, true. Possible values include: + 'True', 'False' + :type require_https: str or ~commondefinitions.models.RequireHttpsMode + :param route: The configuration settings of the paths HTTP requests. + :type route: ~commondefinitions.models.HttpSettingsRoute + """ + + _attribute_map = { + 'require_https': {'key': 'requireHttps', 'type': 'str'}, + 'route': {'key': 'route', 'type': 'HttpSettingsRoute'}, + } + + def __init__(self, **kwargs): + super(HttpSettings, self).__init__(**kwargs) + self.require_https = kwargs.get('require_https', None) + self.route = kwargs.get('route', None) + + +class HttpSettingsRoute(Model): + """The configuration settings of the paths HTTP requests. + + :param api_prefix: The prefix that should precede all the + authentication/authorization paths. + :type api_prefix: str + """ + + _attribute_map = { + 'api_prefix': {'key': 'apiPrefix', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(HttpSettingsRoute, self).__init__(**kwargs) + self.api_prefix = kwargs.get('api_prefix', None) + + +class IdentityProviders(Model): + """The configuration settings of each of the identity providers used to + configure ContainerApp Authentication/Authorization. + + :param azure_active_directory: The configuration settings of the Azure + Active directory provider. + :type azure_active_directory: + ~commondefinitions.models.AzureActiveDirectory + :param facebook: The configuration settings of the Facebook provider. + :type facebook: ~commondefinitions.models.Facebook + :param git_hub: The configuration settings of the GitHub provider. + :type git_hub: ~commondefinitions.models.GitHub + :param google: The configuration settings of the Google provider. + :type google: ~commondefinitions.models.Google + :param legacy_microsoft_account: The configuration settings of the legacy + Microsoft Account provider. + :type legacy_microsoft_account: + ~commondefinitions.models.LegacyMicrosoftAccount + :param twitter: The configuration settings of the Twitter provider. + :type twitter: ~commondefinitions.models.Twitter + :param apple: The configuration settings of the Apple provider. + :type apple: ~commondefinitions.models.Apple + :param azure_static_web_app: The configuration settings of the Azure + Static Web Apps provider. + :type azure_static_web_app: ~commondefinitions.models.AzureStaticWebApp + :param custom_open_id_connect_providers: The map of the name of the alias + of each custom Open ID Connect provider to the + configuration settings of the custom Open ID Connect provider. + :type custom_open_id_connect_providers: dict[str, + ~commondefinitions.models.CustomOpenIdConnectProvider] + """ + + _attribute_map = { + 'azure_active_directory': {'key': 'azureActiveDirectory', 'type': 'AzureActiveDirectory'}, + 'facebook': {'key': 'facebook', 'type': 'Facebook'}, + 'git_hub': {'key': 'gitHub', 'type': 'GitHub'}, + 'google': {'key': 'google', 'type': 'Google'}, + 'legacy_microsoft_account': {'key': 'legacyMicrosoftAccount', 'type': 'LegacyMicrosoftAccount'}, + 'twitter': {'key': 'twitter', 'type': 'Twitter'}, + 'apple': {'key': 'apple', 'type': 'Apple'}, + 'azure_static_web_app': {'key': 'azureStaticWebApp', 'type': 'AzureStaticWebApp'}, + 'custom_open_id_connect_providers': {'key': 'customOpenIdConnectProviders', 'type': '{CustomOpenIdConnectProvider}'}, + } + + def __init__(self, **kwargs): + super(IdentityProviders, self).__init__(**kwargs) + self.azure_active_directory = kwargs.get('azure_active_directory', None) + self.facebook = kwargs.get('facebook', None) + self.git_hub = kwargs.get('git_hub', None) + self.google = kwargs.get('google', None) + self.legacy_microsoft_account = kwargs.get('legacy_microsoft_account', None) + self.twitter = kwargs.get('twitter', None) + self.apple = kwargs.get('apple', None) + self.azure_static_web_app = kwargs.get('azure_static_web_app', None) + self.custom_open_id_connect_providers = kwargs.get('custom_open_id_connect_providers', None) + + +class Ingress(Model): + """Container App Ingress configuration. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar fqdn: Hostname. + :vartype fqdn: str + :param external: Bool indicating if app exposes an external http endpoint. + Default value: False . + :type external: bool + :param target_port: Target Port in containers for traffic from ingress + :type target_port: int + :param transport: Ingress transport protocol. Possible values include: + 'auto', 'http', 'http2' + :type transport: str or ~commondefinitions.models.IngressTransportMethod + :param traffic: Traffic weights for app's revisions + :type traffic: list[~commondefinitions.models.TrafficWeight] + :param custom_domains: custom domain bindings for Container Apps' + hostnames. + :type custom_domains: list[~commondefinitions.models.CustomDomain] + :param allow_insecure: Bool indicating if HTTP connections to is allowed. + If set to false HTTP connections are automatically redirected to HTTPS + connections + :type allow_insecure: bool + """ + + _validation = { + 'fqdn': {'readonly': True}, + } + + _attribute_map = { + 'fqdn': {'key': 'fqdn', 'type': 'str'}, + 'external': {'key': 'external', 'type': 'bool'}, + 'target_port': {'key': 'targetPort', 'type': 'int'}, + 'transport': {'key': 'transport', 'type': 'str'}, + 'traffic': {'key': 'traffic', 'type': '[TrafficWeight]'}, + 'custom_domains': {'key': 'customDomains', 'type': '[CustomDomain]'}, + 'allow_insecure': {'key': 'allowInsecure', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(Ingress, self).__init__(**kwargs) + self.fqdn = None + self.external = kwargs.get('external', False) + self.target_port = kwargs.get('target_port', None) + self.transport = kwargs.get('transport', None) + self.traffic = kwargs.get('traffic', None) + self.custom_domains = kwargs.get('custom_domains', None) + self.allow_insecure = kwargs.get('allow_insecure', None) + + +class LegacyMicrosoftAccount(Model): + """The configuration settings of the legacy Microsoft Account provider. + + :param state: Disabled if the legacy Microsoft Account + provider should not be enabled despite the set registration; otherwise, + Enabled. Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the app registration + for the legacy Microsoft Account provider. + :type registration: ~commondefinitions.models.ClientRegistration + :param login: The configuration settings of the login flow. + :type login: ~commondefinitions.models.LoginScopes + :param validation: The configuration settings of the legacy Microsoft + Account provider token validation flow. + :type validation: ~commondefinitions.models.AllowedAudiencesValidation + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'ClientRegistration'}, + 'login': {'key': 'login', 'type': 'LoginScopes'}, + 'validation': {'key': 'validation', 'type': 'AllowedAudiencesValidation'}, + } + + def __init__(self, **kwargs): + super(LegacyMicrosoftAccount, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + self.login = kwargs.get('login', None) + self.validation = kwargs.get('validation', None) + + +class LogAnalyticsConfiguration(Model): + """Log analytics configuration. + + :param customer_id: Log analytics customer id + :type customer_id: str + :param shared_key: Log analytics customer key + :type shared_key: str + """ + + _attribute_map = { + 'customer_id': {'key': 'customerId', 'type': 'str'}, + 'shared_key': {'key': 'sharedKey', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(LogAnalyticsConfiguration, self).__init__(**kwargs) + self.customer_id = kwargs.get('customer_id', None) + self.shared_key = kwargs.get('shared_key', None) + + +class Login(Model): + """The configuration settings of the login flow of users using ContainerApp + Authentication/Authorization. + + :param route: The route that specify the endpoint used for login and + logout requests. + :type route: ~commondefinitions.models.LoginRoute + :param preserve_url_fragments_for_logins: True if the + fragments from the request are preserved after the login request is made; + otherwise, False. Possible values include: 'True', 'False' + :type preserve_url_fragments_for_logins: str or + ~commondefinitions.models.PreserveUrlFragmentsForLoginsMode + :param allowed_external_redirect_urls: External URLs that can be + redirected to as part of logging in or logging out of the app. Note that + the query string part of the URL is ignored. + This is an advanced setting typically only needed by Windows Store + application backends. + Note that URLs within the current domain are always implicitly allowed. + :type allowed_external_redirect_urls: list[str] + """ + + _attribute_map = { + 'route': {'key': 'route', 'type': 'LoginRoute'}, + 'preserve_url_fragments_for_logins': {'key': 'preserveUrlFragmentsForLogins', 'type': 'str'}, + 'allowed_external_redirect_urls': {'key': 'allowedExternalRedirectUrls', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(Login, self).__init__(**kwargs) + self.route = kwargs.get('route', None) + self.preserve_url_fragments_for_logins = kwargs.get('preserve_url_fragments_for_logins', None) + self.allowed_external_redirect_urls = kwargs.get('allowed_external_redirect_urls', None) + + +class LoginRoute(Model): + """The route that specify the endpoint used for login and logout requests. + + :param logout_endpoint: The endpoint at which a logout request should be + made. + :type logout_endpoint: str + """ + + _attribute_map = { + 'logout_endpoint': {'key': 'logoutEndpoint', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(LoginRoute, self).__init__(**kwargs) + self.logout_endpoint = kwargs.get('logout_endpoint', None) + + +class LoginScopes(Model): + """The configuration settings of the login flow, including the scopes that + should be requested. + + :param scopes: A list of the scopes that should be requested while + authenticating. + :type scopes: list[str] + """ + + _attribute_map = { + 'scopes': {'key': 'scopes', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(LoginScopes, self).__init__(**kwargs) + self.scopes = kwargs.get('scopes', None) + + +class ManagedEnvironment(TrackedResource): + """An environment for hosting container apps. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param tags: Resource tags. + :type tags: dict[str, str] + :param location: Required. The geo-location where the resource lives + :type location: str + :ivar provisioning_state: Provisioning state of the Environment. Possible + values include: 'Succeeded', 'Failed', 'Canceled', 'Waiting', + 'InitializationInProgress', 'InfrastructureSetupInProgress', + 'InfrastructureSetupComplete', 'ScheduledForDelete', 'UpgradeRequested', + 'UpgradeFailed' + :vartype provisioning_state: str or + ~commondefinitions.models.EnvironmentProvisioningState + :param dapr_ai_instrumentation_key: Azure Monitor instrumentation key used + by Dapr to export Service to Service communication telemetry + :type dapr_ai_instrumentation_key: str + :param vnet_configuration: Vnet configuration for the environment + :type vnet_configuration: ~commondefinitions.models.VnetConfiguration + :ivar deployment_errors: Any errors that occurred during deployment or + deployment validation + :vartype deployment_errors: str + :ivar default_domain: Default Domain Name for the cluster + :vartype default_domain: str + :ivar static_ip: Static IP of the Environment + :vartype static_ip: str + :param app_logs_configuration: Cluster configuration which enables the log + daemon to export + app logs to a destination. Currently only "log-analytics" is + supported + :type app_logs_configuration: + ~commondefinitions.models.AppLogsConfiguration + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'location': {'required': True}, + 'provisioning_state': {'readonly': True}, + 'deployment_errors': {'readonly': True}, + 'default_domain': {'readonly': True}, + 'static_ip': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'tags': {'key': 'tags', 'type': '{str}'}, + 'location': {'key': 'location', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'dapr_ai_instrumentation_key': {'key': 'properties.daprAIInstrumentationKey', 'type': 'str'}, + 'vnet_configuration': {'key': 'properties.vnetConfiguration', 'type': 'VnetConfiguration'}, + 'deployment_errors': {'key': 'properties.deploymentErrors', 'type': 'str'}, + 'default_domain': {'key': 'properties.defaultDomain', 'type': 'str'}, + 'static_ip': {'key': 'properties.staticIp', 'type': 'str'}, + 'app_logs_configuration': {'key': 'properties.appLogsConfiguration', 'type': 'AppLogsConfiguration'}, + } + + def __init__(self, **kwargs): + super(ManagedEnvironment, self).__init__(**kwargs) + self.provisioning_state = None + self.dapr_ai_instrumentation_key = kwargs.get('dapr_ai_instrumentation_key', None) + self.vnet_configuration = kwargs.get('vnet_configuration', None) + self.deployment_errors = None + self.default_domain = None + self.static_ip = None + self.app_logs_configuration = kwargs.get('app_logs_configuration', None) + + +class ManagedEnvironmentPatch(Model): + """An environment for hosting container apps. + + :param tags: Application-specific metadata in the form of key-value pairs. + :type tags: dict[str, str] + """ + + _attribute_map = { + 'tags': {'key': 'tags', 'type': '{str}'}, + } + + def __init__(self, **kwargs): + super(ManagedEnvironmentPatch, self).__init__(**kwargs) + self.tags = kwargs.get('tags', None) + + +class ManagedEnvironmentsCollection(Model): + """Collection of Environments. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.ManagedEnvironment] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ManagedEnvironment]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ManagedEnvironmentsCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class ManagedEnvironmentStorage(ProxyResource): + """Storage resource for managedEnvironment. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :param properties: Storage properties + :type properties: + ~commondefinitions.models.ManagedEnvironmentStorageProperties + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'properties': {'key': 'properties', 'type': 'ManagedEnvironmentStorageProperties'}, + } + + def __init__(self, **kwargs): + super(ManagedEnvironmentStorage, self).__init__(**kwargs) + self.properties = kwargs.get('properties', None) + + +class ManagedEnvironmentStorageProperties(Model): + """Storage properties. + + :param azure_file: Azure file properties + :type azure_file: ~commondefinitions.models.AzureFileProperties + """ + + _attribute_map = { + 'azure_file': {'key': 'azureFile', 'type': 'AzureFileProperties'}, + } + + def __init__(self, **kwargs): + super(ManagedEnvironmentStorageProperties, self).__init__(**kwargs) + self.azure_file = kwargs.get('azure_file', None) + + +class ManagedEnvironmentStoragesCollection(Model): + """Collection of Storage for Environments. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of storage resources. + :type value: list[~commondefinitions.models.ManagedEnvironmentStorage] + """ + + _validation = { + 'value': {'required': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ManagedEnvironmentStorage]'}, + } + + def __init__(self, **kwargs): + super(ManagedEnvironmentStoragesCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + + +class ManagedServiceIdentity(Model): + """Managed service identity (system assigned and/or user assigned identities). + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :ivar principal_id: The service principal ID of the system assigned + identity. This property will only be provided for a system assigned + identity. + :vartype principal_id: str + :ivar tenant_id: The tenant ID of the system assigned identity. This + property will only be provided for a system assigned identity. + :vartype tenant_id: str + :param type: Required. Possible values include: 'None', 'SystemAssigned', + 'UserAssigned', 'SystemAssigned,UserAssigned' + :type type: str or ~commondefinitions.models.ManagedServiceIdentityType + :param user_assigned_identities: + :type user_assigned_identities: dict[str, + ~commondefinitions.models.UserAssignedIdentity] + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + 'type': {'required': True}, + } + + _attribute_map = { + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'user_assigned_identities': {'key': 'userAssignedIdentities', 'type': '{UserAssignedIdentity}'}, + } + + def __init__(self, **kwargs): + super(ManagedServiceIdentity, self).__init__(**kwargs) + self.principal_id = None + self.tenant_id = None + self.type = kwargs.get('type', None) + self.user_assigned_identities = kwargs.get('user_assigned_identities', None) + + +class OpenIdConnectClientCredential(Model): + """The authentication client credentials of the custom Open ID Connect + provider. + + :param client_secret_ref_name: The app setting that contains the client + secret for the custom Open ID Connect provider. + :type client_secret_ref_name: str + """ + + _attribute_map = { + 'client_secret_ref_name': {'key': 'clientSecretRefName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(OpenIdConnectClientCredential, self).__init__(**kwargs) + self.client_secret_ref_name = kwargs.get('client_secret_ref_name', None) + + +class OpenIdConnectConfig(Model): + """The configuration settings of the endpoints used for the custom Open ID + Connect provider. + + :param authorization_endpoint: The endpoint to be used to make an + authorization request. + :type authorization_endpoint: str + :param token_endpoint: The endpoint to be used to request a token. + :type token_endpoint: str + :param issuer: The endpoint that issues the token. + :type issuer: str + :param certification_uri: The endpoint that provides the keys necessary to + validate the token. + :type certification_uri: str + :param well_known_open_id_configuration: The endpoint that contains all + the configuration endpoints for the provider. + :type well_known_open_id_configuration: str + """ + + _attribute_map = { + 'authorization_endpoint': {'key': 'authorizationEndpoint', 'type': 'str'}, + 'token_endpoint': {'key': 'tokenEndpoint', 'type': 'str'}, + 'issuer': {'key': 'issuer', 'type': 'str'}, + 'certification_uri': {'key': 'certificationUri', 'type': 'str'}, + 'well_known_open_id_configuration': {'key': 'wellKnownOpenIdConfiguration', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(OpenIdConnectConfig, self).__init__(**kwargs) + self.authorization_endpoint = kwargs.get('authorization_endpoint', None) + self.token_endpoint = kwargs.get('token_endpoint', None) + self.issuer = kwargs.get('issuer', None) + self.certification_uri = kwargs.get('certification_uri', None) + self.well_known_open_id_configuration = kwargs.get('well_known_open_id_configuration', None) + + +class OpenIdConnectLogin(Model): + """The configuration settings of the login flow of the custom Open ID Connect + provider. + + :param name_claim_type: The name of the claim that contains the users + name. + :type name_claim_type: str + :param scopes: A list of the scopes that should be requested while + authenticating. + :type scopes: list[str] + """ + + _attribute_map = { + 'name_claim_type': {'key': 'nameClaimType', 'type': 'str'}, + 'scopes': {'key': 'scopes', 'type': '[str]'}, + } + + def __init__(self, **kwargs): + super(OpenIdConnectLogin, self).__init__(**kwargs) + self.name_claim_type = kwargs.get('name_claim_type', None) + self.scopes = kwargs.get('scopes', None) + + +class OpenIdConnectRegistration(Model): + """The configuration settings of the app registration for the custom Open ID + Connect provider. + + :param client_id: The client id of the custom Open ID Connect provider. + :type client_id: str + :param client_credential: The authentication credentials of the custom + Open ID Connect provider. + :type client_credential: + ~commondefinitions.models.OpenIdConnectClientCredential + :param open_id_connect_configuration: The configuration settings of the + endpoints used for the custom Open ID Connect provider. + :type open_id_connect_configuration: + ~commondefinitions.models.OpenIdConnectConfig + """ + + _attribute_map = { + 'client_id': {'key': 'clientId', 'type': 'str'}, + 'client_credential': {'key': 'clientCredential', 'type': 'OpenIdConnectClientCredential'}, + 'open_id_connect_configuration': {'key': 'openIdConnectConfiguration', 'type': 'OpenIdConnectConfig'}, + } + + def __init__(self, **kwargs): + super(OpenIdConnectRegistration, self).__init__(**kwargs) + self.client_id = kwargs.get('client_id', None) + self.client_credential = kwargs.get('client_credential', None) + self.open_id_connect_configuration = kwargs.get('open_id_connect_configuration', None) + + +class OperationDetail(Model): + """Operation detail payload. + + :param name: Name of the operation + :type name: str + :param is_data_action: Indicates whether the operation is a data action + :type is_data_action: bool + :param display: Display of the operation + :type display: ~commondefinitions.models.OperationDisplay + :param origin: Origin of the operation + :type origin: str + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, + 'display': {'key': 'display', 'type': 'OperationDisplay'}, + 'origin': {'key': 'origin', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(OperationDetail, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.is_data_action = kwargs.get('is_data_action', None) + self.display = kwargs.get('display', None) + self.origin = kwargs.get('origin', None) + + +class OperationDisplay(Model): + """Operation display payload. + + :param provider: Resource provider of the operation + :type provider: str + :param resource: Resource of the operation + :type resource: str + :param operation: Localized friendly name for the operation + :type operation: str + :param description: Localized friendly description for the operation + :type description: str + """ + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(OperationDisplay, self).__init__(**kwargs) + self.provider = kwargs.get('provider', None) + self.resource = kwargs.get('resource', None) + self.operation = kwargs.get('operation', None) + self.description = kwargs.get('description', None) + + +class QueueScaleRule(Model): + """Container App container Azure Queue based scaling rule. + + :param queue_name: Queue name. + :type queue_name: str + :param queue_length: Queue length. + :type queue_length: int + :param auth: Authentication secrets for the queue scale rule. + :type auth: list[~commondefinitions.models.ScaleRuleAuth] + """ + + _attribute_map = { + 'queue_name': {'key': 'queueName', 'type': 'str'}, + 'queue_length': {'key': 'queueLength', 'type': 'int'}, + 'auth': {'key': 'auth', 'type': '[ScaleRuleAuth]'}, + } + + def __init__(self, **kwargs): + super(QueueScaleRule, self).__init__(**kwargs) + self.queue_name = kwargs.get('queue_name', None) + self.queue_length = kwargs.get('queue_length', None) + self.auth = kwargs.get('auth', None) + + +class RegistryCredentials(Model): + """Container App Private Registry. + + :param server: Container Registry Server + :type server: str + :param username: Container Registry Username + :type username: str + :param password_secret_ref: The name of the Secret that contains the + registry login password + :type password_secret_ref: str + """ + + _attribute_map = { + 'server': {'key': 'server', 'type': 'str'}, + 'username': {'key': 'username', 'type': 'str'}, + 'password_secret_ref': {'key': 'passwordSecretRef', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(RegistryCredentials, self).__init__(**kwargs) + self.server = kwargs.get('server', None) + self.username = kwargs.get('username', None) + self.password_secret_ref = kwargs.get('password_secret_ref', None) + + +class RegistryInfo(Model): + """Container App registry information. + + :param registry_url: registry server Url. + :type registry_url: str + :param registry_user_name: registry username. + :type registry_user_name: str + :param registry_password: registry secret. + :type registry_password: str + """ + + _attribute_map = { + 'registry_url': {'key': 'registryUrl', 'type': 'str'}, + 'registry_user_name': {'key': 'registryUserName', 'type': 'str'}, + 'registry_password': {'key': 'registryPassword', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(RegistryInfo, self).__init__(**kwargs) + self.registry_url = kwargs.get('registry_url', None) + self.registry_user_name = kwargs.get('registry_user_name', None) + self.registry_password = kwargs.get('registry_password', None) + + +class Replica(ProxyResource): + """Container App Revision Replica. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :ivar created_time: Timestamp describing when the pod was created by + controller + :vartype created_time: datetime + :param containers: The containers collection under a replica. + :type containers: list[~commondefinitions.models.ReplicaContainer] + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'created_time': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + 'containers': {'key': 'properties.containers', 'type': '[ReplicaContainer]'}, + } + + def __init__(self, **kwargs): + super(Replica, self).__init__(**kwargs) + self.created_time = None + self.containers = kwargs.get('containers', None) + + +class ReplicaCollection(Model): + """Container App Revision Replicas collection ARM resource. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.Replica] + """ + + _validation = { + 'value': {'required': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[Replica]'}, + } + + def __init__(self, **kwargs): + super(ReplicaCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + + +class ReplicaContainer(Model): + """Container object under Container App Revision Replica. + + :param name: The Name of the Container + :type name: str + :param container_id: The Id of the Container + :type container_id: str + :param ready: The container ready status + :type ready: bool + :param started: The container start status + :type started: bool + :param restart_count: The container restart count + :type restart_count: int + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'container_id': {'key': 'containerId', 'type': 'str'}, + 'ready': {'key': 'ready', 'type': 'bool'}, + 'started': {'key': 'started', 'type': 'bool'}, + 'restart_count': {'key': 'restartCount', 'type': 'int'}, + } + + def __init__(self, **kwargs): + super(ReplicaContainer, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.container_id = kwargs.get('container_id', None) + self.ready = kwargs.get('ready', None) + self.started = kwargs.get('started', None) + self.restart_count = kwargs.get('restart_count', None) + + +class Revision(ProxyResource): + """Container App Revision. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :ivar created_time: Timestamp describing when the revision was created + by controller + :vartype created_time: datetime + :ivar fqdn: Fully qualified domain name of the revision + :vartype fqdn: str + :ivar template: Container App Revision Template with all possible settings + and the + defaults if user did not provide them. The defaults are populated + as they were at the creation time + :vartype template: ~commondefinitions.models.Template + :ivar active: Boolean describing if the Revision is Active + :vartype active: bool + :ivar replicas: Number of pods currently running for this revision + :vartype replicas: int + :ivar traffic_weight: Traffic weight assigned to this revision + :vartype traffic_weight: int + :ivar provisioning_error: Optional Field - Platform Error Message + :vartype provisioning_error: str + :ivar health_state: Current health State of the revision. Possible values + include: 'Healthy', 'Unhealthy', 'None' + :vartype health_state: str or + ~commondefinitions.models.RevisionHealthState + :ivar provisioning_state: Current provisioning State of the revision. + Possible values include: 'Provisioning', 'Provisioned', 'Failed', + 'Deprovisioning', 'Deprovisioned' + :vartype provisioning_state: str or + ~commondefinitions.models.RevisionProvisioningState + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'created_time': {'readonly': True}, + 'fqdn': {'readonly': True}, + 'template': {'readonly': True}, + 'active': {'readonly': True}, + 'replicas': {'readonly': True}, + 'traffic_weight': {'readonly': True}, + 'provisioning_error': {'readonly': True}, + 'health_state': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'created_time': {'key': 'properties.createdTime', 'type': 'iso-8601'}, + 'fqdn': {'key': 'properties.fqdn', 'type': 'str'}, + 'template': {'key': 'properties.template', 'type': 'Template'}, + 'active': {'key': 'properties.active', 'type': 'bool'}, + 'replicas': {'key': 'properties.replicas', 'type': 'int'}, + 'traffic_weight': {'key': 'properties.trafficWeight', 'type': 'int'}, + 'provisioning_error': {'key': 'properties.provisioningError', 'type': 'str'}, + 'health_state': {'key': 'properties.healthState', 'type': 'str'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Revision, self).__init__(**kwargs) + self.created_time = None + self.fqdn = None + self.template = None + self.active = None + self.replicas = None + self.traffic_weight = None + self.provisioning_error = None + self.health_state = None + self.provisioning_state = None + + +class RevisionCollection(Model): + """Container App Revisions collection ARM resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.Revision] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[Revision]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(RevisionCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class Scale(Model): + """Container App scaling configurations. + + :param min_replicas: Optional. Minimum number of container replicas. + :type min_replicas: int + :param max_replicas: Optional. Maximum number of container replicas. + Defaults to 10 if not set. + :type max_replicas: int + :param rules: Scaling rules. + :type rules: list[~commondefinitions.models.ScaleRule] + """ + + _attribute_map = { + 'min_replicas': {'key': 'minReplicas', 'type': 'int'}, + 'max_replicas': {'key': 'maxReplicas', 'type': 'int'}, + 'rules': {'key': 'rules', 'type': '[ScaleRule]'}, + } + + def __init__(self, **kwargs): + super(Scale, self).__init__(**kwargs) + self.min_replicas = kwargs.get('min_replicas', None) + self.max_replicas = kwargs.get('max_replicas', None) + self.rules = kwargs.get('rules', None) + + +class ScaleRule(Model): + """Container App container scaling rule. + + :param name: Scale Rule Name + :type name: str + :param azure_queue: Azure Queue based scaling. + :type azure_queue: ~commondefinitions.models.QueueScaleRule + :param custom: Custom scale rule. + :type custom: ~commondefinitions.models.CustomScaleRule + :param http: HTTP requests based scaling. + :type http: ~commondefinitions.models.HttpScaleRule + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'azure_queue': {'key': 'azureQueue', 'type': 'QueueScaleRule'}, + 'custom': {'key': 'custom', 'type': 'CustomScaleRule'}, + 'http': {'key': 'http', 'type': 'HttpScaleRule'}, + } + + def __init__(self, **kwargs): + super(ScaleRule, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.azure_queue = kwargs.get('azure_queue', None) + self.custom = kwargs.get('custom', None) + self.http = kwargs.get('http', None) + + +class ScaleRuleAuth(Model): + """Auth Secrets for Container App Scale Rule. + + :param secret_ref: Name of the Container App secret from which to pull the + auth params. + :type secret_ref: str + :param trigger_parameter: Trigger Parameter that uses the secret + :type trigger_parameter: str + """ + + _attribute_map = { + 'secret_ref': {'key': 'secretRef', 'type': 'str'}, + 'trigger_parameter': {'key': 'triggerParameter', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ScaleRuleAuth, self).__init__(**kwargs) + self.secret_ref = kwargs.get('secret_ref', None) + self.trigger_parameter = kwargs.get('trigger_parameter', None) + + +class Secret(Model): + """Secret definition. + + :param name: Secret Name. + :type name: str + :param value: Secret Value. + :type value: str + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'value': {'key': 'value', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Secret, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.value = kwargs.get('value', None) + + +class SecretsCollection(Model): + """Container App Secrets Collection ARM resource. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.ContainerAppSecret] + """ + + _validation = { + 'value': {'required': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ContainerAppSecret]'}, + } + + def __init__(self, **kwargs): + super(SecretsCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + + +class SourceControl(ProxyResource): + """Container App SourceControl. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Fully qualified resource ID for the resource. Ex - + /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName} + :vartype id: str + :ivar name: The name of the resource + :vartype name: str + :ivar type: The type of the resource. E.g. + "Microsoft.Compute/virtualMachines" or "Microsoft.Storage/storageAccounts" + :vartype type: str + :ivar system_data: Azure Resource Manager metadata containing createdBy + and modifiedBy information. + :vartype system_data: ~commondefinitions.models.SystemData + :ivar operation_state: Current provisioning State of the operation. + Possible values include: 'InProgress', 'Succeeded', 'Failed', 'Canceled' + :vartype operation_state: str or + ~commondefinitions.models.SourceControlOperationState + :param repo_url: The repo url which will be integrated to ContainerApp. + :type repo_url: str + :param branch: The branch which will trigger the auto deployment + :type branch: str + :param github_action_configuration: Container App Revision Template with + all possible settings and the + defaults if user did not provide them. The defaults are populated + as they were at the creation time + :type github_action_configuration: + ~commondefinitions.models.GithubActionConfiguration + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'system_data': {'readonly': True}, + 'operation_state': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'operation_state': {'key': 'properties.operationState', 'type': 'str'}, + 'repo_url': {'key': 'properties.repoUrl', 'type': 'str'}, + 'branch': {'key': 'properties.branch', 'type': 'str'}, + 'github_action_configuration': {'key': 'properties.githubActionConfiguration', 'type': 'GithubActionConfiguration'}, + } + + def __init__(self, **kwargs): + super(SourceControl, self).__init__(**kwargs) + self.operation_state = None + self.repo_url = kwargs.get('repo_url', None) + self.branch = kwargs.get('branch', None) + self.github_action_configuration = kwargs.get('github_action_configuration', None) + + +class SourceControlCollection(Model): + """SourceControl collection ARM resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + All required parameters must be populated in order to send to Azure. + + :param value: Required. Collection of resources. + :type value: list[~commondefinitions.models.SourceControl] + :ivar next_link: Link to next page of resources. + :vartype next_link: str + """ + + _validation = { + 'value': {'required': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[SourceControl]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(SourceControlCollection, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class SystemData(Model): + """Metadata pertaining to creation and last modification of the resource. + + :param created_by: The identity that created the resource. + :type created_by: str + :param created_by_type: The type of identity that created the resource. + Possible values include: 'User', 'Application', 'ManagedIdentity', 'Key' + :type created_by_type: str or ~commondefinitions.models.CreatedByType + :param created_at: The timestamp of resource creation (UTC). + :type created_at: datetime + :param last_modified_by: The identity that last modified the resource. + :type last_modified_by: str + :param last_modified_by_type: The type of identity that last modified the + resource. Possible values include: 'User', 'Application', + 'ManagedIdentity', 'Key' + :type last_modified_by_type: str or + ~commondefinitions.models.CreatedByType + :param last_modified_at: The timestamp of resource last modification (UTC) + :type last_modified_at: datetime + """ + + _attribute_map = { + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_by_type': {'key': 'createdByType', 'type': 'str'}, + 'created_at': {'key': 'createdAt', 'type': 'iso-8601'}, + 'last_modified_by': {'key': 'lastModifiedBy', 'type': 'str'}, + 'last_modified_by_type': {'key': 'lastModifiedByType', 'type': 'str'}, + 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs): + super(SystemData, self).__init__(**kwargs) + self.created_by = kwargs.get('created_by', None) + self.created_by_type = kwargs.get('created_by_type', None) + self.created_at = kwargs.get('created_at', None) + self.last_modified_by = kwargs.get('last_modified_by', None) + self.last_modified_by_type = kwargs.get('last_modified_by_type', None) + self.last_modified_at = kwargs.get('last_modified_at', None) + + +class Template(Model): + """Container App versioned application definition. + Defines the desired state of an immutable revision. + Any changes to this section Will result in a new revision being created. + + :param revision_suffix: User friendly suffix that is appended to the + revision name + :type revision_suffix: str + :param containers: List of container definitions for the Container App. + :type containers: list[~commondefinitions.models.Container] + :param scale: Scaling properties for the Container App. + :type scale: ~commondefinitions.models.Scale + :param volumes: List of volume definitions for the Container App. + :type volumes: list[~commondefinitions.models.Volume] + """ + + _attribute_map = { + 'revision_suffix': {'key': 'revisionSuffix', 'type': 'str'}, + 'containers': {'key': 'containers', 'type': '[Container]'}, + 'scale': {'key': 'scale', 'type': 'Scale'}, + 'volumes': {'key': 'volumes', 'type': '[Volume]'}, + } + + def __init__(self, **kwargs): + super(Template, self).__init__(**kwargs) + self.revision_suffix = kwargs.get('revision_suffix', None) + self.containers = kwargs.get('containers', None) + self.scale = kwargs.get('scale', None) + self.volumes = kwargs.get('volumes', None) + + +class TrafficWeight(Model): + """Traffic weight assigned to a revision. + + :param revision_name: Name of a revision + :type revision_name: str + :param weight: Traffic weight assigned to a revision + :type weight: int + :param latest_revision: Indicates that the traffic weight belongs to a + latest stable revision. Default value: False . + :type latest_revision: bool + """ + + _attribute_map = { + 'revision_name': {'key': 'revisionName', 'type': 'str'}, + 'weight': {'key': 'weight', 'type': 'int'}, + 'latest_revision': {'key': 'latestRevision', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(TrafficWeight, self).__init__(**kwargs) + self.revision_name = kwargs.get('revision_name', None) + self.weight = kwargs.get('weight', None) + self.latest_revision = kwargs.get('latest_revision', False) + + +class Twitter(Model): + """The configuration settings of the Twitter provider. + + :param state: Disabled if the Twitter provider should not be + enabled despite the set registration; otherwise, Enabled. + Possible values include: 'Enabled', 'Disabled' + :type state: str or ~commondefinitions.models.IdentityProviderState + :param registration: The configuration settings of the app registration + for the Twitter provider. + :type registration: ~commondefinitions.models.TwitterRegistration + """ + + _attribute_map = { + 'state': {'key': 'state', 'type': 'str'}, + 'registration': {'key': 'registration', 'type': 'TwitterRegistration'}, + } + + def __init__(self, **kwargs): + super(Twitter, self).__init__(**kwargs) + self.state = kwargs.get('state', None) + self.registration = kwargs.get('registration', None) + + +class TwitterRegistration(Model): + """The configuration settings of the app registration for the Twitter + provider. + + :param consumer_key: The OAuth 1.0a consumer key of the Twitter + application used for sign-in. + This setting is required for enabling Twitter Sign-In. + Twitter Sign-In documentation: https://dev.twitter.com/web/sign-in + :type consumer_key: str + :param consumer_secret_ref_name: The app secret ref name that contains the + OAuth 1.0a consumer secret of the Twitter + application used for sign-in. + :type consumer_secret_ref_name: str + """ + + _attribute_map = { + 'consumer_key': {'key': 'consumerKey', 'type': 'str'}, + 'consumer_secret_ref_name': {'key': 'consumerSecretRefName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(TwitterRegistration, self).__init__(**kwargs) + self.consumer_key = kwargs.get('consumer_key', None) + self.consumer_secret_ref_name = kwargs.get('consumer_secret_ref_name', None) + + +class UserAssignedIdentity(Model): + """User assigned identity properties. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar principal_id: The principal ID of the assigned identity. + :vartype principal_id: str + :ivar client_id: The client ID of the assigned identity. + :vartype client_id: str + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'client_id': {'readonly': True}, + } + + _attribute_map = { + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'client_id': {'key': 'clientId', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(UserAssignedIdentity, self).__init__(**kwargs) + self.principal_id = None + self.client_id = None + + +class VnetConfiguration(Model): + """Configuration properties for apps environment to join a Virtual Network. + + :param internal: Boolean indicating the environment only has an internal + load balancer. These environments do not have a public static IP resource, + must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if + enabling this property + :type internal: bool + :param infrastructure_subnet_id: Resource ID of a subnet for + infrastructure components. This subnet must be in the same VNET as the + subnet defined in runtimeSubnetId. Must not overlap with any other + provided IP ranges. + :type infrastructure_subnet_id: str + :param runtime_subnet_id: Resource ID of a subnet that Container App + containers are injected into. This subnet must be in the same VNET as the + subnet defined in infrastructureSubnetId. Must not overlap with any other + provided IP ranges. + :type runtime_subnet_id: str + :param docker_bridge_cidr: CIDR notation IP range assigned to the Docker + bridge, network. Must not overlap with any other provided IP ranges. + :type docker_bridge_cidr: str + :param platform_reserved_cidr: IP range in CIDR notation that can be + reserved for environment infrastructure IP addresses. Must not overlap + with any other provided IP ranges. + :type platform_reserved_cidr: str + :param platform_reserved_dns_ip: An IP address from the IP range defined + by platformReservedCidr that will be reserved for the internal DNS server. + :type platform_reserved_dns_ip: str + """ + + _attribute_map = { + 'internal': {'key': 'internal', 'type': 'bool'}, + 'infrastructure_subnet_id': {'key': 'infrastructureSubnetId', 'type': 'str'}, + 'runtime_subnet_id': {'key': 'runtimeSubnetId', 'type': 'str'}, + 'docker_bridge_cidr': {'key': 'dockerBridgeCidr', 'type': 'str'}, + 'platform_reserved_cidr': {'key': 'platformReservedCidr', 'type': 'str'}, + 'platform_reserved_dns_ip': {'key': 'platformReservedDnsIP', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(VnetConfiguration, self).__init__(**kwargs) + self.internal = kwargs.get('internal', None) + self.infrastructure_subnet_id = kwargs.get('infrastructure_subnet_id', None) + self.runtime_subnet_id = kwargs.get('runtime_subnet_id', None) + self.docker_bridge_cidr = kwargs.get('docker_bridge_cidr', None) + self.platform_reserved_cidr = kwargs.get('platform_reserved_cidr', None) + self.platform_reserved_dns_ip = kwargs.get('platform_reserved_dns_ip', None) + + +class Volume(Model): + """Volume definitions for the Container App. + + :param name: Volume name. + :type name: str + :param storage_type: Storage type for the volume. If not provided, use + EmptyDir. Possible values include: 'AzureFile', 'EmptyDir' + :type storage_type: str or ~commondefinitions.models.StorageType + :param storage_name: Name of storage resource. No need to provide for + EmptyDir. + :type storage_name: str + """ + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'storage_type': {'key': 'storageType', 'type': 'str'}, + 'storage_name': {'key': 'storageName', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Volume, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.storage_type = kwargs.get('storage_type', None) + self.storage_name = kwargs.get('storage_name', None) + + +class VolumeMount(Model): + """Volume mount for the Container App. + + :param volume_name: This must match the Name of a Volume. + :type volume_name: str + :param mount_path: Path within the container at which the volume should be + mounted.Must not contain ':'. + :type mount_path: str + """ + + _attribute_map = { + 'volume_name': {'key': 'volumeName', 'type': 'str'}, + 'mount_path': {'key': 'mountPath', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(VolumeMount, self).__init__(**kwargs) + self.volume_name = kwargs.get('volume_name', None) + self.mount_path = kwargs.get('mount_path', None) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py new file mode 100644 index 00000000000..7376167f592 --- /dev/null +++ b/src/containerapp/azext_containerapp/_utils.py @@ -0,0 +1,587 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument + +from urllib.parse import urlparse +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) +from azure.cli.core.commands.client_factory import get_subscription_id +from knack.log import get_logger +from msrestazure.tools import parse_resource_id + +from ._clients import ContainerAppClient +from ._client_factory import handle_raw_exception, providers_client_factory, cf_resource_groups, log_analytics_client_factory, log_analytics_shared_key_client_factory + +logger = get_logger(__name__) + + +def _get_location_from_resource_group(cli_ctx, resource_group_name): + client = cf_resource_groups(cli_ctx) + group = client.get(resource_group_name) + return group.location + + +def _validate_subscription_registered(cmd, resource_provider, subscription_id=None): + providers_client = None + if not subscription_id: + subscription_id = get_subscription_id(cmd.cli_ctx) + + try: + providers_client = providers_client_factory(cmd.cli_ctx, subscription_id) + registration_state = getattr(providers_client.get(resource_provider), 'registration_state', "NotRegistered") + + if not (registration_state and registration_state.lower() == 'registered'): + raise ValidationError('Subscription {} is not registered for the {} resource provider. Please run \"az provider register -n {} --wait\" to register your subscription.'.format( + subscription_id, resource_provider, resource_provider)) + except ValidationError as ex: + raise ex + except Exception: # pylint: disable=broad-except + pass + + +def _ensure_location_allowed(cmd, location, resource_provider, resource_type): + providers_client = None + try: + providers_client = providers_client_factory(cmd.cli_ctx, get_subscription_id(cmd.cli_ctx)) + + if providers_client is not None: + resource_types = getattr(providers_client.get(resource_provider), 'resource_types', []) + res_locations = [] + for res in resource_types: + if res and getattr(res, 'resource_type', "") == resource_type: + res_locations = getattr(res, 'locations', []) + + res_locations = [res_loc.lower().replace(" ", "").replace("(", "").replace(")", "") for res_loc in res_locations if res_loc.strip()] + + location_formatted = location.lower().replace(" ", "") + if location_formatted not in res_locations: + raise ValidationError("Location '{}' is not currently supported. To get list of supported locations, run `az provider show -n {} --query \"resourceTypes[?resourceType=='{}'].locations\"`".format( + location, resource_provider, resource_type)) + except ValidationError as ex: + raise ex + except Exception: # pylint: disable=broad-except + pass + + +def parse_env_var_flags(env_list, is_update_containerapp=False): + env_pairs = {} + + for pair in env_list: + key_val = pair.split('=', 1) + if len(key_val) != 2: + if is_update_containerapp: + raise ValidationError("Environment variables must be in the format \"=\" \"=secretref:\" ...\".") + raise ValidationError("Environment variables must be in the format \"=\" \"=secretref:\" ...\".") + if key_val[0] in env_pairs: + raise ValidationError("Duplicate environment variable {env} found, environment variable names must be unique.".format(env=key_val[0])) + value = key_val[1].split('secretref:') + env_pairs[key_val[0]] = value + + env_var_def = [] + for key, value in env_pairs.items(): + if len(value) == 2: + env_var_def.append({ + "name": key, + "secretRef": value[1] + }) + else: + env_var_def.append({ + "name": key, + "value": value[0] + }) + + return env_var_def + + +def parse_secret_flags(secret_list): + secret_pairs = {} + + for pair in secret_list: + key_val = pair.split('=', 1) + if len(key_val) != 2: + raise ValidationError("--secrets: must be in format \"=,=,...\"") + if key_val[0] in secret_pairs: + raise ValidationError("--secrets: duplicate secret {secret} found, secret names must be unique.".format(secret=key_val[0])) + secret_pairs[key_val[0]] = key_val[1] + + secret_var_def = [] + for key, value in secret_pairs.items(): + secret_var_def.append({ + "name": key, + "value": value + }) + + return secret_var_def + + +def _update_revision_env_secretrefs(containers, name): + for container in containers: + if "env" in container: + for var in container["env"]: + if "secretRef" in var: + var["secretRef"] = var["secretRef"].replace("{}-".format(name), "") + + +def _update_revision_env_secretrefs(containers, name): + for container in containers: + if "env" in container: + for var in container["env"]: + if "secretRef" in var: + var["secretRef"] = var["secretRef"].replace("{}-".format(name), "") + + +def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass, update_existing_secret=False): + if registry_pass.startswith("secretref:"): + # If user passed in registry password using a secret + + registry_pass = registry_pass.split("secretref:") + if len(registry_pass) <= 1: + raise ValidationError("Invalid registry password secret. Value must be a non-empty value starting with \'secretref:\'.") + registry_pass = registry_pass[1:] + registry_pass = ''.join(registry_pass) + + if not any(secret for secret in secrets_list if secret['name'].lower() == registry_pass.lower()): + raise ValidationError("Registry password secret with name '{}' does not exist. Add the secret using --secrets".format(registry_pass)) + + return registry_pass + else: + # If user passed in registry password + if urlparse(registry_server).hostname is not None: + registry_secret_name = "{server}-{user}".format(server=urlparse(registry_server).hostname.replace('.', ''), user=registry_user.lower()) + else: + registry_secret_name = "{server}-{user}".format(server=registry_server.replace('.', ''), user=registry_user.lower()) + + for secret in secrets_list: + if secret['name'].lower() == registry_secret_name.lower(): + if secret['value'].lower() != registry_pass.lower(): + if update_existing_secret: + secret['value'] = registry_pass + else: + raise ValidationError('Found secret with name \"{}\" but value does not equal the supplied registry password.'.format(registry_secret_name)) + return registry_secret_name + + logger.warning('Adding registry password as a secret with name \"{}\"'.format(registry_secret_name)) # pylint: disable=logging-format-interpolation + secrets_list.append({ + "name": registry_secret_name, + "value": registry_pass + }) + + return registry_secret_name + + +def parse_list_of_strings(comma_separated_string): + comma_separated = comma_separated_string.split(',') + return [s.strip() for s in comma_separated] + + +def raise_missing_token_suggestion(): + pat_documentation = "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line" + raise RequiredArgumentMissingError("GitHub access token is required to authenticate to your repositories. " + "If you need to create a Github Personal Access Token, " + "please run with the '--login-with-github' flag or follow " + "the steps found at the following link:\n{0}".format(pat_documentation)) + + +def _get_default_log_analytics_location(cmd): + default_location = "eastus" + providers_client = None + try: + providers_client = providers_client_factory(cmd.cli_ctx, get_subscription_id(cmd.cli_ctx)) + resource_types = getattr(providers_client.get("Microsoft.OperationalInsights"), 'resource_types', []) + res_locations = [] + for res in resource_types: + if res and getattr(res, 'resource_type', "") == "workspaces": + res_locations = getattr(res, 'locations', []) + + if len(res_locations) > 0: + location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") + if location: + return location + + except Exception: # pylint: disable=broad-except + return default_location + return default_location + + +# Generate random 4 character string +def _new_tiny_guid(): + import random + import string + return ''.join(random.choices(string.ascii_letters + string.digits, k=4)) + + +# Follow same naming convention as Portal +def _generate_log_analytics_workspace_name(resource_group_name): + import re + prefix = "workspace" + suffix = _new_tiny_guid() + alphaNumericRG = resource_group_name + alphaNumericRG = re.sub(r'[^0-9a-z]', '', resource_group_name) + maxLength = 40 + + name = "{}-{}{}".format( + prefix, + alphaNumericRG, + suffix + ) + + if len(name) > maxLength: + name = name[:maxLength] + return name + + +def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, location, resource_group_name): + if logs_customer_id is None and logs_key is None: + logger.warning("No Log Analytics workspace provided.") + try: + _validate_subscription_registered(cmd, "Microsoft.OperationalInsights") + log_analytics_client = log_analytics_client_factory(cmd.cli_ctx) + log_analytics_shared_key_client = log_analytics_shared_key_client_factory(cmd.cli_ctx) + + log_analytics_location = location + try: + _ensure_location_allowed(cmd, log_analytics_location, "Microsoft.OperationalInsights", "workspaces") + except Exception: # pylint: disable=broad-except + log_analytics_location = _get_default_log_analytics_location(cmd) + + from azure.cli.core.commands import LongRunningOperation + from azure.mgmt.loganalytics.models import Workspace + + workspace_name = _generate_log_analytics_workspace_name(resource_group_name) + workspace_instance = Workspace(location=log_analytics_location) + logger.warning("Generating a Log Analytics workspace with name \"{}\"".format(workspace_name)) # pylint: disable=logging-format-interpolation + + poller = log_analytics_client.begin_create_or_update(resource_group_name, workspace_name, workspace_instance) + log_analytics_workspace = LongRunningOperation(cmd.cli_ctx)(poller) + + logs_customer_id = log_analytics_workspace.customer_id + logs_key = log_analytics_shared_key_client.get_shared_keys( + workspace_name=workspace_name, + resource_group_name=resource_group_name).primary_shared_key + + except Exception as ex: + raise ValidationError("Unable to generate a Log Analytics workspace. You can use \"az monitor log-analytics workspace create\" to create one and supply --logs-customer-id and --logs-key") from ex + elif logs_customer_id is None: + raise ValidationError("Usage error: Supply the --logs-customer-id associated with the --logs-key") + elif logs_key is None: # Try finding the logs-key + log_analytics_client = log_analytics_client_factory(cmd.cli_ctx) + log_analytics_shared_key_client = log_analytics_shared_key_client_factory(cmd.cli_ctx) + + log_analytics_name = None + log_analytics_rg = None + log_analytics = log_analytics_client.list() + + for la in log_analytics: + if la.customer_id and la.customer_id.lower() == logs_customer_id.lower(): + log_analytics_name = la.name + parsed_la = parse_resource_id(la.id) + log_analytics_rg = parsed_la['resource_group'] + + if log_analytics_name is None: + raise ValidationError('Usage error: Supply the --logs-key associated with the --logs-customer-id') + + shared_keys = log_analytics_shared_key_client.get_shared_keys(workspace_name=log_analytics_name, resource_group_name=log_analytics_rg) + + if not shared_keys or not shared_keys.primary_shared_key: + raise ValidationError('Usage error: Supply the --logs-key associated with the --logs-customer-id') + + logs_key = shared_keys.primary_shared_key + + return logs_customer_id, logs_key + + +def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + else: + secrets = [] + try: + secrets = ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) + except Exception as e: # pylint: disable=broad-except + handle_raw_exception(e) + + containerapp_def["properties"]["configuration"]["secrets"] = secrets["value"] + + +def _ensure_identity_resource_id(subscription_id, resource_group, resource): + from msrestazure.tools import resource_id, is_valid_resource_id + if is_valid_resource_id(resource): + return resource + + return resource_id(subscription=subscription_id, + resource_group=resource_group, + namespace='Microsoft.ManagedIdentity', + type='userAssignedIdentities', + name=resource) + + +def _add_or_update_secrets(containerapp_def, add_secrets): + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + + for new_secret in add_secrets: + is_existing = False + for existing_secret in containerapp_def["properties"]["configuration"]["secrets"]: + if existing_secret["name"].lower() == new_secret["name"].lower(): + is_existing = True + existing_secret["value"] = new_secret["value"] + break + + if not is_existing: + containerapp_def["properties"]["configuration"]["secrets"].append(new_secret) + + +def _remove_registry_secret(containerapp_def, server, username): + if urlparse(server).hostname is not None: + registry_secret_name = "{server}-{user}".format(server=urlparse(server).hostname.replace('.', ''), user=username.lower()) + else: + registry_secret_name = "{server}-{user}".format(server=server.replace('.', ''), user=username.lower()) + + _remove_secret(containerapp_def, secret_name=registry_secret_name) + + +def _remove_secret(containerapp_def, secret_name): + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + + for index, value in enumerate(containerapp_def["properties"]["configuration"]["secrets"]): + existing_secret = value + if existing_secret["name"].lower() == secret_name.lower(): + containerapp_def["properties"]["configuration"]["secrets"].pop(index) + break + + +def _add_or_update_env_vars(existing_env_vars, new_env_vars, is_add=False): + for new_env_var in new_env_vars: + + # Check if updating existing env var + is_existing = False + for existing_env_var in existing_env_vars: + if existing_env_var["name"].lower() == new_env_var["name"].lower(): + is_existing = True + if is_add: + logger.warning("Environment variable {} already exists. Replacing environment variable value.".format(new_env_var["name"])) # pylint: disable=logging-format-interpolation + + if "value" in new_env_var: + existing_env_var["value"] = new_env_var["value"] + else: + existing_env_var["value"] = None + + if "secretRef" in new_env_var: + existing_env_var["secretRef"] = new_env_var["secretRef"] + else: + existing_env_var["secretRef"] = None + break + + # If not updating existing env var, add it as a new env var + if not is_existing: + if not is_add: + logger.warning("Environment variable {} does not exist. Adding as new environment variable.".format(new_env_var["name"])) # pylint: disable=logging-format-interpolation + existing_env_vars.append(new_env_var) + + +def _remove_env_vars(existing_env_vars, remove_env_vars): + for old_env_var in remove_env_vars: + + # Check if updating existing env var + is_existing = False + for i, value in enumerate(existing_env_vars): + existing_env_var = value + if existing_env_var["name"].lower() == old_env_var.lower(): + is_existing = True + existing_env_vars.pop(i) + break + + # If not updating existing env var, add it as a new env var + if not is_existing: + logger.warning("Environment variable {} does not exist.".format(old_env_var)) # pylint: disable=logging-format-interpolation + + +def _remove_env_vars(existing_env_vars, remove_env_vars): + for old_env_var in remove_env_vars: + + # Check if updating existing env var + is_existing = False + for index, value in enumerate(existing_env_vars): + existing_env_var = value + if existing_env_var["name"].lower() == old_env_var.lower(): + is_existing = True + existing_env_vars.pop(index) + break + + # If not updating existing env var, add it as a new env var + if not is_existing: + logger.warning("Environment variable {} does not exist.".format(old_env_var)) # pylint: disable=logging-format-interpolation + + +def _add_or_update_tags(containerapp_def, tags): + if 'tags' not in containerapp_def: + if tags: + containerapp_def['tags'] = tags + else: + containerapp_def['tags'] = {} + else: + for key in tags: + containerapp_def['tags'][key] = tags[key] + + +def _object_to_dict(obj): + import json + import datetime + + def default_handler(x): + if isinstance(x, datetime.datetime): + return x.isoformat() + return x.__dict__ + + return json.loads(json.dumps(obj, default=default_handler)) + + +def _to_camel_case(snake_str): + components = snake_str.split('_') + return components[0] + ''.join(x.title() for x in components[1:]) + + +def _convert_object_from_snake_to_camel_case(o): + if isinstance(o, list): + return [_convert_object_from_snake_to_camel_case(i) if isinstance(i, (dict, list)) else i for i in o] + return { + _to_camel_case(a): _convert_object_from_snake_to_camel_case(b) if isinstance(b, (dict, list)) else b for a, b in o.items() + } + + +def _remove_additional_attributes(o): + if isinstance(o, list): + for i in o: + _remove_additional_attributes(i) + elif isinstance(o, dict): + if "additionalProperties" in o: + del o["additionalProperties"] + + for key in o: + _remove_additional_attributes(o[key]) + + +def _remove_readonly_attributes(containerapp_def): + unneeded_properties = [ + "id", + "name", + "type", + "systemData", + "provisioningState", + "latestRevisionName", + "latestRevisionFqdn", + "customDomainVerificationId", + "outboundIpAddresses", + "fqdn" + ] + + for unneeded_property in unneeded_properties: + if unneeded_property in containerapp_def: + del containerapp_def[unneeded_property] + elif unneeded_property in containerapp_def['properties']: + del containerapp_def['properties'][unneeded_property] + + +def _remove_dapr_readonly_attributes(daprcomponent_def): + unneeded_properties = [ + "id", + "name", + "type", + "systemData", + "provisioningState", + "latestRevisionName", + "latestRevisionFqdn", + "customDomainVerificationId", + "outboundIpAddresses", + "fqdn" + ] + + for unneeded_property in unneeded_properties: + if unneeded_property in daprcomponent_def: + del daprcomponent_def[unneeded_property] + + +def update_nested_dictionary(orig_dict, new_dict): + # Recursively update a nested dictionary. If the value is a list, replace the old list with new list + from collections.abc import Mapping + + for key, val in new_dict.items(): + if isinstance(val, Mapping): + tmp = update_nested_dictionary(orig_dict.get(key, {}), val) + orig_dict[key] = tmp + elif isinstance(val, list): + if new_dict[key]: + orig_dict[key] = new_dict[key] + else: + if new_dict[key] is not None: + orig_dict[key] = new_dict[key] + return orig_dict + + +def _is_valid_weight(weight): + try: + n = int(weight) + if 0 <= n <= 100: + return True + return False + except ValueError: + return False + + +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"] = [] + + for new_weight in list_weights: + key_val = new_weight.split('=', 1) + is_existing = False + + if len(key_val) != 2: + raise ValidationError('Traffic weights must be in format \"=weight = ...\"') + + if not _is_valid_weight(key_val[1]): + raise ValidationError('Traffic weights must be integers between 0 and 100') + + if not is_existing: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ + "revisionName": key_val[0], + "weight": int(key_val[1]) + }) + + +def _get_app_from_revision(revision): + if not revision: + raise ValidationError('Invalid revision. Revision must not be empty') + + revision = revision.split('--') + revision.pop() + revision = "--".join(revision) + return 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 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] + + try: + registry_user, registry_pass = _get_acr_cred(cmd.cli_ctx, registry_name) + 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)) from ex + + +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/_validators.py b/src/containerapp/azext_containerapp/_validators.py new file mode 100644 index 00000000000..e7fe0435a11 --- /dev/null +++ b/src/containerapp/azext_containerapp/_validators.py @@ -0,0 +1,88 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long + +from azure.cli.core.azclierror import (ValidationError) + + +def _is_number(s): + try: + float(s) + return True + except ValueError: + return False + + +def validate_memory(namespace): + memory = namespace.memory + + if memory is not None: + valid = False + + if memory.endswith("Gi"): + valid = _is_number(memory[:-2]) + + if not valid: + raise ValidationError("Usage error: --memory must be a number ending with \"Gi\"") + + +def validate_cpu(namespace): + if namespace.cpu: + cpu = namespace.cpu + try: + float(cpu) + except ValueError as e: + raise ValidationError("Usage error: --cpu must be a number eg. \"0.5\"") from e + + +def validate_managed_env_name_or_id(cmd, namespace): + from azure.cli.core.commands.client_factory import get_subscription_id + from msrestazure.tools import is_valid_resource_id, resource_id + + if namespace.managed_env: + if not is_valid_resource_id(namespace.managed_env): + namespace.managed_env = resource_id( + subscription=get_subscription_id(cmd.cli_ctx), + resource_group=namespace.resource_group_name, + namespace='Microsoft.App', + type='managedEnvironments', + name=namespace.managed_env + ) + + +def validate_registry_server(namespace): + if "create" in namespace.command.lower(): + if namespace.registry_server: + if not namespace.registry_user or not namespace.registry_pass: + if ".azurecr.io" not in namespace.registry_server: + raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + + +def validate_registry_user(namespace): + if "create" in namespace.command.lower(): + if namespace.registry_user: + if not namespace.registry_server or (not namespace.registry_pass and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + + +def validate_registry_pass(namespace): + if "create" in namespace.command.lower(): + if namespace.registry_pass: + if not namespace.registry_server or (not namespace.registry_user and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + + +def validate_target_port(namespace): + if "create" in namespace.command.lower(): + if namespace.target_port: + if not namespace.ingress: + raise ValidationError("Usage error: must specify --ingress with --target-port") + + +def validate_ingress(namespace): + if "create" in namespace.command.lower(): + if namespace.ingress: + if not namespace.target_port: + raise ValidationError("Usage error: must specify --target-port with --ingress") diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json new file mode 100644 index 00000000000..cf7b8927a07 --- /dev/null +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -0,0 +1,4 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.15.0" +} diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py new file mode 100644 index 00000000000..c5b924287f8 --- /dev/null +++ b/src/containerapp/azext_containerapp/commands.py @@ -0,0 +1,102 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=line-too-long, too-many-statements, bare-except +# from azure.cli.core.commands import CliCommandType +# from msrestazure.tools import is_valid_resource_id, parse_resource_id +from azext_containerapp._client_factory import ex_handler_factory + + +def transform_containerapp_output(app): + props = ['name', 'location', 'resourceGroup', 'provisioningState'] + result = {k: app[k] for k in app if k in props} + + try: + result['fqdn'] = app['properties']['configuration']['ingress']['fqdn'] + except: + result['fqdn'] = None + + return result + + +def transform_containerapp_list_output(apps): + return [transform_containerapp_output(a) for a in apps] + + +def transform_revision_output(rev): + props = ['name', 'active', 'createdTime', 'trafficWeight'] + result = {k: rev['properties'][k] for k in rev['properties'] if k in props} + + if 'name' in rev: + result['name'] = rev['name'] + + if 'fqdn' in rev['properties']['template']: + result['fqdn'] = rev['properties']['template']['fqdn'] + + return result + + +def transform_revision_list_output(revs): + return [transform_revision_output(r) for r in revs] + + +def load_command_table(self, _): + with self.command_group('containerapp', is_preview=True) as g: + g.custom_show_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(), table_transformer=transform_containerapp_output) + g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory(), table_transformer=transform_containerapp_output) + g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + + with self.command_group('containerapp env') as g: + g.custom_show_command('show', 'show_managed_environment') + g.custom_command('list', 'list_managed_environments') + g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + + with self.command_group('containerapp env dapr-component') as g: + g.custom_command('list', 'list_dapr_components') + g.custom_show_command('show', 'show_dapr_component') + g.custom_command('set', 'create_or_update_dapr_component') + g.custom_command('remove', 'remove_dapr_component') + + with self.command_group('containerapp github-action') as g: + g.custom_command('add', 'create_or_update_github_action', exception_handler=ex_handler_factory()) + g.custom_show_command('show', 'show_github_action', exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_github_action', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp revision') as g: + g.custom_command('activate', 'activate_revision') + g.custom_command('deactivate', 'deactivate_revision') + g.custom_command('list', 'list_revisions', table_transformer=transform_revision_list_output, exception_handler=ex_handler_factory()) + g.custom_command('restart', 'restart_revision') + g.custom_show_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) + g.custom_command('copy', 'copy_revision', exception_handler=ex_handler_factory()) + g.custom_command('set-mode', 'set_revision_mode', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp ingress') as g: + g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) + g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) + g.custom_show_command('show', 'show_ingress') + + with self.command_group('containerapp ingress traffic') as g: + g.custom_command('set', 'set_ingress_traffic', exception_handler=ex_handler_factory()) + g.custom_show_command('show', 'show_ingress_traffic') + + with self.command_group('containerapp registry') as g: + g.custom_command('set', 'set_registry', exception_handler=ex_handler_factory()) + g.custom_show_command('show', 'show_registry') + g.custom_command('list', 'list_registry') + g.custom_command('remove', 'remove_registry', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp secret') as g: + g.custom_command('list', 'list_secrets') + g.custom_show_command('show', 'show_secret') + g.custom_command('remove', 'remove_secrets', exception_handler=ex_handler_factory()) + g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp dapr') as g: + g.custom_command('enable', 'enable_dapr', exception_handler=ex_handler_factory()) + g.custom_command('disable', 'disable_dapr', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py new file mode 100644 index 00000000000..15cea81f0ff --- /dev/null +++ b/src/containerapp/azext_containerapp/custom.py @@ -0,0 +1,1934 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- +# 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 + +from urllib.parse import urlparse +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) +from azure.cli.core.azclierror import ( + RequiredArgumentMissingError, + ValidationError, + ResourceNotFoundError, + CLIError, + CLIInternalError, + InvalidArgumentValueError) +from azure.cli.core.commands.client_factory import get_subscription_id +from knack.log import get_logger + +from msrestazure.tools import parse_resource_id, is_valid_resource_id +from msrest.exceptions import DeserializationError + +from ._client_factory import handle_raw_exception +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient +from ._github_oauth import get_github_access_token +from ._models import ( + ManagedEnvironment as ManagedEnvironmentModel, + VnetConfiguration as VnetConfigurationModel, + AppLogsConfiguration as AppLogsConfigurationModel, + LogAnalyticsConfiguration as LogAnalyticsConfigurationModel, + Ingress as IngressModel, + Configuration as ConfigurationModel, + Template as TemplateModel, + RegistryCredentials as RegistryCredentialsModel, + ContainerApp as ContainerAppModel, + Dapr as DaprModel, + ContainerResources as ContainerResourcesModel, + Scale as ScaleModel, + Container as ContainerModel, + GitHubActionConfiguration, + RegistryInfo as RegistryInfoModel, + AzureCredentials as AzureCredentialsModel, + SourceControl as SourceControlModel, + ManagedServiceIdentity as ManagedServiceIdentityModel) +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, + _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, + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret, + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, _update_revision_env_secretrefs) + +logger = get_logger(__name__) + + +# These properties should be under the "properties" attribute. Move the properties under "properties" attribute +def process_loaded_yaml(yaml_containerapp): + if not yaml_containerapp.get('properties'): + yaml_containerapp['properties'] = {} + + nested_properties = ["provisioningState", "managedEnvironmentId", "latestRevisionName", "latestRevisionFqdn", "customDomainVerificationId", "configuration", "template", "outboundIPAddresses"] + for nested_property in nested_properties: + tmp = yaml_containerapp.get(nested_property) + if tmp: + yaml_containerapp['properties'][nested_property] = tmp + del yaml_containerapp[nested_property] + + return yaml_containerapp + + +def load_yaml_file(file_name): + import yaml + import errno + + try: + with open(file_name) as stream: # pylint: disable=unspecified-encoding + return yaml.safe_load(stream) + except (IOError, OSError) as ex: + if getattr(ex, 'errno', 0) == errno.ENOENT: + raise ValidationError('{} does not exist'.format(file_name)) from ex + raise + except (yaml.parser.ParserError, UnicodeDecodeError) as ex: + raise ValidationError('Error parsing {} ({})'.format(file_name, str(ex))) from ex + + +def create_deserializer(): + from ._sdk_models import ContainerApp # pylint: disable=unused-import + from msrest import Deserializer + import sys + import inspect + + sdkClasses = inspect.getmembers(sys.modules["azext_containerapp._sdk_models"]) + deserializer = {} + + for sdkClass in sdkClasses: + deserializer[sdkClass[0]] = sdkClass[1] + + return Deserializer(deserializer) + + +def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_revision=None, no_wait=False): + yaml_containerapp = process_loaded_yaml(load_yaml_file(file_name)) + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck + raise ValidationError('Invalid YAML provided. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') + + if not yaml_containerapp.get('name'): + yaml_containerapp['name'] = name + elif yaml_containerapp.get('name').lower() != name.lower(): + logger.warning('The app name provided in the --yaml file "{}" does not match the one provided in the --name flag "{}". The one provided in the --yaml file will be used.'.format( + yaml_containerapp.get('name'), name)) + name = yaml_containerapp.get('name') + + if not yaml_containerapp.get('type'): + yaml_containerapp['type'] = 'Microsoft.App/containerApps' + elif yaml_containerapp.get('type').lower() != "microsoft.app/containerapps": + raise ValidationError('Containerapp type must be \"Microsoft.App/ContainerApps\"') + + current_containerapp_def = None + containerapp_def = None + try: + current_containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except Exception: + pass + + if not current_containerapp_def: + raise ValidationError("The containerapp '{}' does not exist".format(name)) + + # Change which revision we update from + if from_revision: + try: + r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) + except CLIError as e: + handle_raw_exception(e) + _update_revision_env_secretrefs(r["properties"]["template"]["containers"], name) + current_containerapp_def["properties"]["template"] = r["properties"]["template"] + + # Deserialize the yaml into a ContainerApp object. Need this since we're not using SDK + try: + deserializer = create_deserializer() + + containerapp_def = deserializer('ContainerApp', yaml_containerapp) + except DeserializationError as ex: + raise ValidationError('Invalid YAML provided. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') from ex + + # Remove tags before converting from snake case to camel case, then re-add tags. We don't want to change the case of the tags. Need this since we're not using SDK + tags = None + if yaml_containerapp.get('tags'): + tags = yaml_containerapp.get('tags') + del yaml_containerapp['tags'] + + containerapp_def = _convert_object_from_snake_to_camel_case(_object_to_dict(containerapp_def)) + containerapp_def['tags'] = tags + + # After deserializing, some properties may need to be moved under the "properties" attribute. Need this since we're not using SDK + containerapp_def = process_loaded_yaml(containerapp_def) + + _get_existing_secrets(cmd, resource_group_name, name, current_containerapp_def) + + update_nested_dictionary(current_containerapp_def, containerapp_def) + + # Remove "additionalProperties" and read-only attributes that are introduced in the deserialization. Need this since we're not using SDK + _remove_additional_attributes(current_containerapp_def) + _remove_readonly_attributes(current_containerapp_def) + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=current_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 creation in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format( + name, resource_group_name + )) + + return r + except Exception as e: + handle_raw_exception(e) + + +def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait=False): + yaml_containerapp = process_loaded_yaml(load_yaml_file(file_name)) + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck + raise ValidationError('Invalid YAML provided. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') + + if not yaml_containerapp.get('name'): + yaml_containerapp['name'] = name + elif yaml_containerapp.get('name').lower() != name.lower(): + logger.warning('The app name provided in the --yaml file "{}" does not match the one provided in the --name flag "{}". The one provided in the --yaml file will be used.'.format( + yaml_containerapp.get('name'), name)) + name = yaml_containerapp.get('name') + + if not yaml_containerapp.get('type'): + yaml_containerapp['type'] = 'Microsoft.App/containerApps' + elif yaml_containerapp.get('type').lower() != "microsoft.app/containerapps": + raise ValidationError('Containerapp type must be \"Microsoft.App/ContainerApps\"') + + # Deserialize the yaml into a ContainerApp object. Need this since we're not using SDK + containerapp_def = None + try: + deserializer = create_deserializer() + + containerapp_def = deserializer('ContainerApp', yaml_containerapp) + except DeserializationError as ex: + raise ValidationError('Invalid YAML provided. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') from ex + + # Remove tags before converting from snake case to camel case, then re-add tags. We don't want to change the case of the tags. Need this since we're not using SDK + tags = None + if yaml_containerapp.get('tags'): + tags = yaml_containerapp.get('tags') + del yaml_containerapp['tags'] + + containerapp_def = _convert_object_from_snake_to_camel_case(_object_to_dict(containerapp_def)) + containerapp_def['tags'] = tags + + # After deserializing, some properties may need to be moved under the "properties" attribute. Need this since we're not using SDK + containerapp_def = process_loaded_yaml(containerapp_def) + + # Remove "additionalProperties" and read-only attributes that are introduced in the deserialization. Need this since we're not using SDK + _remove_additional_attributes(containerapp_def) + _remove_readonly_attributes(containerapp_def) + + # Validate managed environment + if not containerapp_def["properties"].get('managedEnvironmentId'): + raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') + + env_id = containerapp_def["properties"]['managedEnvironmentId'] + env_name = None + env_rg = None + env_info = None + + if is_valid_resource_id(env_id): + parsed_managed_env = parse_resource_id(env_id) + env_name = parsed_managed_env['name'] + env_rg = parsed_managed_env['resource_group'] + else: + raise ValidationError('Invalid managedEnvironmentId specified. Environment not found') + + try: + env_info = ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=env_rg, name=env_name) + except: + pass + + if not env_info: + raise ValidationError("The environment '{}' in resource group '{}' was not found".format(env_name, env_rg)) + + # Validate location + if not containerapp_def.get('location'): + containerapp_def['location'] = env_info['location'] + + 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 creation in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format( + name, resource_group_name + )) + + if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: + logger.warning("\nContainer app created. Access your app at https://{}/\n".format(r["properties"]["configuration"]["ingress"]["fqdn"])) + else: + logger.warning("\nContainer app created. To access it over HTTPS, enable ingress: az containerapp ingress enable --help\n") + + return r + except Exception as e: + handle_raw_exception(e) + + +def create_containerapp(cmd, + name, + resource_group_name, + yaml=None, + image=None, + container_name=None, + managed_env=None, + min_replicas=None, + max_replicas=None, + target_port=None, + transport="auto", + ingress=None, + revisions_mode="single", + secrets=None, + env_vars=None, + cpu=None, + memory=None, + registry_server=None, + registry_user=None, + registry_pass=None, + dapr_enabled=False, + dapr_app_port=None, + dapr_app_id=None, + dapr_app_protocol=None, + revision_suffix=None, + startup_command=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if yaml: + if image or managed_env or min_replicas or max_replicas or target_port or ingress 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\ + 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 create_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait) + + if not image: + image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest" + + if managed_env is None: + raise RequiredArgumentMissingError('Usage error: --environment is required if not using --yaml') + + # Validate managed environment + parsed_managed_env = parse_resource_id(managed_env) + managed_env_name = parsed_managed_env['name'] + managed_env_rg = parsed_managed_env['resource_group'] + managed_env_info = None + + try: + managed_env_info = ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=managed_env_rg, name=managed_env_name) + except: + pass + + if not managed_env_info: + raise ValidationError("The environment '{}' does not exist. Specify a valid environment".format(managed_env)) + + location = managed_env_info["location"] + _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") + + external_ingress = None + if ingress is not None: + if ingress.lower() == "internal": + external_ingress = False + elif ingress.lower() == "external": + external_ingress = True + + ingress_def = None + if target_port is not None and ingress is not None: + ingress_def = IngressModel + ingress_def["external"] = external_ingress + ingress_def["targetPort"] = target_port + ingress_def["transport"] = transport + + secrets_def = None + if secrets is not None: + secrets_def = parse_secret_flags(secrets) + + registries_def = None + if registry_server is not None: + registries_def = RegistryCredentialsModel + + # Infer credentials if not supplied and its azurecr + 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 + registries_def["username"] = registry_user + + if secrets_def is None: + secrets_def = [] + registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + + dapr_def = None + if dapr_enabled: + dapr_def = DaprModel + dapr_def["enabled"] = True + dapr_def["appId"] = dapr_app_id + dapr_def["appPort"] = dapr_app_port + dapr_def["appProtocol"] = dapr_app_protocol + + config_def = ConfigurationModel + config_def["secrets"] = secrets_def + config_def["activeRevisionsMode"] = revisions_mode + config_def["ingress"] = ingress_def + config_def["registries"] = [registries_def] if registries_def is not None else None + config_def["dapr"] = dapr_def + + scale_def = None + if min_replicas is not None or max_replicas is not None: + scale_def = ScaleModel + scale_def["minReplicas"] = min_replicas + scale_def["maxReplicas"] = max_replicas + + resources_def = None + if cpu is not None or memory is not None: + resources_def = ContainerResourcesModel + resources_def["cpu"] = cpu + resources_def["memory"] = memory + + container_def = ContainerModel + 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) + if startup_command is not None: + container_def["command"] = startup_command + if args is not None: + container_def["args"] = args + if resources_def is not None: + container_def["resources"] = resources_def + + template_def = TemplateModel + template_def["containers"] = [container_def] + template_def["scale"] = scale_def + + if revision_suffix is not None: + template_def["revisionSuffix"] = revision_suffix + + containerapp_def = ContainerAppModel + containerapp_def["location"] = location + containerapp_def["properties"]["managedEnvironmentId"] = managed_env + containerapp_def["properties"]["configuration"] = config_def + containerapp_def["properties"]["template"] = template_def + containerapp_def["tags"] = tags + + 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 creation in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + + if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: + logger.warning("\nContainer app created. Access your app at https://{}/\n".format(r["properties"]["configuration"]["ingress"]["fqdn"])) + else: + logger.warning("\nContainer app created. To access it over HTTPS, enable ingress: az containerapp ingress enable --help\n") + + return r + except Exception as e: + handle_raw_exception(e) + + +def update_containerapp(cmd, + name, + resource_group_name, + yaml=None, + image=None, + container_name=None, + min_replicas=None, + max_replicas=None, + set_env_vars=None, + remove_env_vars=None, + replace_env_vars=None, + remove_all_env_vars=False, + cpu=None, + memory=None, + revision_suffix=None, + startup_command=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if yaml: + if image or min_replicas or max_replicas or\ + set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory 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) + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + # 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['scale'] = min_replicas or max_replicas + update_map['container'] = image or container_name or set_env_vars is not None or remove_env_vars is not None or replace_env_vars is not None or remove_all_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) + + if revision_suffix is not None: + containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix + + # Containers + if update_map["container"]: + if not container_name: + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + 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() == container_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + + if set_env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + # env vars + _add_or_update_env_vars(c["env"], parse_env_var_flags(set_env_vars), is_add=True) + + if replace_env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + # env vars + _add_or_update_env_vars(c["env"], parse_env_var_flags(replace_env_vars)) + + if remove_env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + # env vars + _remove_env_vars(c["env"], remove_env_vars) + + if remove_all_env_vars: + c["env"] = [] + + if startup_command is not None: + if isinstance(startup_command, list) and not startup_command: + c["command"] = None + else: + c["command"] = startup_command + if args is not None: + if isinstance(args, list) and not args: + c["args"] = None + else: + c["args"] = args + if cpu is not None or memory is not None: + if "resources" in c and c["resources"]: + if cpu is not None: + c["resources"]["cpu"] = cpu + if memory is not None: + c["resources"]["memory"] = memory + else: + c["resources"] = { + "cpu": cpu, + "memory": memory + } + + # If not updating existing container, add as new container + if not updating_existing_container: + if image is None: + raise ValidationError("Usage error: --image is required when adding a new container") + + resources_def = None + if cpu is not None or memory is not None: + resources_def = ContainerResourcesModel + resources_def["cpu"] = cpu + resources_def["memory"] = memory + + container_def = ContainerModel + container_def["name"] = container_name + container_def["image"] = image + container_def["env"] = [] + + if set_env_vars is not None: + # env vars + _add_or_update_env_vars(container_def["env"], parse_env_var_flags(set_env_vars), is_add=True) + + if replace_env_vars is not None: + # env vars + _add_or_update_env_vars(container_def["env"], parse_env_var_flags(replace_env_vars)) + + if remove_env_vars is not None: + # env vars + _remove_env_vars(container_def["env"], remove_env_vars) + + if remove_all_env_vars: + container_def["env"] = [] + + if startup_command is not None: + if isinstance(startup_command, list) and not startup_command: + container_def["command"] = None + else: + container_def["command"] = startup_command + if args is not None: + if isinstance(args, list) and not args: + container_def["args"] = None + else: + container_def["args"] = args + if resources_def is not None: + container_def["resources"] = resources_def + + containerapp_def["properties"]["template"]["containers"].append(container_def) + + # Scale + if update_map["scale"]: + 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 update 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") + + try: + return ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except CLIError as e: + handle_raw_exception(e) + + +def list_containerapp(cmd, resource_group_name=None): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + containerapps = [] + if resource_group_name is None: + containerapps = ContainerAppClient.list_by_subscription(cmd=cmd) + else: + containerapps = ContainerAppClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) + + return containerapps + except CLIError as e: + handle_raw_exception(e) + + +def delete_containerapp(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + except CLIError as e: + handle_raw_exception(e) + + +def create_managed_environment(cmd, + name, + resource_group_name, + logs_customer_id=None, + logs_key=None, + location=None, + instrumentation_key=None, + infrastructure_subnet_resource_id=None, + docker_bridge_cidr=None, + platform_reserved_cidr=None, + platform_reserved_dns_ip=None, + internal_only=False, + tags=None, + no_wait=False): + + location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) + + _validate_subscription_registered(cmd, "Microsoft.App") + _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") + + if logs_customer_id is None or logs_key is None: + logs_customer_id, logs_key = _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, location, resource_group_name) + + log_analytics_config_def = LogAnalyticsConfigurationModel + log_analytics_config_def["customerId"] = logs_customer_id + log_analytics_config_def["sharedKey"] = logs_key + + app_logs_config_def = AppLogsConfigurationModel + app_logs_config_def["destination"] = "log-analytics" + app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def + + managed_env_def = ManagedEnvironmentModel + managed_env_def["location"] = location + managed_env_def["properties"]["internalLoadBalancerEnabled"] = False + managed_env_def["properties"]["appLogsConfiguration"] = app_logs_config_def + managed_env_def["tags"] = tags + + if instrumentation_key is not None: + managed_env_def["properties"]["daprAIInstrumentationKey"] = instrumentation_key + + if infrastructure_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: + vnet_config_def = VnetConfigurationModel + + if infrastructure_subnet_resource_id is not None: + vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id + + if docker_bridge_cidr is not None: + vnet_config_def["dockerBridgeCidr"] = docker_bridge_cidr + + if platform_reserved_cidr is not None: + vnet_config_def["platformReservedCidr"] = platform_reserved_cidr + + if platform_reserved_dns_ip is not None: + vnet_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip + + managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def + + if internal_only: + if not infrastructure_subnet_resource_id: + raise ValidationError('Infrastructure subnet resource ID needs to be supplied for internal only environments.') + managed_env_def["properties"]["internalLoadBalancerEnabled"] = True + + try: + r = ManagedEnvironmentClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_envelope=managed_env_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 environment creation in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) + + logger.warning("\nContainer Apps environment created. To deploy a container app, use: az containerapp create --help\n") + + return r + except Exception as e: + handle_raw_exception(e) + + +def update_managed_environment(cmd, + name, + resource_group_name, + tags=None, + no_wait=False): + raise CLIInternalError('Containerapp env update is not yet supported.') + + +def show_managed_environment(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + return ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except CLIError as e: + handle_raw_exception(e) + + +def list_managed_environments(cmd, resource_group_name=None): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + managed_envs = [] + if resource_group_name is None: + managed_envs = ManagedEnvironmentClient.list_by_subscription(cmd=cmd) + else: + managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) + + return managed_envs + except CLIError as e: + handle_raw_exception(e) + + +def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + return ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + except CLIError as e: + handle_raw_exception(e) + + +def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + # if no identities, then assign system by default + if not identities: + identities = ['[system]'] + logger.warning('Identities not specified. Assigning managed system identity.') + + identities = [x.lower() for x in identities] + assign_system_identity = '[system]' in identities + assign_user_identities = [x for x in identities if x != '[system]'] + + containerapp_def = None + + # Get containerapp properties of CA we are updating + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + # If identity not returned + try: + containerapp_def["identity"] + containerapp_def["identity"]["type"] + except: + containerapp_def["identity"] = {} + containerapp_def["identity"]["type"] = "None" + + if assign_system_identity and containerapp_def["identity"]["type"].__contains__("SystemAssigned"): + logger.warning("System identity is already assigned to containerapp") + + # Assign correct type + try: + if containerapp_def["identity"]["type"] != "None": + if containerapp_def["identity"]["type"] == "SystemAssigned" and assign_user_identities: + containerapp_def["identity"]["type"] = "SystemAssigned,UserAssigned" + if containerapp_def["identity"]["type"] == "UserAssigned" and assign_system_identity: + containerapp_def["identity"]["type"] = "SystemAssigned,UserAssigned" + else: + if assign_system_identity and assign_user_identities: + containerapp_def["identity"]["type"] = "SystemAssigned,UserAssigned" + elif assign_system_identity: + containerapp_def["identity"]["type"] = "SystemAssigned" + elif assign_user_identities: + containerapp_def["identity"]["type"] = "UserAssigned" + except: + # Always returns "type": "None" when CA has no previous identities + pass + + if assign_user_identities: + try: + containerapp_def["identity"]["userAssignedIdentities"] + except: + containerapp_def["identity"]["userAssignedIdentities"] = {} + + subscription_id = get_subscription_id(cmd.cli_ctx) + + for r in assign_user_identities: + old_id = r + r = _ensure_identity_resource_id(subscription_id, resource_group_name, r).replace("resourceGroup", "resourcegroup") + try: + containerapp_def["identity"]["userAssignedIdentities"][r] + logger.warning("User identity {} is already assigned to containerapp".format(old_id)) + except: + containerapp_def["identity"]["userAssignedIdentities"][r] = {} + + 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 identity is not returned, do nothing + return r["identity"] + + except Exception as e: + handle_raw_exception(e) + + +def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + identities = [x.lower() for x in identities] + remove_system_identity = '[system]' in identities + remove_user_identities = [x for x in identities if x != '[system]'] + remove_id_size = len(remove_user_identities) + + # Remove duplicate identities that are passed and notify + remove_user_identities = list(set(remove_user_identities)) + if remove_id_size != len(remove_user_identities): + logger.warning("At least one identity was passed twice.") + + containerapp_def = None + # Get containerapp properties of CA we are updating + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + # If identity not returned + try: + containerapp_def["identity"] + containerapp_def["identity"]["type"] + except: + containerapp_def["identity"] = {} + containerapp_def["identity"]["type"] = "None" + + if containerapp_def["identity"]["type"] == "None": + raise InvalidArgumentValueError("The containerapp {} has no system or user assigned identities.".format(name)) + + if remove_system_identity: + if containerapp_def["identity"]["type"] == "UserAssigned": + raise InvalidArgumentValueError("The containerapp {} has no system assigned identities.".format(name)) + containerapp_def["identity"]["type"] = ("None" if containerapp_def["identity"]["type"] == "SystemAssigned" else "UserAssigned") + + if remove_user_identities: + subscription_id = get_subscription_id(cmd.cli_ctx) + try: + containerapp_def["identity"]["userAssignedIdentities"] + except: + containerapp_def["identity"]["userAssignedIdentities"] = {} + for remove_id in remove_user_identities: + given_id = remove_id + remove_id = _ensure_identity_resource_id(subscription_id, resource_group_name, remove_id) + wasRemoved = False + + for old_user_identity in containerapp_def["identity"]["userAssignedIdentities"]: + if old_user_identity.lower() == remove_id.lower(): + containerapp_def["identity"]["userAssignedIdentities"].pop(old_user_identity) + wasRemoved = True + break + + if not wasRemoved: + raise InvalidArgumentValueError("The containerapp does not have specified user identity '{}' assigned, so it cannot be removed.".format(given_id)) + + if containerapp_def["identity"]["userAssignedIdentities"] == {}: + containerapp_def["identity"]["userAssignedIdentities"] = None + containerapp_def["identity"]["type"] = ("None" if containerapp_def["identity"]["type"] == "UserAssigned" else "SystemAssigned") + + 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) + return r["identity"] + except Exception as e: + handle_raw_exception(e) + + +def show_managed_identity(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + r = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except CLIError as e: + handle_raw_exception(e) + + try: + return r["identity"] + except: + r["identity"] = {} + r["identity"]["type"] = "None" + return r["identity"] + + +def create_or_update_github_action(cmd, + name, + resource_group_name, + repo_url, + registry_url=None, + registry_username=None, + registry_password=None, + branch=None, + token=None, + login_with_github=False, + docker_file_path=None, + service_principal_client_id=None, + service_principal_client_secret=None, + service_principal_tenant_id=None): + if not token and not login_with_github: + raise_missing_token_suggestion() + elif not token: + scopes = ["admin:repo_hook", "repo", "workflow"] + token = get_github_access_token(cmd, scopes) + elif token and login_with_github: + logger.warning("Both token and --login-with-github flag are provided. Will use provided token") + + try: + # Verify github repo + from github import Github, GithubException + from github.GithubException import BadCredentialsException + + repo = None + repo = repo_url.split('/') + if len(repo) >= 2: + repo = '/'.join(repo[-2:]) + + if repo: + g = Github(token) + github_repo = None + try: + github_repo = g.get_repo(repo) + if not github_repo.permissions.push or not github_repo.permissions.maintain: + raise ValidationError("The token does not have appropriate access rights to repository {}.".format(repo)) + try: + github_repo.get_branch(branch=branch) + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIInternalError(error_msg) from e + logger.warning('Verified GitHub repo and branch') + except BadCredentialsException as e: + raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " + "the --token argument. Run 'az webapp deployment github-actions add --help' " + "for more information.") from e + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} repo".format(repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIInternalError(error_msg) from e + except CLIError as clierror: + raise clierror + except Exception: + # If exception due to github package missing, etc just continue without validating the repo and rely on api validation + pass + + source_control_info = None + + try: + source_control_info = GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + + except Exception as ex: + if not service_principal_client_id or not service_principal_client_secret or not service_principal_tenant_id: + raise RequiredArgumentMissingError('Service principal client ID, secret and tenant ID are required to add github actions for the first time. Please create one using the command \"az ad sp create-for-rbac --name {{name}} --role contributor --scopes /subscriptions/{{subscription}}/resourceGroups/{{resourceGroup}} --sdk-auth\"') from ex + source_control_info = SourceControlModel + + source_control_info["properties"]["repoUrl"] = repo_url + + if branch: + source_control_info["properties"]["branch"] = branch + if not source_control_info["properties"]["branch"]: + source_control_info["properties"]["branch"] = "main" + + azure_credentials = None + + if service_principal_client_id or service_principal_client_secret or service_principal_tenant_id: + azure_credentials = AzureCredentialsModel + azure_credentials["clientId"] = service_principal_client_id + azure_credentials["clientSecret"] = service_principal_client_secret + azure_credentials["tenantId"] = service_principal_tenant_id + azure_credentials["subscriptionId"] = get_subscription_id(cmd.cli_ctx) + + # Registry + 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') + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(registry_url) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + + try: + registry_username, registry_password = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + + registry_info = RegistryInfoModel + registry_info["registryUrl"] = registry_url + registry_info["registryUserName"] = registry_username + registry_info["registryPassword"] = registry_password + + github_action_configuration = GitHubActionConfiguration + github_action_configuration["registryInfo"] = registry_info + github_action_configuration["azureCredentials"] = azure_credentials + github_action_configuration["dockerfilePath"] = docker_file_path + + source_control_info["properties"]["githubActionConfiguration"] = github_action_configuration + + headers = ["x-ms-github-auxiliary={}".format(token)] + + try: + r = GitHubActionClient.create_or_update(cmd=cmd, resource_group_name=resource_group_name, name=name, github_action_envelope=source_control_info, headers=headers) + return r + except Exception as e: + handle_raw_exception(e) + + +def show_github_action(cmd, name, resource_group_name): + try: + return GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except Exception as e: + handle_raw_exception(e) + + +def delete_github_action(cmd, name, resource_group_name, token=None, login_with_github=False): + # Check if there is an existing source control to delete + try: + github_action_config = GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except Exception as e: + handle_raw_exception(e) + + repo_url = github_action_config["properties"]["repoUrl"] + + if not token and not login_with_github: + raise_missing_token_suggestion() + elif not token: + scopes = ["admin:repo_hook", "repo", "workflow"] + token = get_github_access_token(cmd, scopes) + elif token and login_with_github: + logger.warning("Both token and --login-with-github flag are provided. Will use provided token") + + # Check if PAT can access repo + try: + # Verify github repo + from github import Github, GithubException + from github.GithubException import BadCredentialsException + + repo = None + repo = repo_url.split('/') + if len(repo) >= 2: + repo = '/'.join(repo[-2:]) + + if repo: + g = Github(token) + github_repo = None + try: + github_repo = g.get_repo(repo) + if not github_repo.permissions.push or not github_repo.permissions.maintain: + raise ValidationError("The token does not have appropriate access rights to repository {}.".format(repo)) + except BadCredentialsException as e: + raise CLIInternalError("Could not authenticate to the repository. Please create a Personal Access Token and use " + "the --token argument. Run 'az webapp deployment github-actions add --help' " + "for more information.") from e + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} repo".format(repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIInternalError(error_msg) from e + except CLIError as clierror: + raise clierror + except Exception: + # If exception due to github package missing, etc just continue without validating the repo and rely on api validation + pass + + headers = ["x-ms-github-auxiliary={}".format(token)] + + try: + return GitHubActionClient.delete(cmd=cmd, resource_group_name=resource_group_name, name=name, headers=headers) + except Exception as e: + handle_raw_exception(e) + + +def list_revisions(cmd, name, resource_group_name): + try: + return ContainerAppClient.list_revisions(cmd=cmd, resource_group_name=resource_group_name, name=name) + except CLIError as e: + handle_raw_exception(e) + + +def show_revision(cmd, resource_group_name, revision_name, name=None): + if not name: + name = _get_app_from_revision(revision_name) + + try: + return ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) + except CLIError as e: + handle_raw_exception(e) + + +def restart_revision(cmd, resource_group_name, revision_name, name=None): + if not name: + name = _get_app_from_revision(revision_name) + + try: + return ContainerAppClient.restart_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) + except CLIError as e: + handle_raw_exception(e) + + +def activate_revision(cmd, resource_group_name, revision_name, name=None): + if not name: + name = _get_app_from_revision(revision_name) + + try: + return ContainerAppClient.activate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) + except CLIError as e: + handle_raw_exception(e) + + +def deactivate_revision(cmd, resource_group_name, revision_name, name=None): + if not name: + name = _get_app_from_revision(revision_name) + + try: + return ContainerAppClient.deactivate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) + except CLIError as e: + handle_raw_exception(e) + + +def copy_revision(cmd, + resource_group_name, + from_revision=None, + # label=None, + name=None, + yaml=None, + image=None, + container_name=None, + min_replicas=None, + max_replicas=None, + set_env_vars=None, + replace_env_vars=None, + remove_env_vars=None, + remove_all_env_vars=False, + 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 name and not from_revision: + raise RequiredArgumentMissingError('Usage error: --name is required if not using --from-revision.') + + if not name: + name = _get_app_from_revision(from_revision) + + if yaml: + if image or min_replicas or max_replicas or\ + set_env_vars or replace_env_vars or remove_env_vars or \ + remove_all_env_vars or cpu or memory 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, from_revision=from_revision, no_wait=no_wait) + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + if from_revision: + try: + r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) + except CLIError as e: + # Error handle the case where revision not found? + handle_raw_exception(e) + + _update_revision_env_secretrefs(r["properties"]["template"]["containers"], name) + 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['scale'] = min_replicas or max_replicas + update_map['container'] = image or container_name or set_env_vars or replace_env_vars or remove_env_vars or remove_all_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) + + if revision_suffix is not None: + containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix + + # Containers + if update_map["container"]: + if not container_name: + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + 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() == container_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + + if set_env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + # env vars + _add_or_update_env_vars(c["env"], parse_env_var_flags(set_env_vars), is_add=True) + + if replace_env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + # env vars + _add_or_update_env_vars(c["env"], parse_env_var_flags(replace_env_vars)) + + if remove_env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + # env vars + _remove_env_vars(c["env"], remove_env_vars) + + if remove_all_env_vars: + c["env"] = [] + + if startup_command is not None: + if isinstance(startup_command, list) and not startup_command: + c["command"] = None + else: + c["command"] = startup_command + if args is not None: + if isinstance(args, list) and not args: + c["args"] = None + else: + c["args"] = args + if cpu is not None or memory is not None: + if "resources" in c and c["resources"]: + if cpu is not None: + c["resources"]["cpu"] = cpu + if memory is not None: + c["resources"]["memory"] = memory + else: + c["resources"] = { + "cpu": cpu, + "memory": memory + } + + # If not updating existing container, add as new container + if not updating_existing_container: + if image is None: + raise ValidationError("Usage error: --image is required when adding a new container") + + resources_def = None + if cpu is not None or memory is not None: + resources_def = ContainerResourcesModel + resources_def["cpu"] = cpu + resources_def["memory"] = memory + + container_def = ContainerModel + container_def["name"] = container_name + container_def["image"] = image + + if set_env_vars is not None: + # env vars + _add_or_update_env_vars(container_def["env"], parse_env_var_flags(set_env_vars), is_add=True) + + if replace_env_vars is not None: + # env vars + _add_or_update_env_vars(container_def["env"], parse_env_var_flags(replace_env_vars)) + + if remove_env_vars is not None: + # env vars + _remove_env_vars(container_def["env"], remove_env_vars) + + if remove_all_env_vars: + container_def["env"] = [] + + if startup_command is not None: + if isinstance(startup_command, list) and not startup_command: + container_def["command"] = None + else: + container_def["command"] = startup_command + if args is not None: + if isinstance(args, list) and not args: + container_def["args"] = None + else: + container_def["args"] = args + if resources_def is not None: + container_def["resources"] = resources_def + + containerapp_def["properties"]["template"]["containers"].append(container_def) + + # Scale + if update_map["scale"]: + 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 update 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 set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = mode.lower() + + _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) + return r["properties"]["configuration"]["activeRevisionsMode"] + except Exception as e: + handle_raw_exception(e) + + +def show_ingress(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"] + except Exception as e: + raise ValidationError("The containerapp '{}' does not have ingress enabled.".format(name)) from e + + +def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, allow_insecure=False, no_wait=False): # pylint: disable=redefined-builtin + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + external_ingress = None + if type is not None: + if type.lower() == "internal": + external_ingress = False + elif type.lower() == "external": + external_ingress = True + + ingress_def = None + if target_port is not None and type is not None: + ingress_def = IngressModel + ingress_def["external"] = external_ingress + ingress_def["targetPort"] = target_port + ingress_def["transport"] = transport + ingress_def["allowInsecure"] = allow_insecure + + containerapp_def["properties"]["configuration"]["ingress"] = ingress_def + + _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) + logger.warning("\nIngress enabled. Access your app at https://{}/\n".format(r["properties"]["configuration"]["ingress"]["fqdn"])) + return r["properties"]["configuration"]["ingress"] + except Exception as e: + handle_raw_exception(e) + + +def disable_ingress(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + containerapp_def["properties"]["configuration"]["ingress"] = None + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + try: + ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=containerapp_def, no_wait=no_wait) + logger.warning("Ingress has been disabled successfully.") + return + except Exception as e: + handle_raw_exception(e) + + +def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["ingress"] + except Exception as e: + raise ValidationError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") from e + + if traffic_weights is not None: + _update_traffic_weights(containerapp_def, traffic_weights) + + _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) + return r["properties"]["configuration"]["ingress"]["traffic"] + except Exception as e: + handle_raw_exception(e) + + +def show_ingress_traffic(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] + except Exception as e: + raise ValidationError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") from e + + +def show_registry(cmd, name, resource_group_name, server): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["registries"] + except Exception as e: + raise ValidationError("The containerapp {} has no assigned registries.".format(name)) from e + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + for r in registries_def: + if r['server'].lower() == server.lower(): + return r + raise InvalidArgumentValueError("The containerapp {} does not have specified registry assigned.".format(name)) + + +def list_registry(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["registries"] + except Exception as e: + raise ValidationError("The containerapp {} has no assigned registries.".format(name)) from e + + +def set_registry(cmd, name, resource_group_name, server, username=None, password=None, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + registries_def = None + registry = None + + if "registries" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["registries"] = [] + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + if not username or not password: + # If registry is Azure Container Registry, we can try inferring credentials + if '.azurecr.io' not in server: + raise RequiredArgumentMissingError('Registry username and password are required if you are not using Azure Container Registry.') + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + + try: + username, password = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + + # Check if updating existing registry + updating_existing_registry = False + for r in registries_def: + if r['server'].lower() == server.lower(): + logger.warning("Updating existing registry.") + updating_existing_registry = True + if username: + r["username"] = username + if password: + r["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + r["username"], + r["server"], + password, + update_existing_secret=True) + + # If not updating existing registry, add as new registry + if not updating_existing_registry: + registry = RegistryCredentialsModel + registry["server"] = server + registry["username"] = username + registry["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + username, + server, + password, + update_existing_secret=True) + + registries_def.append(registry) + + 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) + + return r["properties"]["configuration"]["registries"] + except Exception as e: + handle_raw_exception(e) + + +def remove_registry(cmd, name, resource_group_name, server, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + registries_def = None + + try: + containerapp_def["properties"]["configuration"]["registries"] + except Exception as e: + raise ValidationError("The containerapp {} has no assigned registries.".format(name)) from e + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + wasRemoved = False + for i, value in enumerate(registries_def): + r = value + if r['server'].lower() == server.lower(): + registries_def.pop(i) + _remove_registry_secret(containerapp_def=containerapp_def, server=server, username=r["username"]) + wasRemoved = True + break + + if not wasRemoved: + raise ValidationError("Containerapp does not have registry server {} assigned.".format(server)) + + if len(containerapp_def["properties"]["configuration"]["registries"]) == 0: + containerapp_def["properties"]["configuration"].pop("registries") + + 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) + logger.warning("Registry successfully removed.") + return r["properties"]["configuration"]["registries"] + # No registries to return, so return nothing + except Exception: + pass + + +def list_secrets(cmd, name, resource_group_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + try: + return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] + except Exception as e: + raise ValidationError("The containerapp {} has no assigned secrets.".format(name)) from e + + +def show_secret(cmd, name, resource_group_name, secret_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + r = ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) + for secret in r["value"]: + if secret["name"].lower() == secret_name.lower(): + return secret + raise ValidationError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + + +def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + for secret_name in secret_names: + wasRemoved = False + for secret in containerapp_def["properties"]["configuration"]["secrets"]: + if secret["name"].lower() == secret_name.lower(): + _remove_secret(containerapp_def, secret_name=secret["name"]) + wasRemoved = True + break + if not wasRemoved: + raise ValidationError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + 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) + logger.warning("Secret(s) successfully removed.") + try: + return r["properties"]["configuration"]["secrets"] + # No secrets to return + except: + pass + except Exception as e: + handle_raw_exception(e) + + +def set_secrets(cmd, name, resource_group_name, secrets, + # yaml=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + # if not yaml and not secrets: + # raise RequiredArgumentMissingError('Usage error: --secrets is required if not using --yaml') + + # if not secrets: + # secrets = [] + + # if yaml: + # yaml_secrets = load_yaml_file(yaml).split(' ') + # try: + # parse_secret_flags(yaml_secrets) + # except: + # raise ValidationError("YAML secrets must be a list of secrets in key=value format, delimited by new line.") + # for secret in yaml_secrets: + # secrets.append(secret.strip()) + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) + + 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) + return r["properties"]["configuration"]["secrets"] + except Exception as e: + handle_raw_exception(e) + + +def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port=None, dapr_app_protocol=None, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + if 'configuration' not in containerapp_def['properties']: + containerapp_def['properties']['configuration'] = {} + + if 'dapr' not in containerapp_def['properties']['configuration']: + containerapp_def['properties']['configuration']['dapr'] = {} + + if dapr_app_id: + containerapp_def['properties']['configuration']['dapr']['appId'] = dapr_app_id + + if dapr_app_port: + containerapp_def['properties']['configuration']['dapr']['appPort'] = dapr_app_port + + if dapr_app_protocol: + containerapp_def['properties']['configuration']['dapr']['appProtocol'] = dapr_app_protocol + + containerapp_def['properties']['configuration']['dapr']['enabled'] = True + + 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) + return r["properties"]['configuration']['dapr'] + except Exception as e: + handle_raw_exception(e) + + +def disable_dapr(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + 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 ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + if 'configuration' not in containerapp_def['properties']: + containerapp_def['properties']['configuration'] = {} + + if 'dapr' not in containerapp_def['properties']['configuration']: + containerapp_def['properties']['configuration']['dapr'] = {} + + containerapp_def['properties']['configuration']['dapr']['enabled'] = False + + 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) + return r["properties"]['configuration']['dapr'] + except Exception as e: + handle_raw_exception(e) + + +def list_dapr_components(cmd, resource_group_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + return DaprComponentClient.list(cmd, resource_group_name, environment_name) + + +def show_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + return DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) + + +def create_or_update_dapr_component(cmd, resource_group_name, environment_name, dapr_component_name, yaml): + _validate_subscription_registered(cmd, "Microsoft.App") + + yaml_containerapp = load_yaml_file(yaml) + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck + raise ValidationError('Invalid YAML provided. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') + + # Deserialize the yaml into a DaprComponent object. Need this since we're not using SDK + daprcomponent_def = None + try: + deserializer = create_deserializer() + + daprcomponent_def = deserializer('DaprComponent', yaml_containerapp) + except DeserializationError as ex: + raise ValidationError('Invalid YAML provided. Please see https://aka.ms/azure-container-apps-yaml for a valid containerapps YAML spec.') from ex + + daprcomponent_def = _convert_object_from_snake_to_camel_case(_object_to_dict(daprcomponent_def)) + + # Remove "additionalProperties" and read-only attributes that are introduced in the deserialization. Need this since we're not using SDK + _remove_additional_attributes(daprcomponent_def) + _remove_dapr_readonly_attributes(daprcomponent_def) + + if not daprcomponent_def["ignoreErrors"]: + daprcomponent_def["ignoreErrors"] = False + + dapr_component_envelope = {} + + dapr_component_envelope["properties"] = daprcomponent_def + + try: + r = DaprComponentClient.create_or_update(cmd, resource_group_name=resource_group_name, environment_name=environment_name, dapr_component_envelope=dapr_component_envelope, name=dapr_component_name) + return r + except Exception as e: + handle_raw_exception(e) + + +def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environment_name): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) + except Exception as e: + raise ResourceNotFoundError("Dapr component not found.") from e + + try: + r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) + logger.warning("Dapr componenet successfully deleted.") + return r + except Exception as e: + handle_raw_exception(e) diff --git a/src/containerapp/azext_containerapp/tests/__init__.py b/src/containerapp/azext_containerapp/tests/__init__.py new file mode 100644 index 00000000000..2dcf9bb68b3 --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/tests/latest/__init__.py b/src/containerapp/azext_containerapp/tests/latest/__init__.py new file mode 100644 index 00000000000..2dcf9bb68b3 --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/latest/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py new file mode 100644 index 00000000000..9a89dcc55c9 --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import time +import unittest + +from azure.cli.testsdk.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, JMESPathCheck) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +@unittest.skip("Managed environment flaky") +class ContainerappScenarioTest(ScenarioTest): + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="centraluseuap") + def test_containerapp_e2e(self, resource_group): + containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) + env_name = self.create_random_name(prefix='containerapp-e2e-env', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + # Sleep in case env create takes a while + time.sleep(60) + self.cmd('containerapp env list -g {}'.format(resource_group), checks=[ + JMESPathCheck('length(@)', 1), + JMESPathCheck('[0].name', env_name), + ]) + + self.cmd('containerapp create -g {} -n {} --environment {}'.format(resource_group, containerapp_name, env_name), checks=[ + JMESPathCheck('name', containerapp_name) + ]) + + # Sleep in case containerapp create takes a while + time.sleep(60) + self.cmd('containerapp show -g {} -n {}'.format(resource_group, containerapp_name), checks=[ + JMESPathCheck('name', containerapp_name) + ]) diff --git a/src/containerapp/setup.cfg b/src/containerapp/setup.cfg new file mode 100644 index 00000000000..3c6e79cf31d --- /dev/null +++ b/src/containerapp/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py new file mode 100644 index 00000000000..e23b0011367 --- /dev/null +++ b/src/containerapp/setup.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from codecs import open +from setuptools import setup, find_packages +try: + from azure_bdist_wheel import cmdclass +except ImportError: + from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") + +# TODO: Confirm this is the right version number you want and it matches your +# HISTORY.rst entry. +VERSION = '0.1.0' + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'License :: OSI Approved :: MIT License', +] + +# TODO: Add any additional SDK dependencies here +DEPENDENCIES = [ + 'azure-cli-core' +] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='containerapp', + version=VERSION, + description='Microsoft Azure Command-Line Tools Containerapp Extension', + # TODO: Update author and email, if applicable + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + # TODO: consider pointing directly to your source code instead of the generic repo + url='https://github.com/Azure/azure-cli-extensions', + long_description=README + '\n\n' + HISTORY, + license='MIT', + classifiers=CLASSIFIERS, + packages=find_packages(), + install_requires=DEPENDENCIES, + package_data={'azext_containerapp': ['azext_metadata.json']}, +) diff --git a/src/service_name.json b/src/service_name.json index e8ab509ca95..20147066339 100644 --- a/src/service_name.json +++ b/src/service_name.json @@ -99,6 +99,11 @@ "AzureServiceName": "Azure Arc", "URL": "https://docs.microsoft.com/azure/azure-arc/servers/overview" }, + { + "Command": "az containerapp", + "AzureServiceName": "Azure Container Apps", + "URL": "https://docs.microsoft.com/en-us/azure/container-apps/" + }, { "Command": "az costmanagement", "AzureServiceName": "Azure Cost Management + Billing",