From a8eb7d7113033603505a1902fde3183fa368186a Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 2 Feb 2022 20:27:02 -0800 Subject: [PATCH 001/177] Skeleton code --- .github/CODEOWNERS | 4 +- src/containerapp/HISTORY.rst | 8 +++ src/containerapp/README.rst | 5 ++ .../azext_containerapp/__init__.py | 32 ++++++++++ .../azext_containerapp/_client_factory.py | 12 ++++ src/containerapp/azext_containerapp/_help.py | 38 ++++++++++++ .../azext_containerapp/_params.py | 23 +++++++ .../azext_containerapp/_validators.py | 20 +++++++ .../azext_containerapp/azext_metadata.json | 5 ++ .../azext_containerapp/commands.py | 29 +++++++++ src/containerapp/azext_containerapp/custom.py | 20 +++++++ .../azext_containerapp/tests/__init__.py | 5 ++ .../tests/latest/__init__.py | 5 ++ .../latest/test_containerapp_scenario.py | 17 ++++++ src/containerapp/setup.cfg | 2 + src/containerapp/setup.py | 60 +++++++++++++++++++ 16 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/containerapp/HISTORY.rst create mode 100644 src/containerapp/README.rst create mode 100644 src/containerapp/azext_containerapp/__init__.py create mode 100644 src/containerapp/azext_containerapp/_client_factory.py create mode 100644 src/containerapp/azext_containerapp/_help.py create mode 100644 src/containerapp/azext_containerapp/_params.py create mode 100644 src/containerapp/azext_containerapp/_validators.py create mode 100644 src/containerapp/azext_containerapp/azext_metadata.json create mode 100644 src/containerapp/azext_containerapp/commands.py create mode 100644 src/containerapp/azext_containerapp/custom.py create mode 100644 src/containerapp/azext_containerapp/tests/__init__.py create mode 100644 src/containerapp/azext_containerapp/tests/latest/__init__.py create mode 100644 src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py create mode 100644 src/containerapp/setup.cfg create mode 100644 src/containerapp/setup.py 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/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst new file mode 100644 index 00000000000..8c34bccfff8 --- /dev/null +++ b/src/containerapp/HISTORY.rst @@ -0,0 +1,8 @@ +.. :changelog: + +Release History +=============== + +0.1.0 +++++++ +* Initial release. \ No newline at end of file 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..e19af22d9e8 --- /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. +# -------------------------------------------------------------------------------------------- + +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 + from azext_containerapp._client_factory import cf_containerapp + containerapp_custom = CliCommandType( + operations_tmpl='azext_containerapp.custom#{}', + client_factory=cf_containerapp) + 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..842d3a16731 --- /dev/null +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +def cf_containerapp(cli_ctx, *_): + + from azure.cli.core.commands.client_factory import get_mgmt_service_client + # TODO: Replace CONTOSO with the appropriate label and uncomment + # from azure.mgmt.CONTOSO import CONTOSOManagementClient + # return get_mgmt_service_client(cli_ctx, CONTOSOManagementClient) + return None diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py new file mode 100644 index 00000000000..27af014f101 --- /dev/null +++ b/src/containerapp/azext_containerapp/_help.py @@ -0,0 +1,38 @@ +# 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: Commands to manage Containerapps. +""" + +helps['containerapp create'] = """ + type: command + short-summary: Create a Containerapp. +""" + +helps['containerapp list'] = """ + type: command + short-summary: List Containerapps. +""" + +# helps['containerapp delete'] = """ +# type: command +# short-summary: Delete a Containerapp. +# """ + +# helps['containerapp show'] = """ +# type: command +# short-summary: Show details of a Containerapp. +# """ + +# helps['containerapp update'] = """ +# type: command +# short-summary: Update a Containerapp. +# """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py new file mode 100644 index 00000000000..c732a35b7ce --- /dev/null +++ b/src/containerapp/azext_containerapp/_params.py @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------------------------- +# 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 knack.arguments import CLIArgumentType + + +def load_arguments(self, _): + + from azure.cli.core.commands.parameters import tags_type + from azure.cli.core.commands.validators import get_default_location_from_resource_group + + containerapp_name_type = CLIArgumentType(options_list='--containerapp-name-name', help='Name of the Containerapp.', id_part='name') + + with self.argument_context('containerapp') as c: + c.argument('tags', tags_type) + c.argument('location', validator=get_default_location_from_resource_group) + c.argument('containerapp_name', containerapp_name_type, options_list=['--name', '-n']) + + with self.argument_context('containerapp list') as c: + c.argument('containerapp_name', containerapp_name_type, id_part=None) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py new file mode 100644 index 00000000000..821630f5f34 --- /dev/null +++ b/src/containerapp/azext_containerapp/_validators.py @@ -0,0 +1,20 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def example_name_or_id_validator(cmd, namespace): + # Example of a storage account name or ID validator. + # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters + from azure.cli.core.commands.client_factory import get_subscription_id + from msrestazure.tools import is_valid_resource_id, resource_id + if namespace.storage_account: + if not is_valid_resource_id(namespace.RESOURCE): + namespace.storage_account = resource_id( + subscription=get_subscription_id(cmd.cli_ctx), + resource_group=namespace.resource_group_name, + namespace='Microsoft.Storage', + type='storageAccounts', + name=namespace.storage_account + ) diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json new file mode 100644 index 00000000000..c2d0f4fe8d0 --- /dev/null +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -0,0 +1,5 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "azext.maxCliCoreVersion": "2.33.0" +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py new file mode 100644 index 00000000000..07d4b120e47 --- /dev/null +++ b/src/containerapp/azext_containerapp/commands.py @@ -0,0 +1,29 @@ +# -------------------------------------------------------------------------------------------- +# 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.commands import CliCommandType +from azext_containerapp._client_factory import cf_containerapp + + +def load_command_table(self, _): + + # TODO: Add command type here + # containerapp_sdk = CliCommandType( + # operations_tmpl='.operations#None.{}', + # client_factory=cf_containerapp) + + + with self.command_group('containerapp') as g: + g.custom_command('create', 'create_containerapp') + # g.command('delete', 'delete') + g.custom_command('list', 'list_containerapp') + # g.show_command('show', 'get') + # g.generic_update_command('update', setter_name='update', custom_func_name='update_containerapp') + + + with self.command_group('containerapp', is_preview=True): + pass + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py new file mode 100644 index 00000000000..01a6a709509 --- /dev/null +++ b/src/containerapp/azext_containerapp/custom.py @@ -0,0 +1,20 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.util import CLIError + + +def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): + raise CLIError('TODO: Implement `containerapp create`') + + +def list_containerapp(cmd, resource_group_name=None): + raise CLIError('TODO: Implement `containerapp list`') + + +def update_containerapp(cmd, instance, tags=None): + with cmd.update_context(instance) as c: + c.set_param('tags', tags) + return instance \ No newline at end of file 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..f18855ca4eb --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -0,0 +1,17 @@ +# -------------------------------------------------------------------------------------------- +# 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 unittest + +from azure_devtools.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +class ContainerappScenarioTest(ScenarioTest): + pass \ No newline at end of file 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..b9f57ada671 --- /dev/null +++ b/src/containerapp/setup.py @@ -0,0 +1,60 @@ +#!/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', + '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']}, +) \ No newline at end of file From c2963ce46709431feb0a7252cea990bdf2add641 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 10:18:09 -0800 Subject: [PATCH 002/177] az containerapp env show --- .../azext_containerapp/_client_factory.py | 49 +++++++++++++++++++ .../azext_containerapp/_clients.py | 29 +++++++++++ src/containerapp/azext_containerapp/_help.py | 29 +++++------ .../azext_containerapp/_params.py | 11 ++--- .../azext_containerapp/commands.py | 15 +++--- src/containerapp/azext_containerapp/custom.py | 17 ++++--- 6 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_clients.py diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 842d3a16731..53c03131967 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -3,6 +3,55 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from knack.util import CLIError + + +# pylint: disable=inconsistent-return-statements +def ex_handler_factory(creating_plan=False, no_throw=False): + def _polish_bad_errors(ex): + import json + from knack.util import CLIError + try: + content = json.loads(ex.response.content) + if 'message' in content: + detail = content['message'] + elif 'Message' in content: + detail = content['Message'] + + if creating_plan: + if 'Requested features are not supported in region' in detail: + detail = ("Plan with linux worker is not supported in current region. For " + + "supported regions, please refer to https://docs.microsoft.com/" + "azure/app-service-web/app-service-linux-intro") + elif 'Not enough available reserved instance servers to satisfy' in detail: + detail = ("Plan with Linux worker can only be created in a group " + + "which has never contained a Windows worker, and vice versa. " + + "Please use a new resource group. Original error:" + detail) + ex = CLIError(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 CLIError('({}) {}'.format(code, message)) + raise e + + def cf_containerapp(cli_ctx, *_): from azure.cli.core.commands.client_factory import get_mgmt_service_client diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py new file mode 100644 index 00000000000..62b2fe00951 --- /dev/null +++ b/src/containerapp/azext_containerapp/_clients.py @@ -0,0 +1,29 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from sys import api_version +from azure.cli.core.util import send_raw_request +from azure.cli.core.commands.client_factory import get_subscription_id + + +API_VERSION = "2021-03-01" + + +class KubeEnvironmentClient(): + @classmethod + def show(cls, cmd, resource_group_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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() diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 27af014f101..2f77234a8f6 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -17,22 +17,17 @@ short-summary: Create a Containerapp. """ -helps['containerapp list'] = """ - type: command - short-summary: List Containerapps. +# Environment Commands +helps['containerapp env'] = """ + type: group + short-summary: Commands to manage Containerapps environments. """ -# helps['containerapp delete'] = """ -# type: command -# short-summary: Delete a Containerapp. -# """ - -# helps['containerapp show'] = """ -# type: command -# short-summary: Show details of a Containerapp. -# """ - -# helps['containerapp update'] = """ -# type: command -# short-summary: Update a Containerapp. -# """ +helps['containerapp env show'] = """ + type: command + short-summary: Show details of a Containerapp environment. + examples: + - name: Show the details of a Containerapp Environment. + text: | + az containerapp env show -n MyContainerappEnvironment -g MyResourceGroup +""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c732a35b7ce..9642e2de985 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -12,12 +12,7 @@ def load_arguments(self, _): from azure.cli.core.commands.parameters import tags_type from azure.cli.core.commands.validators import get_default_location_from_resource_group - containerapp_name_type = CLIArgumentType(options_list='--containerapp-name-name', help='Name of the Containerapp.', id_part='name') + name_type = CLIArgumentType(options_list=['--name', '-n']) - with self.argument_context('containerapp') as c: - c.argument('tags', tags_type) - c.argument('location', validator=get_default_location_from_resource_group) - c.argument('containerapp_name', containerapp_name_type, options_list=['--name', '-n']) - - with self.argument_context('containerapp list') as c: - c.argument('containerapp_name', containerapp_name_type, id_part=None) + with self.argument_context('containerapp env show') as c: + c.argument('name', name_type, help='Name of the Kubernetes Environment.') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 07d4b120e47..69bcd468a57 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -5,7 +5,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType -from azext_containerapp._client_factory import cf_containerapp +from azext_containerapp._client_factory import cf_containerapp, ex_handler_factory def load_command_table(self, _): @@ -18,12 +18,11 @@ def load_command_table(self, _): with self.command_group('containerapp') as g: g.custom_command('create', 'create_containerapp') - # g.command('delete', 'delete') - g.custom_command('list', 'list_containerapp') - # g.show_command('show', 'get') - # g.generic_update_command('update', setter_name='update', custom_func_name='update_containerapp') - with self.command_group('containerapp', is_preview=True): - pass - + with self.command_group('containerapp env') as g: + g.custom_command('show', 'show_kube_environment') + # g.custom_command('list', 'list_kube_environments') + # g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 01a6a709509..a5ab7043e76 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,18 +3,19 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.core.azclierror import (ResourceNotFoundError) from knack.util import CLIError +from ._client_factory import handle_raw_exception +from ._clients import KubeEnvironmentClient + def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): raise CLIError('TODO: Implement `containerapp create`') -def list_containerapp(cmd, resource_group_name=None): - raise CLIError('TODO: Implement `containerapp list`') - - -def update_containerapp(cmd, instance, tags=None): - with cmd.update_context(instance) as c: - c.set_param('tags', tags) - return instance \ No newline at end of file +def show_kube_environment(cmd, name, resource_group_name): + try: + return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except CLIError as e: + handle_raw_exception(e) From e46138494d657c14fe0e8d98098aefd694ee6e25 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 12:10:38 -0800 Subject: [PATCH 003/177] List kube/managed environments --- .../azext_containerapp/_clients.py | 135 ++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 14 +- .../azext_containerapp/_params.py | 2 +- .../azext_containerapp/commands.py | 5 +- src/containerapp/azext_containerapp/custom.py | 41 +++++- 5 files changed, 193 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 62b2fe00951..7332c740398 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -9,6 +9,7 @@ API_VERSION = "2021-03-01" +NEW_API_VERSION = "2022-01-01-preview" class KubeEnvironmentClient(): @@ -27,3 +28,137 @@ def show(cls, cmd, resource_group_name, name): r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() + + @classmethod + def list_by_subscription(cls, cmd, formatter=lambda x: x): + kube_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/providers/Microsoft.Web/kubeEnvironments?api-version={}".format( + management_hostname.strip('/'), + sub_id, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list + + @classmethod + def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): + kube_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments?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 kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list + + +class ManagedEnvironmentClient(): + @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): + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list + + @classmethod + def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 2f77234a8f6..52469aef296 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -30,4 +30,16 @@ - name: Show the details of a Containerapp Environment. text: | az containerapp env show -n MyContainerappEnvironment -g MyResourceGroup -""" \ No newline at end of file +""" + +helps['containerapp env list'] = """ + type: command + short-summary: List Containerapp environments by subscription or resource group. + examples: + - name: List Containerapp Environments by subscription. + text: | + az containerapp env list + - name: List Containerapp Environments by resource group. + text: | + az containerapp env list -g MyResourceGroup +""" diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 9642e2de985..545da01b7de 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -15,4 +15,4 @@ def load_arguments(self, _): name_type = CLIArgumentType(options_list=['--name', '-n']) with self.argument_context('containerapp env show') as c: - c.argument('name', name_type, help='Name of the Kubernetes Environment.') \ No newline at end of file + c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 69bcd468a57..bf81094d722 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -22,7 +22,10 @@ def load_command_table(self, _): with self.command_group('containerapp env') as g: g.custom_command('show', 'show_kube_environment') - # g.custom_command('list', 'list_kube_environments') + # g.custom_command('show', 'show_managed_environment') + g.custom_command('list', 'list_kube_environments') + # g.custom_command('list', 'list_managed_environments') + # g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a5ab7043e76..2eaa63c5ce7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -7,7 +7,7 @@ from knack.util import CLIError from ._client_factory import handle_raw_exception -from ._clients import KubeEnvironmentClient +from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): @@ -19,3 +19,42 @@ def show_kube_environment(cmd, name, resource_group_name): return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) except CLIError as e: handle_raw_exception(e) + + +def list_kube_environments(cmd, resource_group_name=None): + try: + kube_envs = [] + if resource_group_name is None: + kube_envs = KubeEnvironmentClient.list_by_subscription(cmd=cmd) + else: + kube_envs = KubeEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) + + return [e for e in kube_envs if "properties" in e and + "environmentType" in e["properties"] and + e["properties"]["environmentType"] and + e["properties"]["environmentType"].lower() == "managed"] + except CLIError as e: + handle_raw_exception(e) + + +def show_managed_environment(cmd, name, resource_group_name): + 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): + 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 [e for e in managed_envs if "properties" in e and + "environmentType" in e["properties"] and + e["properties"]["environmentType"] and + e["properties"]["environmentType"].lower() == "managed"] + except CLIError as e: + handle_raw_exception(e) From b5e938187d6e614727d260ff2c99f20c1e7fb872 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 19:21:31 -0800 Subject: [PATCH 004/177] Create kube environment, wait doesn't work yet --- .../azext_containerapp/_client_factory.py | 18 ++++ .../azext_containerapp/_clients.py | 18 ++++ src/containerapp/azext_containerapp/_help.py | 12 +++ .../azext_containerapp/_models.py | 45 ++++++++++ .../azext_containerapp/_params.py | 30 ++++++- src/containerapp/azext_containerapp/_utils.py | 54 ++++++++++++ .../azext_containerapp/commands.py | 3 +- src/containerapp/azext_containerapp/custom.py | 85 ++++++++++++++++++- 8 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_models.py create mode 100644 src/containerapp/azext_containerapp/_utils.py diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 53c03131967..4c8eeeb7f86 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -3,6 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.core.commands.client_factory import get_mgmt_service_client +from azure.cli.core.profiles import ResourceType + from knack.util import CLIError @@ -40,18 +43,33 @@ 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 CLIError('({}) {}'.format(code, message)) + elif "Message" in jsonError: + message = jsonError["Message"] + raise CLIError(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 cf_containerapp(cli_ctx, *_): from azure.cli.core.commands.client_factory import get_mgmt_service_client diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 7332c740398..4a2577a9d1c 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import json + from sys import api_version from azure.cli.core.util import send_raw_request from azure.cli.core.commands.client_factory import get_subscription_id @@ -13,6 +15,22 @@ class KubeEnvironmentClient(): + @classmethod + def create(cls, cmd, resource_group_name, name, kube_environment_envelope): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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(kube_environment_envelope)) + return r.json() + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 52469aef296..f0e33a7c83a 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -23,6 +23,18 @@ short-summary: Commands to manage Containerapps environments. """ +helps['containerapp env create'] = """ + type: command + short-summary: Create a Containerapp environment. + examples: + - name: Create a Containerapp Environment. + text: | + az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ + --logs-workspace-id myLogsWorkspaceID \\ + --logs-workspace-key myLogsWorkspaceKey \\ + --location Canada Central +""" + helps['containerapp env show'] = """ type: command short-summary: Show details of a Containerapp environment. diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py new file mode 100644 index 00000000000..71242502bdc --- /dev/null +++ b/src/containerapp/azext_containerapp/_models.py @@ -0,0 +1,45 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +ContainerAppsConfiguration = { + "daprAIInstrumentationKey": None, + "appSubnetResourceId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIP": None, + "internalOnly": False +} + +KubeEnvironment = { + "id": None, # readonly + "name": None, # readonly + "kind": None, + "location": None, + "tags": None, + "properties": { + "type": None, + "environmentType": None, + "containerAppsConfiguration": None, + "provisioningState": None, # readonly + "deploymentErrors": None, # readonly + "defaultDomain": None, # readonly + "staticIp": None, + "arcConfiguration": None, + "appLogsConfiguration": None, + "aksResourceId": None + }, + "extendedLocation": None +} + +AppLogsConfiguration = { + "destination": None, + "logAnalyticsConfiguration": None +} + +LogAnalyticsConfiguration = { + "customerId": None, + "sharedKey": None +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 545da01b7de..0c35090f82e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -6,13 +6,37 @@ from knack.arguments import CLIArgumentType +from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, + get_resource_name_completion_list, + get_three_state_flag, get_enum_type, tags_type) +from azure.cli.core.commands.validators import get_default_location_from_resource_group -def load_arguments(self, _): - from azure.cli.core.commands.parameters import tags_type - from azure.cli.core.commands.validators import get_default_location_from_resource_group +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') + 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 env') as c: + c.argument('name', name_type) + 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('logs_destination', options_list=['--logs-dest']) + c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') + c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') + c.argument('instrumentation_key', options_list=['--instrumentation-key']) + c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') + c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') + c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py new file mode 100644 index 00000000000..6b0a92b4914 --- /dev/null +++ b/src/containerapp/azext_containerapp/_utils.py @@ -0,0 +1,54 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.commands.client_factory import get_subscription_id + +from ._client_factory import providers_client_factory, cf_resource_groups + + +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): + providers_client = None + try: + providers_client = providers_client_factory(cmd.cli_ctx, get_subscription_id(cmd.cli_ctx)) + 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( + resource_provider, resource_provider)) + except ValidationError as ex: + raise ex + except Exception: + pass + + +def _ensure_location_allowed(cmd, location, resource_provider): + 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', "") == 'containerApps': + res_locations = getattr(res, 'locations', []) + + res_locations = [res_loc.lower().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=='containerApps'].locations'`".format( + location, resource_provider)) + except ValidationError as ex: + raise ex + except Exception: + pass diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index bf81094d722..be632e0a997 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -25,7 +25,8 @@ def load_command_table(self, _): # g.custom_command('show', 'show_managed_environment') g.custom_command('list', 'list_kube_environments') # g.custom_command('list', 'list_managed_environments') + g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2eaa63c5ce7..2e2bf3f467f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,17 +3,96 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azure.cli.core.azclierror import (ResourceNotFoundError) +from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient +from ._models import KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed - -def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): +def create_containerapp(cmd, resource_group_name, name, location=None, tags=None): raise CLIError('TODO: Implement `containerapp create`') +def create_kube_environment(cmd, + name, + resource_group_name, + logs_customer_id, + logs_key, + logs_destination="log-analytics", + location=None, + instrumentation_key=None, + controlplane_subnet_resource_id=None, + app_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.Web") + _ensure_location_allowed(cmd, location, "Microsoft.Web") + + containerapps_config_def = ContainerAppsConfiguration + + if instrumentation_key is not None: + containerapps_config_def["daprAIInstrumentationKey"] = instrumentation_key + + if controlplane_subnet_resource_id is not None: + if not app_subnet_resource_id: + raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') + containerapps_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id + + if app_subnet_resource_id is not None: + if not controlplane_subnet_resource_id: + raise ValidationError('Controlplane subnet resource ID needs to be supplied with app subnet resource ID.') + containerapps_config_def["appSubnetResourceId"] = app_subnet_resource_id + + if docker_bridge_cidr is not None: + containerapps_config_def["dockerBridgeCidr"] = docker_bridge_cidr + + if platform_reserved_cidr is not None: + containerapps_config_def["platformReservedCidr"] = platform_reserved_cidr + + if platform_reserved_dns_ip is not None: + containerapps_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip + + if internal_only: + if not controlplane_subnet_resource_id or not app_subnet_resource_id: + raise ValidationError('Controlplane subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') + containerapps_config_def["internalOnly"] = True + + log_analytics_config_def = LogAnalyticsConfiguration + log_analytics_config_def["customerId"] = logs_customer_id + log_analytics_config_def["sharedKey"] = logs_key + + app_logs_config_def = AppLogsConfiguration + app_logs_config_def["destination"] = logs_destination + app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def + + kube_def = KubeEnvironment + kube_def["location"] = location + kube_def["properties"]["internalLoadBalancerEnabled"] = False + kube_def["properties"]["environmentType"] = "managed" + kube_def["properties"]["type"] = "managed" + kube_def["properties"]["appLogsConfiguration"] = app_logs_config_def + kube_def["properties"]["containerAppsConfiguration"] = containerapps_config_def + kube_def["tags"] = tags + + try: + return sdk_no_wait(no_wait, KubeEnvironmentClient.create, + cmd=cmd, resource_group_name=resource_group_name, + name=name, kube_environment_envelope=kube_def) + except Exception as e: + handle_raw_exception(e) + + def show_kube_environment(cmd, name, resource_group_name): try: return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) From 23ca42fae774409055c1c74bf6c23353245adba6 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 20:55:52 -0800 Subject: [PATCH 005/177] Update containerapp stubs (check if it is supported now) --- src/containerapp/azext_containerapp/_help.py | 5 +++++ src/containerapp/azext_containerapp/_params.py | 4 ++++ src/containerapp/azext_containerapp/commands.py | 3 ++- src/containerapp/azext_containerapp/custom.py | 8 ++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index f0e33a7c83a..62e7cd7740c 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -35,6 +35,11 @@ --location Canada Central """ +helps['containerapp env update'] = """ + type: command + short-summary: Update a Containerapp environment. Currently Unsupported. +""" + helps['containerapp env show'] = """ type: command short-summary: Show details of a Containerapp environment. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 0c35090f82e..cc9dece1784 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -38,5 +38,9 @@ def load_arguments(self, _): c.argument('internal_only', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env update') as c: + c.argument('name', name_type, help='Name of the kubernetes environment.') + c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index be632e0a997..44b72bd037d 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -27,6 +27,7 @@ def load_command_table(self, _): # g.custom_command('list', 'list_managed_environments') g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2e2bf3f467f..9f8cc1809fe 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -93,6 +93,14 @@ def create_kube_environment(cmd, handle_raw_exception(e) +def update_kube_environment(cmd, + name, + resource_group_name, + tags=None, + no_wait=False): + raise CLIError('Containerapp env update is not yet implemented') + + def show_kube_environment(cmd, name, resource_group_name): try: return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) From 919ed4e0a64b2be70f0583b7e6c067ee5b1210ad Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 21:05:44 -0800 Subject: [PATCH 006/177] Containerapp env delete, polling not working yet --- src/containerapp/azext_containerapp/_clients.py | 15 +++++++++++++++ src/containerapp/azext_containerapp/_help.py | 8 ++++++++ src/containerapp/azext_containerapp/_params.py | 3 +++ src/containerapp/azext_containerapp/commands.py | 4 ++-- src/containerapp/azext_containerapp/custom.py | 7 +++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 4a2577a9d1c..5c0c06f58a1 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -31,6 +31,21 @@ def create(cls, cmd, resource_group_name, name, kube_environment_envelope): r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(kube_environment_envelope)) return r.json() + @classmethod + def delete(cls, cmd, resource_group_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + send_raw_request(cmd.cli_ctx, "DELETE", request_url) # API doesn't return JSON for some reason + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 62e7cd7740c..a0b0f1421e8 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -40,6 +40,14 @@ short-summary: Update a Containerapp environment. Currently Unsupported. """ +helps['containerapp env delete'] = """ + type: command + short-summary: Deletes a Containerapp Environment. + examples: + - name: Delete Containerapp Environment. + text: az containerapp env delete -g MyResourceGroup -n MyContainerappEnvironment +""" + helps['containerapp env show'] = """ type: command short-summary: Show details of a Containerapp environment. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index cc9dece1784..2cc985f43ce 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -42,5 +42,8 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the kubernetes 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 Kubernetes Environment.') + with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 44b72bd037d..7a94c3cb3e8 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -29,5 +29,5 @@ def load_command_table(self, _): # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - - # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + # g.command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 9f8cc1809fe..b07a9e114d3 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -101,6 +101,13 @@ def update_kube_environment(cmd, raise CLIError('Containerapp env update is not yet implemented') +def delete_kube_environment(cmd, name, resource_group_name): + try: + return KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) + except CLIError as e: + handle_raw_exception(e) + + def show_kube_environment(cmd, name, resource_group_name): try: return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) From f1d2d0c96a2ef139a8dd41856161286c0a5e0742 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 4 Feb 2022 15:03:22 -0800 Subject: [PATCH 007/177] Added polling for create and delete --- .../azext_containerapp/_clients.py | 86 ++++++++++++++++++- src/containerapp/azext_containerapp/custom.py | 21 +++-- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5c0c06f58a1..779aa1b2143 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -3,7 +3,10 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from ast import NotEq import json +import time +import sys from sys import api_version from azure.cli.core.util import send_raw_request @@ -12,11 +15,62 @@ 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): + 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 not "properties" in r2 or not "provisioningState" 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: + animation.flush() + + if poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete + return + + raise e class KubeEnvironmentClient(): @classmethod - def create(cls, cmd, resource_group_name, name, kube_environment_envelope): + def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) @@ -29,10 +83,23 @@ def create(cls, cmd, resource_group_name, name, kube_environment_envelope): api_version) r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(kube_environment_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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): + def delete(cls, cmd, resource_group_name, name, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) @@ -44,7 +111,20 @@ def delete(cls, cmd, resource_group_name, name): name, api_version) - send_raw_request(cmd.cli_ctx, "DELETE", request_url) # API doesn't return JSON for some reason + 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.Web/kubeEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + poll(cmd, request_url, "scheduledfordelete") + return @classmethod def show(cls, cmd, resource_group_name, name): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index b07a9e114d3..4420b7f6516 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -7,12 +7,16 @@ from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait from knack.util import CLIError +from knack.log import get_logger from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient from ._models import KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed +logger = get_logger(__name__) + + def create_containerapp(cmd, resource_group_name, name, location=None, tags=None): raise CLIError('TODO: Implement `containerapp create`') @@ -86,9 +90,13 @@ def create_kube_environment(cmd, kube_def["tags"] = tags try: - return sdk_no_wait(no_wait, KubeEnvironmentClient.create, - cmd=cmd, resource_group_name=resource_group_name, - name=name, kube_environment_envelope=kube_def) + r = KubeEnvironmentClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=kube_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) @@ -101,9 +109,12 @@ def update_kube_environment(cmd, raise CLIError('Containerapp env update is not yet implemented') -def delete_kube_environment(cmd, name, resource_group_name): +def delete_kube_environment(cmd, name, resource_group_name, no_wait=False): try: - return KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) + r = KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + if not r and not no_wait: + logger.warning('Containerapp successfully deleted') + return r except CLIError as e: handle_raw_exception(e) From 22ecb097225a77ec4cec580e7fa0e428699faba4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 4 Feb 2022 16:25:32 -0800 Subject: [PATCH 008/177] Use Microsoft.App RP for show, list, delete command --- .../azext_containerapp/_clients.py | 28 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/commands.py | 9 ++---- src/containerapp/azext_containerapp/custom.py | 16 +++++++++-- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 779aa1b2143..dd7961f401c 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -202,6 +202,34 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) class ManagedEnvironmentClient(): + @classmethod + def delete(cls, cmd, resource_group_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = 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) + poll(cmd, request_url, "scheduledfordelete") + return + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a0b0f1421e8..18ce06e05be 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -20,7 +20,7 @@ # Environment Commands helps['containerapp env'] = """ type: group - short-summary: Commands to manage Containerapps environments. + short-summary: Commands to manage Containerapp environments. """ helps['containerapp env create'] = """ diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 7a94c3cb3e8..3539787f326 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -21,13 +21,10 @@ def load_command_table(self, _): with self.command_group('containerapp env') as g: - g.custom_command('show', 'show_kube_environment') - # g.custom_command('show', 'show_managed_environment') - g.custom_command('list', 'list_kube_environments') - # g.custom_command('list', 'list_managed_environments') + g.custom_command('show', 'show_managed_environment') + g.custom_command('list', 'list_managed_environments') g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) - # g.command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4420b7f6516..43b2eec5542 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -94,7 +94,7 @@ def create_kube_environment(cmd, cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=kube_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)) + logger.warning('Containerapp environment creation in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) return r except Exception as e: @@ -113,7 +113,7 @@ def delete_kube_environment(cmd, name, resource_group_name, no_wait=False): try: r = KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) if not r and not no_wait: - logger.warning('Containerapp successfully deleted') + logger.warning('Containerapp environment successfully deleted') return r except CLIError as e: handle_raw_exception(e) @@ -155,7 +155,7 @@ def list_managed_environments(cmd, resource_group_name=None): 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) + managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return [e for e in managed_envs if "properties" in e and "environmentType" in e["properties"] and @@ -163,3 +163,13 @@ def list_managed_environments(cmd, resource_group_name=None): e["properties"]["environmentType"].lower() == "managed"] except CLIError as e: handle_raw_exception(e) + + +def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): + try: + r = ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + if not r and not no_wait: + logger.warning('Containerapp environment successfully deleted') + return r + except CLIError as e: + handle_raw_exception(e) From e756d4eb7277c2747302a774a8f612f7d9d81de4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Sat, 5 Feb 2022 18:29:07 -0800 Subject: [PATCH 009/177] Create containerapp env using Microsoft.App RP --- .../azext_containerapp/_clients.py | 60 +++++++++++- .../azext_containerapp/_models.py | 20 ++++ .../azext_containerapp/_params.py | 14 +-- src/containerapp/azext_containerapp/_utils.py | 2 +- .../azext_containerapp/commands.py | 6 +- src/containerapp/azext_containerapp/custom.py | 94 +++++++++++++++---- 6 files changed, 166 insertions(+), 30 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index dd7961f401c..f245e6863e4 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -202,10 +202,68 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) class ManagedEnvironmentClient(): + @classmethod + def create(cls, cmd, resource_group_name, name, kube_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(kube_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, kube_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(kube_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 = API_VERSION + 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( diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 71242502bdc..d3c503d559a 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -34,6 +34,26 @@ "extendedLocation": None } +ManagedEnvironment = { + "id": None, # readonly + "name": None, # readonly + "kind": None, + "location": None, + "tags": None, + "properties": { + "daprAIInstrumentationKey": None, + "vnetConfiguration": { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIp": None + }, + "internalLoadBalancer": None, + "appLogsConfiguration": None + } +} + AppLogsConfiguration = { "destination": None, "logAnalyticsConfiguration": None diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 2cc985f43ce..40fd153c5e0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -29,13 +29,13 @@ def load_arguments(self, _): c.argument('logs_destination', options_list=['--logs-dest']) c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') - c.argument('instrumentation_key', options_list=['--instrumentation-key']) - c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') - c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') - c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') + # c.argument('instrumentation_key', options_list=['--instrumentation-key']) + # c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + # c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') + # c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') c.argument('tags', arg_type=tags_type) with self.argument_context('containerapp env update') as c: diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 6b0a92b4914..f62cd64cb45 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -46,7 +46,7 @@ def _ensure_location_allowed(cmd, location, resource_provider): 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=='containerApps'].locations'`".format( + raise ValidationError("Location '{}' is not currently supported. To get list of supported locations, run `az provider show -n {} --query \"resourceTypes[?resourceType=='containerApps'].locations\"`".format( location, resource_provider)) except ValidationError as ex: raise ex diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 3539787f326..7696326525e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -23,8 +23,6 @@ def load_command_table(self, _): with self.command_group('containerapp env') as g: g.custom_command('show', 'show_managed_environment') g.custom_command('list', 'list_managed_environments') - g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('update', 'update_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()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 43b2eec5542..8a203c55155 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -11,7 +11,7 @@ from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient -from ._models import KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._models import ManagedEnvironment, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed logger = get_logger(__name__) @@ -39,38 +39,38 @@ def create_kube_environment(cmd, no_wait=False): location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) - + _validate_subscription_registered(cmd, "Microsoft.Web") _ensure_location_allowed(cmd, location, "Microsoft.Web") - containerapps_config_def = ContainerAppsConfiguration + containerapp_env_config_def = ContainerAppsConfiguration if instrumentation_key is not None: - containerapps_config_def["daprAIInstrumentationKey"] = instrumentation_key + containerapp_env_config_def["daprAIInstrumentationKey"] = instrumentation_key if controlplane_subnet_resource_id is not None: if not app_subnet_resource_id: raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') - containerapps_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id + containerapp_env_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id if app_subnet_resource_id is not None: if not controlplane_subnet_resource_id: raise ValidationError('Controlplane subnet resource ID needs to be supplied with app subnet resource ID.') - containerapps_config_def["appSubnetResourceId"] = app_subnet_resource_id + containerapp_env_config_def["appSubnetResourceId"] = app_subnet_resource_id if docker_bridge_cidr is not None: - containerapps_config_def["dockerBridgeCidr"] = docker_bridge_cidr + containerapp_env_config_def["dockerBridgeCidr"] = docker_bridge_cidr if platform_reserved_cidr is not None: - containerapps_config_def["platformReservedCidr"] = platform_reserved_cidr + containerapp_env_config_def["platformReservedCidr"] = platform_reserved_cidr if platform_reserved_dns_ip is not None: - containerapps_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip + containerapp_env_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip if internal_only: if not controlplane_subnet_resource_id or not app_subnet_resource_id: raise ValidationError('Controlplane subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') - containerapps_config_def["internalOnly"] = True + containerapp_env_config_def["internalOnly"] = True log_analytics_config_def = LogAnalyticsConfiguration log_analytics_config_def["customerId"] = logs_customer_id @@ -86,7 +86,7 @@ def create_kube_environment(cmd, kube_def["properties"]["environmentType"] = "managed" kube_def["properties"]["type"] = "managed" kube_def["properties"]["appLogsConfiguration"] = app_logs_config_def - kube_def["properties"]["containerAppsConfiguration"] = containerapps_config_def + kube_def["properties"]["containerAppsConfiguration"] = containerapp_env_config_def kube_def["tags"] = tags try: @@ -101,12 +101,69 @@ def create_kube_environment(cmd, handle_raw_exception(e) -def update_kube_environment(cmd, +def create_managed_environment(cmd, + name, + resource_group_name, + logs_customer_id, + logs_key, + logs_destination="log-analytics", + location=None, + 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") + + log_analytics_config_def = LogAnalyticsConfiguration + log_analytics_config_def["customerId"] = logs_customer_id + log_analytics_config_def["sharedKey"] = logs_key + + app_logs_config_def = AppLogsConfiguration + app_logs_config_def["destination"] = logs_destination + app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def + + managed_env_def = ManagedEnvironment + 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 + + try: + r = ManagedEnvironmentClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, kube_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 show -n {} -g {}`'.format(name, resource_group_name)) + + 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 CLIError('Containerapp env update is not yet implemented') + raise CLIError('Containerapp env update is not yet supported.') + + _validate_subscription_registered(cmd, "Microsoft.App") + + managed_env_def = ManagedEnvironment + managed_env_def["tags"] = tags + + try: + r = ManagedEnvironmentClient.update( + cmd=cmd, resource_group_name=resource_group_name, name=name, kube_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 update 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 delete_kube_environment(cmd, name, resource_group_name, no_wait=False): @@ -143,6 +200,8 @@ def list_kube_environments(cmd, resource_group_name=None): 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: @@ -150,6 +209,8 @@ def show_managed_environment(cmd, name, resource_group_name): def list_managed_environments(cmd, resource_group_name=None): + _validate_subscription_registered(cmd, "Microsoft.App") + try: managed_envs = [] if resource_group_name is None: @@ -157,15 +218,14 @@ def list_managed_environments(cmd, resource_group_name=None): else: managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) - return [e for e in managed_envs if "properties" in e and - "environmentType" in e["properties"] and - e["properties"]["environmentType"] and - e["properties"]["environmentType"].lower() == "managed"] + 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: r = ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) if not r and not no_wait: From 339275544f51a9e5dd35d4663e37e535bc695a9a Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Feb 2022 07:06:53 -0800 Subject: [PATCH 010/177] Add optional containerapp env create arguments --- .../azext_containerapp/_models.py | 8 ++++ .../azext_containerapp/_params.py | 18 ++++---- src/containerapp/azext_containerapp/custom.py | 42 ++++++++++++++++++- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index d3c503d559a..b9abfdd0ac5 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -63,3 +63,11 @@ "customerId": None, "sharedKey": None } + +VnetConfiguration = { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIp": None +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 40fd153c5e0..2c659029454 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -29,15 +29,19 @@ def load_arguments(self, _): c.argument('logs_destination', options_list=['--logs-dest']) c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') - # c.argument('instrumentation_key', options_list=['--instrumentation-key']) - # c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') - # c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') - # c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env', arg_group='Dapr') as c: + c.argument('instrumentation_key', options_list=['--instrumentation-key'], help='Azure Monitor 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', options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + c.argument('app_subnet_resource_id', 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', 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', 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', 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', 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 kubernetes environment.') c.argument('tags', arg_type=tags_type) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 8a203c55155..d2e839fd352 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from platform import platform from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait @@ -11,7 +12,7 @@ from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient -from ._models import ManagedEnvironment, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._models import ManagedEnvironment, VnetConfiguration, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed logger = get_logger(__name__) @@ -108,6 +109,13 @@ def create_managed_environment(cmd, logs_key, logs_destination="log-analytics", location=None, + instrumentation_key=None, + infrastructure_subnet_resource_id=None, + app_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): @@ -130,6 +138,38 @@ def create_managed_environment(cmd, 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 app_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: + vnet_config_def = VnetConfiguration + + if infrastructure_subnet_resource_id is not None: + if not app_subnet_resource_id: + raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') + vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id + + if app_subnet_resource_id is not None: + if not infrastructure_subnet_resource_id: + raise ValidationError('Infrastructure subnet resource ID needs to be supplied with app subnet resource ID.') + vnet_config_def["runtimeSubnetId"] = app_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["platformReservedCidr"] = platform_reserved_dns_ip + + managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def + + if internal_only: + if not infrastructure_subnet_resource_id or not app_subnet_resource_id: + raise ValidationError('Infrastructure subnet resource ID and App subnet resource ID need 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, kube_environment_envelope=managed_env_def, no_wait=no_wait) From ef13ecec3bca6eab39a2b21b448b4686ac48fca0 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Feb 2022 07:14:07 -0800 Subject: [PATCH 011/177] Remove old kube environment code, naming fixes --- .../azext_containerapp/_clients.py | 137 +----------------- .../azext_containerapp/_models.py | 48 +----- .../azext_containerapp/_params.py | 6 +- src/containerapp/azext_containerapp/custom.py | 123 +--------------- 4 files changed, 16 insertions(+), 298 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index f245e6863e4..5785ca0518d 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -68,142 +68,9 @@ def poll(cmd, request_url, poll_if_status): raise e -class KubeEnvironmentClient(): - @classmethod - def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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(kube_environment_envelope)) - - if no_wait: - return r.json() - elif r.status_code == 201: - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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 = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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.Web/kubeEnvironments/{}?api-version={}" - request_url = url_fmt.format( - management_hostname.strip('/'), - sub_id, - resource_group_name, - name, - api_version) - poll(cmd, request_url, "scheduledfordelete") - return - - @classmethod - def show(cls, cmd, resource_group_name, name): - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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): - kube_list = [] - - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - request_url = "{}/subscriptions/{}/providers/Microsoft.Web/kubeEnvironments?api-version={}".format( - management_hostname.strip('/'), - sub_id, - api_version) - - r = send_raw_request(cmd.cli_ctx, "GET", request_url) - j = r.json() - for kube in j["value"]: - formatted = formatter(kube) - kube_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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) - - return kube_list - - @classmethod - def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): - kube_list = [] - - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments?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 kube in j["value"]: - formatted = formatter(kube) - kube_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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) - - return kube_list - - class ManagedEnvironmentClient(): @classmethod - def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): + 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) @@ -215,7 +82,7 @@ def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wa name, api_version) - r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(kube_environment_envelope)) + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(managed_environment_envelope)) if no_wait: return r.json() diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index b9abfdd0ac5..c95a9dfda0e 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -4,34 +4,12 @@ # -------------------------------------------------------------------------------------------- -ContainerAppsConfiguration = { - "daprAIInstrumentationKey": None, - "appSubnetResourceId": None, +VnetConfiguration = { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, "dockerBridgeCidr": None, "platformReservedCidr": None, - "platformReservedDnsIP": None, - "internalOnly": False -} - -KubeEnvironment = { - "id": None, # readonly - "name": None, # readonly - "kind": None, - "location": None, - "tags": None, - "properties": { - "type": None, - "environmentType": None, - "containerAppsConfiguration": None, - "provisioningState": None, # readonly - "deploymentErrors": None, # readonly - "defaultDomain": None, # readonly - "staticIp": None, - "arcConfiguration": None, - "appLogsConfiguration": None, - "aksResourceId": None - }, - "extendedLocation": None + "platformReservedDnsIP": None } ManagedEnvironment = { @@ -42,14 +20,8 @@ "tags": None, "properties": { "daprAIInstrumentationKey": None, - "vnetConfiguration": { - "infrastructureSubnetId": None, - "runtimeSubnetId": None, - "dockerBridgeCidr": None, - "platformReservedCidr": None, - "platformReservedDnsIp": None - }, - "internalLoadBalancer": None, + "vnetConfiguration": VnetConfiguration, + "internalLoadBalancerEnabled": None, "appLogsConfiguration": None } } @@ -63,11 +35,3 @@ "customerId": None, "sharedKey": None } - -VnetConfiguration = { - "infrastructureSubnetId": None, - "runtimeSubnetId": None, - "dockerBridgeCidr": None, - "platformReservedCidr": None, - "platformReservedDnsIp": None -} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 2c659029454..1f38065f2ae 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -43,11 +43,11 @@ def load_arguments(self, _): c.argument('internal_only', 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 kubernetes environment.') + c.argument('name', name_type, help='Name of the managed 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 Kubernetes Environment.') + c.argument('name', name_type, help='Name of the managed Environment.') with self.argument_context('containerapp env show') as c: - c.argument('name', name_type, help='Name of the Kubernetes Environment.') + c.argument('name', name_type, help='Name of the managed Environment.') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d2e839fd352..a7887c1bdc2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -11,8 +11,8 @@ from knack.log import get_logger from ._client_factory import handle_raw_exception -from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient -from ._models import ManagedEnvironment, VnetConfiguration, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._clients import ManagedEnvironmentClient +from ._models import ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed logger = get_logger(__name__) @@ -22,86 +22,6 @@ def create_containerapp(cmd, resource_group_name, name, location=None, tags=None raise CLIError('TODO: Implement `containerapp create`') -def create_kube_environment(cmd, - name, - resource_group_name, - logs_customer_id, - logs_key, - logs_destination="log-analytics", - location=None, - instrumentation_key=None, - controlplane_subnet_resource_id=None, - app_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.Web") - _ensure_location_allowed(cmd, location, "Microsoft.Web") - - containerapp_env_config_def = ContainerAppsConfiguration - - if instrumentation_key is not None: - containerapp_env_config_def["daprAIInstrumentationKey"] = instrumentation_key - - if controlplane_subnet_resource_id is not None: - if not app_subnet_resource_id: - raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') - containerapp_env_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id - - if app_subnet_resource_id is not None: - if not controlplane_subnet_resource_id: - raise ValidationError('Controlplane subnet resource ID needs to be supplied with app subnet resource ID.') - containerapp_env_config_def["appSubnetResourceId"] = app_subnet_resource_id - - if docker_bridge_cidr is not None: - containerapp_env_config_def["dockerBridgeCidr"] = docker_bridge_cidr - - if platform_reserved_cidr is not None: - containerapp_env_config_def["platformReservedCidr"] = platform_reserved_cidr - - if platform_reserved_dns_ip is not None: - containerapp_env_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip - - if internal_only: - if not controlplane_subnet_resource_id or not app_subnet_resource_id: - raise ValidationError('Controlplane subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') - containerapp_env_config_def["internalOnly"] = True - - log_analytics_config_def = LogAnalyticsConfiguration - log_analytics_config_def["customerId"] = logs_customer_id - log_analytics_config_def["sharedKey"] = logs_key - - app_logs_config_def = AppLogsConfiguration - app_logs_config_def["destination"] = logs_destination - app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def - - kube_def = KubeEnvironment - kube_def["location"] = location - kube_def["properties"]["internalLoadBalancerEnabled"] = False - kube_def["properties"]["environmentType"] = "managed" - kube_def["properties"]["type"] = "managed" - kube_def["properties"]["appLogsConfiguration"] = app_logs_config_def - kube_def["properties"]["containerAppsConfiguration"] = containerapp_env_config_def - kube_def["tags"] = tags - - try: - r = KubeEnvironmentClient.create( - cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=kube_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 show -n {} -g {}`'.format(name, resource_group_name)) - - return r - except Exception as e: - handle_raw_exception(e) - - def create_managed_environment(cmd, name, resource_group_name, @@ -161,7 +81,7 @@ def create_managed_environment(cmd, vnet_config_def["platformReservedCidr"] = platform_reserved_cidr if platform_reserved_dns_ip is not None: - vnet_config_def["platformReservedCidr"] = platform_reserved_dns_ip + vnet_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def @@ -172,7 +92,7 @@ def create_managed_environment(cmd, try: r = ManagedEnvironmentClient.create( - cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=managed_env_def, no_wait=no_wait) + 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 show -n {} -g {}`'.format(name, resource_group_name)) @@ -196,7 +116,7 @@ def update_managed_environment(cmd, try: r = ManagedEnvironmentClient.update( - cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=managed_env_def, no_wait=no_wait) + 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 update in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) @@ -206,39 +126,6 @@ def update_managed_environment(cmd, handle_raw_exception(e) -def delete_kube_environment(cmd, name, resource_group_name, no_wait=False): - try: - r = KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - if not r and not no_wait: - logger.warning('Containerapp environment successfully deleted') - return r - except CLIError as e: - handle_raw_exception(e) - - -def show_kube_environment(cmd, name, resource_group_name): - try: - return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: - handle_raw_exception(e) - - -def list_kube_environments(cmd, resource_group_name=None): - try: - kube_envs = [] - if resource_group_name is None: - kube_envs = KubeEnvironmentClient.list_by_subscription(cmd=cmd) - else: - kube_envs = KubeEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) - - return [e for e in kube_envs if "properties" in e and - "environmentType" in e["properties"] and - e["properties"]["environmentType"] and - e["properties"]["environmentType"].lower() == "managed"] - except CLIError as e: - handle_raw_exception(e) - - def show_managed_environment(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") From eeb4c8b506e1dc01a2e5ee5245cf3cf9cbf24f32 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 10 Feb 2022 21:43:09 -0800 Subject: [PATCH 012/177] Containerapp create almost done --- .../azext_containerapp/_models.py | 153 +++++++++++++++++- .../azext_containerapp/_params.py | 43 +++++ src/containerapp/azext_containerapp/_utils.py | 99 ++++++++++++ .../azext_containerapp/_validators.py | 62 +++++++ src/containerapp/azext_containerapp/custom.py | 150 ++++++++++++++++- 5 files changed, 498 insertions(+), 9 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index c95a9dfda0e..379e69b0029 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -13,14 +13,11 @@ } ManagedEnvironment = { - "id": None, # readonly - "name": None, # readonly - "kind": None, "location": None, "tags": None, "properties": { "daprAIInstrumentationKey": None, - "vnetConfiguration": VnetConfiguration, + "vnetConfiguration": None, # VnetConfiguration "internalLoadBalancerEnabled": None, "appLogsConfiguration": None } @@ -35,3 +32,151 @@ "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] + "allowInsecure": None +} + +RegistryCredentials = { + "server": None, + "username": None, + "passwordSecretRef": None +} + +Template = { + "revisionSuffix": None, + "containers": None, # [Container] + "scale": Scale, + "dapr": Dapr, + "volumes": None # [Volume] +} + +Configuration = { + "secrets": None, # [Secret] + "activeRevisionsMode": None, # 'multiple' or 'single' + "ingress": None, # Ingress + "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 +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 1f38065f2ae..618d1b4ba13 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -11,6 +11,8 @@ 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) def load_arguments(self, _): @@ -22,6 +24,47 @@ def load_arguments(self, _): 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 create') as c: + c.argument('tags', arg_type=tags_type) + c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment', '-e'], help="Name or resource ID of the containerapp's environment.") + c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') + + # Container + with self.argument_context('containerapp create', arg_group='Container') as c: + c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") + c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") + c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") + c.argument('env_vars', type=str, options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Comma-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") + c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") + c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") + + # Scale + with self.argument_context('containerapp create', arg_group='Scale') as c: + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + + # Configuration + with self.argument_context('containerapp create', arg_group='Configuration') as c: + c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") + c.argument('secrets', type=str, options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Comma-separated values in 'key=value' format.") + + # Ingress + with self.argument_context('containerapp create', arg_group='Ingress') as c: + c.argument('ingress', options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") + c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") + + # Dapr + with self.argument_context('containerapp create', arg_group='Dapr') as c: + c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) + c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") + c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") + c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") + with self.argument_context('containerapp env') as c: c.argument('name', name_type) c.argument('resource_group_name', arg_type=resource_group_name_type) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index f62cd64cb45..45b552676e7 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,9 +5,13 @@ from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id +from knack.log import get_logger +from urllib.parse import urlparse from ._client_factory import providers_client_factory, cf_resource_groups +logger = get_logger(__name__) + def _get_location_from_resource_group(cli_ctx, resource_group_name): client = cf_resource_groups(cli_ctx) @@ -52,3 +56,98 @@ def _ensure_location_allowed(cmd, location, resource_provider): raise ex except Exception: pass + + +def parse_env_var_flags(env_string, is_update_containerapp=False): + env_pair_strings = env_string.split(',') + env_pairs = {} + + for pair in env_pair_strings: + key_val = pair.split('=') + if len(key_val) is not 2: + if is_update_containerapp: + raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") + 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) is 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_string): + secret_pair_strings = secret_string.split(',') + secret_pairs = {} + + for pair in secret_pair_strings: + key_val = pair.split('=', 1) + if len(key_val) is not 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 store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass): + 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(): + raise ValidationError('Found secret with name \"{}\" but value does not equal the supplied registry password.'.format(registry_secret_name)) + else: + return registry_secret_name + + logger.warning('Adding registry password as a secret with name \"{}\"'.format(registry_secret_name)) + 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] diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 821630f5f34..0843ab2a374 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -3,6 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from unicodedata import name +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) + def example_name_or_id_validator(cmd, namespace): # Example of a storage account name or ID validator. @@ -18,3 +21,62 @@ def example_name_or_id_validator(cmd, namespace): type='storageAccounts', name=namespace.storage_account ) + +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: + if namespace.cpu is None: + raise RequiredArgumentMissingError('Usage error: --cpu required if specifying --memory') + + 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 is not None and namespace.memory is None: + raise RequiredArgumentMissingError('Usage error: --memory required if specifying --cpu') + +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 namespace.registry_server: + if not namespace.registry_user or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + +def validate_registry_user(namespace): + if namespace.registry_user: + if not namespace.registry_server or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + +def validate_registry_pass(namespace): + if namespace.registry_pass: + if not namespace.registry_user or not namespace.registry_server: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + +def validate_target_port(namespace): + if namespace.target_port: + if not namespace.ingress: + raise ValidationError("Usage error: must specify --ingress with --target-port") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a7887c1bdc2..989f4b91b71 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,22 +4,162 @@ # -------------------------------------------------------------------------------------------- from platform import platform -from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger +from msrestazure.tools import parse_resource_id from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient -from ._models import ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration -from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed +from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, + Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) +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_list_of_strings, parse_env_var_flags) logger = get_logger(__name__) -def create_containerapp(cmd, resource_group_name, name, location=None, tags=None): - raise CLIError('TODO: Implement `containerapp create`') +def create_containerapp(cmd, + name, + resource_group_name, + yaml=None, + image_name=None, + managed_env=None, + min_replicas=None, + max_replicas=None, + target_port=None, + transport="auto", + ingress=None, + revisions_mode=None, + 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, + # dapr_components=None, + location=None, + startup_command=None, + args=None, + 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") + + if yaml: + # TODO: Implement yaml + return + + if image_name is None: + raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') + + 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 = location or managed_env_info.location + + 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 = Ingress + ingress_def["external"] = external_ingress + ingress_def["target_port"] = 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: + credentials_def = RegistryCredentials + credentials_def["server"] = registry_server + credentials_def["username"] = registry_user + + if secrets_def is None: + secrets_def = [] + credentials_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + + config_def = Configuration + config_def["secrets"] = secrets_def + config_def["activeRevisionsMode"] = revisions_mode + config_def["ingress"] = ingress_def + config_def["registries"] = registries_def + + scale_def = None + if min_replicas is not None or max_replicas is not None: + scale_def = Scale + 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 = ContainerResources + resources_def["cpu"] = cpu + resources_def["memory"] = memory + + container_def = Container + container_def["name"] = name + container_def["image"] = image_name + if env_vars is not None: + container_def["env"] = parse_env_var_flags(env_vars) + if startup_command is not None: + container_def["command"] = parse_list_of_strings(startup_command) + if args is not None: + container_def["args"] = parse_list_of_strings(args) + if resources_def is not None: + container_def["resources"] = resources_def + + dapr_def = None + if dapr_enabled: + dapr_def = Dapr + dapr_def["daprEnabled"] = True + dapr_def["appId"] = dapr_app_id + dapr_def["appPort"] = dapr_app_port + dapr_def["appProtocol"] = dapr_app_protocol + + template_def = Template + template_def["container"] = [container_def] + template_def["scale"] = scale_def + template_def["dapr"] = dapr_def + + containerapp_def = ContainerApp + container_def["location"] = location + containerapp_def["properties"]["managedEnvironmentId"] = managed_env + containerapp_def["properties"]["configuration"] = config_def + containerapp_def["properties"]["template"] = template_def + container_def["tags"] = tags + + # TODO: Call create with nowait poller def create_managed_environment(cmd, From 12cf40c199689ccafba5b3cc6ce901cd970c18db Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 08:08:58 -0800 Subject: [PATCH 013/177] Done containerapp create, except for --yaml. Need to test --- .../azext_containerapp/_clients.py | 31 +++++++++++++++++++ src/containerapp/azext_containerapp/custom.py | 23 +++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5785ca0518d..57411e1a438 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -68,6 +68,37 @@ def poll(cmd, request_url, poll_if_status): raise e +class ContainerAppClient(): + @classmethod + def create(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, "waiting") + + return r.json() + + class ManagedEnvironmentClient(): @classmethod def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no_wait=False): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 989f4b91b71..de2a48aa5a4 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -12,7 +12,7 @@ from msrestazure.tools import parse_resource_id from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) from ._utils import (_validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed, @@ -57,7 +57,7 @@ def create_containerapp(cmd, if yaml: # TODO: Implement yaml - return + raise CLIError("--yaml is not yet implemented") if image_name is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') @@ -153,13 +153,22 @@ def create_containerapp(cmd, template_def["dapr"] = dapr_def containerapp_def = ContainerApp - container_def["location"] = location + containerapp_def["location"] = location containerapp_def["properties"]["managedEnvironmentId"] = managed_env containerapp_def["properties"]["configuration"] = config_def containerapp_def["properties"]["template"] = template_def - container_def["tags"] = tags + containerapp_def["tags"] = tags - # TODO: Call create with nowait poller + try: + r = ContainerAppClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_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)) + + return r + except Exception as e: + handle_raw_exception(e) def create_managed_environment(cmd, @@ -235,7 +244,7 @@ def create_managed_environment(cmd, 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 show -n {} -g {}`'.format(name, resource_group_name)) + logger.warning('Containerapp environment creation in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) return r except Exception as e: @@ -259,7 +268,7 @@ def update_managed_environment(cmd, 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 update in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + logger.warning('Containerapp environment update in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) return r except Exception as e: From 509b91cc36ea334e790cf96aad0e92165a1d5196 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 10:52:37 -0800 Subject: [PATCH 014/177] Containerapp show, list --- .../azext_containerapp/_clients.py | 107 +++++++++++++++++- src/containerapp/azext_containerapp/_help.py | 21 ++++ .../azext_containerapp/_models.py | 2 +- .../azext_containerapp/commands.py | 4 +- src/containerapp/azext_containerapp/custom.py | 30 ++++- 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 57411e1a438..82cc2c6be23 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -94,9 +94,112 @@ def create(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= resource_group_name, name, api_version) - return poll(cmd, request_url, "waiting") + 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 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 r.json() + return app_list class ManagedEnvironmentClient(): diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 18ce06e05be..655e528985e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -17,6 +17,27 @@ short-summary: Create a Containerapp. """ +helps['containerapp show'] = """ + type: command + short-summary: Show details of a Containerapp. + examples: + - name: Show the details of a Containerapp. + text: | + az containerapp show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp list'] = """ + type: command + short-summary: List Containerapps. + examples: + - name: List Containerapps by subscription. + text: | + az containerapp list + - name: List Containerapps by resource group. + text: | + az containerapp list -g MyResourceGroup +""" + # Environment Commands helps['containerapp env'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 379e69b0029..f0d068b1bbc 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -137,7 +137,7 @@ "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight "customDomains": None, # [CustomDomain] - "allowInsecure": None + # "allowInsecure": None } RegistryCredentials = { diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 7696326525e..d3c3853d2f6 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -17,7 +17,9 @@ def load_command_table(self, _): with self.command_group('containerapp') as g: - g.custom_command('create', 'create_containerapp') + g.custom_command('show', 'show_containerapp') + g.custom_command('list', 'list_containerapp') + g.custom_command('create', 'create_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index de2a48aa5a4..ff347f1226a 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -92,7 +92,7 @@ def create_containerapp(cmd, if target_port is not None and ingress is not None: ingress_def = Ingress ingress_def["external"] = external_ingress - ingress_def["target_port"] = target_port + ingress_def["targetPort"] = target_port ingress_def["transport"] = transport secrets_def = None @@ -148,7 +148,7 @@ def create_containerapp(cmd, dapr_def["appProtocol"] = dapr_app_protocol template_def = Template - template_def["container"] = [container_def] + template_def["containers"] = [container_def] template_def["scale"] = scale_def template_def["dapr"] = dapr_def @@ -161,7 +161,7 @@ def create_containerapp(cmd, try: r = ContainerAppClient.create( - cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_envelope=containerapp_def, no_wait=no_wait) + 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)) @@ -171,6 +171,30 @@ def create_containerapp(cmd, 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 create_managed_environment(cmd, name, resource_group_name, From e60ad9d24d91d7c32d28ef7c46da55adc80ed575 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 11:44:42 -0800 Subject: [PATCH 015/177] Fix helptext --- src/containerapp/azext_containerapp/_help.py | 54 +++++++++++++++++++ .../azext_containerapp/_params.py | 20 +++---- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 655e528985e..200b45cc1e6 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -15,6 +15,60 @@ helps['containerapp create'] = """ type: command short-summary: Create a Containerapp. + examples: + - name: Create a Containerapp + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with secrets and environment variables + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --secrets mysecret=escapefromtarkov,anothersecret=isadifficultgame \\ + --environment-variables myenvvar=foo,anotherenvvar=bar \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp that only accepts internal traffic + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --ingress internal \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp using an image from a private registry + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --secrets mypassword=verysecurepassword \\ + --registry-login-server MyRegistryServerAddress \\ + --registry-username MyUser \\ + --registry-password mypassword \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with a specified startup command and arguments + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --command "/bin/sh" \\ + --args "-c", "while true; do echo hello; sleep 10;done" \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with a minimum resource and replica requirements + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --cpu 0.5 --memory 1.0Gi \\ + --min-replicas 4 --max-replicas 8 \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with dapr components + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --enable-dapr --dapr-app-port myAppPort \\ + --dapr-app-id myAppID \\ + --dapr-components PathToDaprComponentsFile \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + -- yaml "C:/path/to/yaml/file.yml" """ helps['containerapp show'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 618d1b4ba13..78ae210d3c4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -30,7 +30,7 @@ def load_arguments(self, _): c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container - with self.argument_context('containerapp create', arg_group='Container') as c: + with self.argument_context('containerapp create', arg_group='Container (Creates new revision)') as c: c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") @@ -39,10 +39,18 @@ def load_arguments(self, _): c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") # Scale - with self.argument_context('containerapp create', arg_group='Scale') as c: + with self.argument_context('containerapp create', arg_group='Scale (Creates new revision)') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + # Dapr + with self.argument_context('containerapp create', arg_group='Dapr (Creates new revision)') as c: + c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) + c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") + c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") + c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") + # Configuration with self.argument_context('containerapp create', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") @@ -57,14 +65,6 @@ def load_arguments(self, _): c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") - # Dapr - with self.argument_context('containerapp create', arg_group='Dapr') as c: - c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) - c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") - c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") - c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") - c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") - with self.argument_context('containerapp env') as c: c.argument('name', name_type) c.argument('resource_group_name', arg_type=resource_group_name_type) From b99c05483efd62d3a7630bdbecf9caeebf143d14 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 13:03:48 -0800 Subject: [PATCH 016/177] Containerapp delete --- .../azext_containerapp/_clients.py | 28 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 8 ++++++ .../azext_containerapp/commands.py | 22 +++++++++++++-- src/containerapp/azext_containerapp/custom.py | 12 ++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 82cc2c6be23..f08399aaf06 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -127,6 +127,34 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= 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) + poll(cmd, request_url, "cancelled") + return + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 200b45cc1e6..0c81811d0e3 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -71,6 +71,14 @@ -- yaml "C:/path/to/yaml/file.yml" """ +helps['containerapp delete'] = """ + type: command + short-summary: Delete a Containerapp. + examples: + - name: Delete a Containerapp. + text: az containerapp delete -g MyResourceGroup -n MyContainerapp +""" + helps['containerapp show'] = """ type: command short-summary: Show details of a Containerapp. diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index d3c3853d2f6..995557294ca 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -5,9 +5,26 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType +from msrestazure.tools import is_valid_resource_id, parse_resource_id from azext_containerapp._client_factory import cf_containerapp, 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 Exception: + result['fqdn'] = None + + return result + + +def transform_containerapp_list_output(apps): + return [transform_containerapp_output(a) for a in apps] + + def load_command_table(self, _): # TODO: Add command type here @@ -17,9 +34,10 @@ def load_command_table(self, _): with self.command_group('containerapp') as g: - g.custom_command('show', 'show_containerapp') - g.custom_command('list', 'list_containerapp') + g.custom_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()) + g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ff347f1226a..2c4751aa124 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -195,6 +195,18 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) +def delete_containerapp(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + r = ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + if not r and not no_wait: + logger.warning('Containerapp successfully deleted') + return r + except CLIError as e: + handle_raw_exception(e) + + def create_managed_environment(cmd, name, resource_group_name, From f538a24d9d3d2df1f2493236991c4d6299783e0d Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Feb 2022 09:44:34 -0800 Subject: [PATCH 017/177] Containerapp update. Needs secrets api to be implemented, and testing --- .../azext_containerapp/_client_factory.py | 3 + .../azext_containerapp/_clients.py | 2 +- src/containerapp/azext_containerapp/_help.py | 46 ++++- .../azext_containerapp/_params.py | 19 +- src/containerapp/azext_containerapp/_utils.py | 10 +- .../azext_containerapp/_validators.py | 40 ++-- .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 188 +++++++++++++++++- 8 files changed, 263 insertions(+), 46 deletions(-) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 4c8eeeb7f86..3ee674ace77 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -58,6 +58,9 @@ def handle_raw_exception(e): elif "Message" in jsonError: message = jsonError["Message"] raise CLIError(message) + elif "message" in jsonError: + message = jsonError["message"] + raise CLIError(message) raise e diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index f08399aaf06..9b2f0f89750 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -70,7 +70,7 @@ def poll(cmd, request_url, poll_if_status): class ContainerAppClient(): @classmethod - def create(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): + 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) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 0c81811d0e3..27b2c0c98f3 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -57,20 +57,50 @@ --cpu 0.5 --memory 1.0Gi \\ --min-replicas 4 --max-replicas 8 \\ --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with dapr components - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --enable-dapr --dapr-app-port myAppPort \\ - --dapr-app-id myAppID \\ - --dapr-components PathToDaprComponentsFile \\ - --query properties.configuration.ingress.fqdn - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ -- yaml "C:/path/to/yaml/file.yml" """ +helps['containerapp update'] = """ + type: command + short-summary: Update a Containerapp. + examples: + - name: Update a Containerapp's container image + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --image MyNewContainerImage + - name: Update a Containerapp with secrets and environment variables + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --secrets mysecret=secretfoo,anothersecret=secretbar + --environment-variables myenvvar=foo,anotherenvvar=secretref:mysecretname + - name: Update a Containerapp's ingress setting to internal + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --ingress internal + - name: Update a Containerapp using an image from a private registry + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --image MyNewContainerImage \\ + --secrets mypassword=verysecurepassword \\ + --registry-login-server MyRegistryServerAddress \\ + --registry-username MyUser \\ + --registry-password mypassword + - name: Update a Containerapp using a specified startup command and arguments + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage \\ + --command "/bin/sh" + --args "-c", "while true; do echo hello; sleep 10;done" + - name: Update a Containerapp with a minimum resource and replica requirements + 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 Containerapp. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 78ae210d3c4..c41e729e2d2 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -12,7 +12,7 @@ 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_registry_user, validate_registry_pass, validate_target_port, validate_ingress) def load_arguments(self, _): @@ -24,27 +24,28 @@ def load_arguments(self, _): 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 create') as c: + 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', '-e'], help="Name or resource ID of the containerapp's environment.") c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container - with self.argument_context('containerapp create', arg_group='Container (Creates new revision)') as c: + with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', type=str, options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Comma-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") + c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale - with self.argument_context('containerapp create', arg_group='Scale (Creates new revision)') as c: + with self.argument_context('containerapp', arg_group='Scale (Creates new revision)') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") # Dapr - with self.argument_context('containerapp create', arg_group='Dapr (Creates new revision)') as c: + with self.argument_context('containerapp', arg_group='Dapr (Creates new revision)') as c: c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") @@ -52,7 +53,7 @@ def load_arguments(self, _): c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") # Configuration - with self.argument_context('containerapp create', arg_group='Configuration') as c: + with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, 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 container image registry server. If stored as a secret, value must start with \'secretref:\' followed by the secret name.") @@ -60,13 +61,13 @@ def load_arguments(self, _): c.argument('secrets', type=str, options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Comma-separated values in 'key=value' format.") # Ingress - with self.argument_context('containerapp create', arg_group='Ingress') as c: - c.argument('ingress', options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") + with self.argument_context('containerapp', arg_group='Ingress') as c: + c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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 env') as c: - c.argument('name', name_type) + c.argument('name', name_type, help='Name of the containerapp 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('logs_destination', options_list=['--logs-dest']) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 45b552676e7..c2565c651b6 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -111,7 +111,7 @@ def parse_secret_flags(secret_string): return secret_var_def -def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass): +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 @@ -135,9 +135,11 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ for secret in secrets_list: if secret['name'].lower() == registry_secret_name.lower(): if secret['value'].lower() != registry_pass.lower(): - raise ValidationError('Found secret with name \"{}\" but value does not equal the supplied registry password.'.format(registry_secret_name)) - else: - return registry_secret_name + 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)) secrets_list.append({ diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 0843ab2a374..1f5913e3fed 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -33,9 +33,6 @@ def validate_memory(namespace): memory = namespace.memory if memory is not None: - if namespace.cpu is None: - raise RequiredArgumentMissingError('Usage error: --cpu required if specifying --memory') - valid = False if memory.endswith("Gi"): @@ -45,8 +42,7 @@ def validate_memory(namespace): raise ValidationError("Usage error: --memory must be a number ending with \"Gi\"") def validate_cpu(namespace): - if namespace.cpu is not None and namespace.memory is None: - raise RequiredArgumentMissingError('Usage error: --memory required if specifying --cpu') + return def validate_managed_env_name_or_id(cmd, namespace): from azure.cli.core.commands.client_factory import get_subscription_id @@ -62,21 +58,31 @@ def validate_managed_env_name_or_id(cmd, namespace): ) def validate_registry_server(namespace): - if namespace.registry_server: - if not namespace.registry_user or not namespace.registry_pass: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if "create" in namespace.command.lower(): + if namespace.registry_server: + if not namespace.registry_user or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") def validate_registry_user(namespace): - if namespace.registry_user: - if not namespace.registry_server or not namespace.registry_pass: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if "create" in namespace.command.lower(): + if namespace.registry_user: + if not namespace.registry_server or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") def validate_registry_pass(namespace): - if namespace.registry_pass: - if not namespace.registry_user or not namespace.registry_server: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if "create" in namespace.command.lower(): + if namespace.registry_pass: + if not namespace.registry_user or not namespace.registry_server: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") def validate_target_port(namespace): - if namespace.target_port: - if not namespace.ingress: - raise ValidationError("Usage error: must specify --ingress with --target-port") + 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/commands.py b/src/containerapp/azext_containerapp/commands.py index 995557294ca..330ac7234cb 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -37,6 +37,7 @@ def load_command_table(self, _): g.custom_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()) + g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2c4751aa124..68db52f3238 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -101,19 +101,19 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: - credentials_def = RegistryCredentials - credentials_def["server"] = registry_server - credentials_def["username"] = registry_user + registries_def = RegistryCredentials + registries_def["server"] = registry_server + registries_def["username"] = registry_user if secrets_def is None: secrets_def = [] - credentials_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) config_def = Configuration - config_def["secrets"] = secrets_def + config_def["secrets"] = None # TODO: Uncomment secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def - config_def["registries"] = registries_def + config_def["registries"] = [registries_def] scale_def = None if min_replicas is not None or max_replicas is not None: @@ -160,7 +160,7 @@ def create_containerapp(cmd, containerapp_def["tags"] = tags try: - r = ContainerAppClient.create( + 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: @@ -171,6 +171,180 @@ def create_containerapp(cmd, handle_raw_exception(e) +def update_containerapp(cmd, + name, + resource_group_name, + yaml=None, + image_name=None, + min_replicas=None, + max_replicas=None, + ingress=None, + target_port=None, + transport=None, + # traffic_weights=None, + revisions_mode=None, + secrets=None, + env_vars=None, + cpu=None, + memory=None, + registry_server=None, + registry_user=None, + registry_pass=None, + dapr_enabled=None, + dapr_app_port=None, + dapr_app_id=None, + dapr_app_protocol=None, + # dapr_components=None, + revision_suffix=None, + startup_command=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if yaml: + # TODO: Implement yaml + raise CLIError("--yaml is not yet implemented") + + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + update_map = {} + update_map['secrets'] = secrets is not None + update_map['ingress'] = ingress or target_port or transport + update_map['registries'] = registry_server or registry_user or registry_pass + update_map['scale'] = min_replicas or max_replicas + update_map['container'] = image_name or env_vars or cpu or memory or startup_command or args + update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol + update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None + + if update_map['container'] and len(containerapp_def['properties']['template']['containers']) > 1: + raise CLIError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") + + if tags: + containerapp_def['tags'] = tags + + if revision_suffix is not None: + containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix + + # Containers + if image_name is not None: + containerapp_def["properties"]["template"]["containers"][0]["image"] = image_name + if env_vars is not None: + containerapp_def["properties"]["template"]["containers"][0]["env"] = parse_env_var_flags(env_vars) + if startup_command is not None: + containerapp_def["properties"]["template"]["containers"][0]["command"] = parse_list_of_strings(startup_command) + if args is not None: + containerapp_def["properties"]["template"]["containers"][0]["args"] = parse_list_of_strings(startup_command) + if cpu is not None or memory is not None: + resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] + if resources: + if cpu is not None: + resources["cpu"] = cpu + if memory is not None: + resources["memory"] = memory + else: + resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] = { + "cpu": cpu, + "memory": memory + } + + # 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 + + # Dapr + if update_map["dapr"]: + if "dapr" not in containerapp_def["properties"]["template"]: + containerapp_def["properties"]["template"]["dapr"] = {} + if dapr_enabled is not None: + containerapp_def["properties"]["template"]["dapr"]["daprEnabled"] = dapr_enabled + if dapr_app_id is not None: + containerapp_def["properties"]["template"]["dapr"]["appId"] = dapr_app_id + if dapr_app_port is not None: + containerapp_def["properties"]["template"]["dapr"]["appPort"] = dapr_app_port + if dapr_app_protocol is not None: + containerapp_def["properties"]["template"]["dapr"]["appProtocol"] = dapr_app_protocol + + # Configuration + if revisions_mode is not None: + containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode + + if update_map["ingress"]: + external_ingress = None + if ingress is not None: + if ingress.lower() == "internal": + external_ingress = False + elif ingress.lower() == "external": + external_ingress = True + containerapp_def["properties"]["configuration"]["external"] = external_ingress + + if target_port is not None: + containerapp_def["properties"]["configuration"]["targetPort"] = target_port + + config = containerapp_def["properties"]["configuration"] + if (config["targetPort"] is not None and config["external"] is None) or (config["targetPort"] is None and config["external"] is not None): + raise ValidationError("Usage error: must specify --target-port with --ingress") + + if transport is not None: + containerapp_def["properties"]["configuration"]["transport"] = transport + + # TODO: Need list_secrets API to do secrets before registries + + if update_map["registries"]: + 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 len(registries_def) == 0: # Adding new registry + if not(registry_server is not None and registry_user is not None and registry_pass is not None): + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") + + registry = RegistryCredentials + registry["server"] = registry_server + registry["username"] = registry_user + registries_def.append(registry) + elif len(registries_def) == 1: # Modifying single registry + if registry_server is not None: + registries_def[0]["server"] = registry_server + if registry_user is not None: + registries_def[0]["username"] = registry_user + else: # Multiple registries + raise ValidationError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") + + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + secrets_def = containerapp_def["properties"]["configuration"]["secrets"] + + registries_def[0]["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass, update_existing_secret=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) + + 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") From 25e8e1ab12af32871647537236770285a9f27ff8 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Feb 2022 14:52:07 -0800 Subject: [PATCH 018/177] Add scale command --- src/containerapp/azext_containerapp/_help.py | 8 +++ .../azext_containerapp/_params.py | 4 ++ .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 70 ++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 27b2c0c98f3..bb669b2a11f 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -109,6 +109,14 @@ text: az containerapp delete -g MyResourceGroup -n MyContainerapp """ +helps['containerapp scale'] = """ + type: command + short-summary: Set the min and max replicas for a Containerapp. + examples: + - name: Scale a Containerapp. + text: az containerapp scale -g MyResourceGroup -n MyContainerapp --min-replicas 1 --max-replicas 2 +""" + helps['containerapp show'] = """ type: command short-summary: Show details of a Containerapp. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c41e729e2d2..8c52a2eecc2 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,6 +66,10 @@ def load_arguments(self, _): c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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 scale') as c: + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + with self.argument_context('containerapp env') as c: c.argument('name', name_type, help='Name of the containerapp environment') c.argument('resource_group_name', arg_type=resource_group_name_type) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 330ac7234cb..d2cb9c22668 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -37,6 +37,7 @@ def load_command_table(self, _): g.custom_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()) + g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 68db52f3238..147dee12fd9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from platform import platform +from turtle import update from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait @@ -110,7 +111,7 @@ def create_containerapp(cmd, registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) config_def = Configuration - config_def["secrets"] = None # TODO: Uncomment secrets_def + config_def["secrets"] = secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def config_def["registries"] = [registries_def] @@ -345,6 +346,73 @@ def update_containerapp(cmd, handle_raw_exception(e) +def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_replicas=None, no_wait=False): + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + shouldWork = False # TODO: Should only setting minReplicas and maxReplicas in the body work? Or do we have to do a GET on the containerapp, add in secrets, then modify minReplicas and maxReplicas + if shouldWork: + updated_containerapp_def = { + "location": containerapp_def["location"], + "properties": { + "template": { + "scale": None + } + } + } + + if "scale" not in containerapp_def["properties"]["template"]: + updated_containerapp_def["properties"]["template"]["scale"] = {} + else: + updated_containerapp_def["properties"]["template"]["scale"] = containerapp_def["properties"]["template"]["scale"] + + if min_replicas is not None: + updated_containerapp_def["properties"]["template"]["scale"]["minReplicas"] = min_replicas + + if max_replicas is not None: + updated_containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=updated_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 scale 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) + else: + 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 + + del containerapp_def["properties"]["configuration"]["registries"] + del containerapp_def["properties"]["configuration"]["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) + + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp scale 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") From 4cce2e8968dec4d546bb401f460a8566ed2017b2 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 15 Feb 2022 11:46:47 -0800 Subject: [PATCH 019/177] Various validations, small fixes --- .../azext_containerapp/_clients.py | 36 +++++++++---------- .../azext_containerapp/_params.py | 20 +++++------ src/containerapp/azext_containerapp/_utils.py | 8 ++--- .../azext_containerapp/_validators.py | 7 +++- .../azext_containerapp/commands.py | 7 ---- src/containerapp/azext_containerapp/custom.py | 2 +- 6 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 9b2f0f89750..b4552ebfaeb 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -261,7 +261,7 @@ def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no return r.json() @classmethod - def update(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): + 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) @@ -273,7 +273,7 @@ def update(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wa name, api_version) - r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(kube_environment_envelope)) + r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(managed_environment_envelope)) if no_wait: return r.json() @@ -335,7 +335,7 @@ def show(cls, cmd, resource_group_name, name): @classmethod def list_by_subscription(cls, cmd, formatter=lambda x: x): - kube_list = [] + env_list = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -347,23 +347,23 @@ def list_by_subscription(cls, cmd, formatter=lambda x: x): r = send_raw_request(cmd.cli_ctx, "GET", request_url) j = r.json() - for kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + 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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + for env in j["value"]: + formatted = formatter(env) + env_list.append(formatted) - return kube_list + return env_list @classmethod def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): - kube_list = [] + env_list = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -377,16 +377,16 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) r = send_raw_request(cmd.cli_ctx, "GET", request_url) j = r.json() - for kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + 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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + for env in j["value"]: + formatted = formatter(env) + env_list.append(formatted) - return kube_list + return env_list diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 8c52a2eecc2..c36d70c5fea 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -12,7 +12,7 @@ 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) + validate_registry_user, validate_registry_pass, validate_target_port, validate_ingress) def load_arguments(self, _): @@ -74,21 +74,21 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the containerapp 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('logs_destination', options_list=['--logs-dest']) - c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') - c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') + c.argument('logs_destination', type=str, options_list=['--logs-dest']) + c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Log analytics workspace ID') + c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log analytics workspace key') c.argument('tags', arg_type=tags_type) with self.argument_context('containerapp env', arg_group='Dapr') as c: c.argument('instrumentation_key', options_list=['--instrumentation-key'], help='Azure Monitor 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', options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') - c.argument('app_subnet_resource_id', 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', 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', 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', 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', 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') + c.argument('infrastructure_subnet_resource_id', type=str, options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + 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 managed environment.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index c2565c651b6..c77d45f3557 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -63,8 +63,8 @@ def parse_env_var_flags(env_string, is_update_containerapp=False): env_pairs = {} for pair in env_pair_strings: - key_val = pair.split('=') - if len(key_val) is not 2: + key_val = pair.split('=', 1) + if len(key_val) != 2: if is_update_containerapp: raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\".") @@ -75,7 +75,7 @@ def parse_env_var_flags(env_string, is_update_containerapp=False): env_var_def = [] for key, value in env_pairs.items(): - if len(value) is 2: + if len(value) == 2: env_var_def.append({ "name": key, "secretRef": value[1] @@ -95,7 +95,7 @@ def parse_secret_flags(secret_string): for pair in secret_pair_strings: key_val = pair.split('=', 1) - if len(key_val) is not 2: + 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])) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 1f5913e3fed..b0dcb62a9e7 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -42,7 +42,12 @@ def validate_memory(namespace): raise ValidationError("Usage error: --memory must be a number ending with \"Gi\"") def validate_cpu(namespace): - return + if namespace.cpu: + cpu = namespace.cpu + try: + float(cpu) + except ValueError: + raise ValidationError("Usage error: --cpu must be a number eg. \"0.5\"") def validate_managed_env_name_or_id(cmd, namespace): from azure.cli.core.commands.client_factory import get_subscription_id diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index d2cb9c22668..177aee414b6 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -26,13 +26,6 @@ def transform_containerapp_list_output(apps): def load_command_table(self, _): - - # TODO: Add command type here - # containerapp_sdk = CliCommandType( - # operations_tmpl='.operations#None.{}', - # client_factory=cf_containerapp) - - with self.command_group('containerapp') as g: g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) g.custom_command('list', 'list_containerapp', table_transformer=transform_containerapp_list_output) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 147dee12fd9..c272a42c91e 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -493,7 +493,7 @@ def create_managed_environment(cmd, if infrastructure_subnet_resource_id is not None: if not app_subnet_resource_id: - raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') + raise ValidationError('App subnet resource ID needs to be supplied with infrastructure subnet resource ID.') vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id if app_subnet_resource_id is not None: From 074e1adacbb7200d52a8b1aabc9fcebfbf687e01 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 18 Feb 2022 07:57:42 -0800 Subject: [PATCH 020/177] listSecrets API for updates, autogen log analytics for env --- .../azext_containerapp/_client_factory.py | 9 ++ .../azext_containerapp/_clients.py | 38 ++++- src/containerapp/azext_containerapp/_help.py | 6 +- .../azext_containerapp/_params.py | 8 +- src/containerapp/azext_containerapp/_utils.py | 133 +++++++++++++++++- .../azext_containerapp/_validators.py | 3 +- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 104 +++++--------- 8 files changed, 224 insertions(+), 79 deletions(-) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 3ee674ace77..cc9da7661ec 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -72,6 +72,15 @@ 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 def cf_containerapp(cli_ctx, *_): diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index b4552ebfaeb..9575a1ced03 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -11,7 +11,9 @@ from sys import api_version 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" @@ -152,7 +154,14 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): resource_group_name, name, api_version) - poll(cmd, request_url, "cancelled") + + 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') return @classmethod @@ -229,6 +238,24 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) return app_list + @classmethod + def list_secrets(cls, cmd, resource_group_name, name): + secrets = [] + + 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() + class ManagedEnvironmentClient(): @classmethod @@ -314,7 +341,14 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): resource_group_name, name, api_version) - poll(cmd, request_url, "scheduledfordelete") + + 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 diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index bb669b2a11f..d6a4b353e15 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -148,7 +148,11 @@ type: command short-summary: Create a Containerapp environment. examples: - - name: Create a Containerapp Environment. + - name: Create a Containerapp Environment with an autogenerated Log Analytics + text: | + az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ + -- location Canada Central + - name: Create a Containerapp Environment with Log Analytics text: | az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ --logs-workspace-id myLogsWorkspaceID \\ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c36d70c5fea..184a3a0e100 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -74,11 +74,13 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the containerapp 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('logs_destination', type=str, options_list=['--logs-dest']) - c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Log analytics workspace ID') - c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log analytics workspace key') c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env', arg_group='Log Analytics') as c: + c.argument('logs_destination', type=str, options_list=['--logs-dest']) + 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=['--instrumentation-key'], help='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index c77d45f3557..573b5ead3a5 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -3,12 +3,15 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from distutils.filelist import findall from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) 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 urllib.parse import urlparse -from ._client_factory import providers_client_factory, cf_resource_groups +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__) @@ -34,7 +37,7 @@ def _validate_subscription_registered(cmd, resource_provider): pass -def _ensure_location_allowed(cmd, location, resource_provider): +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)) @@ -43,15 +46,15 @@ def _ensure_location_allowed(cmd, location, resource_provider): resource_types = getattr(providers_client.get(resource_provider), 'resource_types', []) res_locations = [] for res in resource_types: - if res and getattr(res, 'resource_type', "") == 'containerApps': + if res and getattr(res, 'resource_type', "") == resource_type: res_locations = getattr(res, 'locations', []) - res_locations = [res_loc.lower().replace(" ", "") for res_loc in res_locations if res_loc.strip()] + 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=='containerApps'].locations\"`".format( - location, resource_provider)) + 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: @@ -153,3 +156,121 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ def parse_list_of_strings(comma_separated_string): comma_separated = comma_separated_string.split(',') return [s.strip() for s in comma_separated] + + +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): + location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") + if location: + return location + + except Exception: + return default_location + return default_location + +# Generate random 4 character string +def _new_tiny_guid(): + import random, 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: + 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)) + + 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") + 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: + handle_raw_exception(e) + + containerapp_def["properties"]["configuration"]["secrets"] = secrets["value"] diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index b0dcb62a9e7..4b3286fa687 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -58,7 +58,8 @@ def validate_managed_env_name_or_id(cmd, namespace): namespace.managed_env = resource_id( subscription=get_subscription_id(cmd.cli_ctx), resource_group=namespace.resource_group_name, - namespace='Microsoft.App', type='managedEnvironments', + namespace='Microsoft.App', + type='managedEnvironments', name=namespace.managed_env ) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 177aee414b6..998e41cf3ae 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -39,5 +39,5 @@ def load_command_table(self, _): g.custom_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('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('update', 'update_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()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c272a42c91e..a2827b62eea 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -17,7 +17,8 @@ from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) 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_list_of_strings, parse_env_var_flags) + parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, parse_env_var_flags, + _generate_log_analytics_if_not_provided, _get_existing_secrets) logger = get_logger(__name__) @@ -46,6 +47,7 @@ def create_containerapp(cmd, dapr_app_id=None, dapr_app_protocol=None, # dapr_components=None, + revision_suffix=None, location=None, startup_command=None, args=None, @@ -54,7 +56,7 @@ def create_containerapp(cmd, 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") + _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: # TODO: Implement yaml @@ -80,7 +82,13 @@ def create_containerapp(cmd, if not managed_env_info: raise ValidationError("The environment '{}' does not exist. Specify a valid environment".format(managed_env)) - location = location or managed_env_info.location + if not location: + location = managed_env_info["location"] + elif location.lower() != managed_env_info["location"].lower(): + raise ValidationError("The location \"{}\" of the containerapp must be the same as the Managed Environment location \"{}\"".format( + location, + managed_env_info["location"] + )) external_ingress = None if ingress is not None: @@ -114,7 +122,7 @@ def create_containerapp(cmd, config_def["secrets"] = secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def - config_def["registries"] = [registries_def] + config_def["registries"] = [registries_def] if registries_def is not None else None scale_def = None if min_replicas is not None or max_replicas is not None: @@ -153,6 +161,9 @@ def create_containerapp(cmd, template_def["scale"] = scale_def template_def["dapr"] = dapr_def + if revision_suffix is not None: + template_def["revisionSuffix"] = revision_suffix + containerapp_def = ContainerApp containerapp_def["location"] = location containerapp_def["properties"]["managedEnvironmentId"] = managed_env @@ -301,7 +312,7 @@ def update_containerapp(cmd, if transport is not None: containerapp_def["properties"]["configuration"]["transport"] = transport - # TODO: Need list_secrets API to do secrets before registries + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) if update_map["registries"]: registries_def = None @@ -356,61 +367,27 @@ def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_re if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) - shouldWork = False # TODO: Should only setting minReplicas and maxReplicas in the body work? Or do we have to do a GET on the containerapp, add in secrets, then modify minReplicas and maxReplicas - if shouldWork: - updated_containerapp_def = { - "location": containerapp_def["location"], - "properties": { - "template": { - "scale": None - } - } - } - - if "scale" not in containerapp_def["properties"]["template"]: - updated_containerapp_def["properties"]["template"]["scale"] = {} - else: - updated_containerapp_def["properties"]["template"]["scale"] = containerapp_def["properties"]["template"]["scale"] - - if min_replicas is not None: - updated_containerapp_def["properties"]["template"]["scale"]["minReplicas"] = min_replicas - - if max_replicas is not None: - updated_containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - - try: - r = ContainerAppClient.create_or_update( - cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=updated_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 scale 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) - else: - if "scale" not in containerapp_def["properties"]["template"]: - containerapp_def["properties"]["template"]["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 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 + if max_replicas is not None: + containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - del containerapp_def["properties"]["configuration"]["registries"] - del containerapp_def["properties"]["configuration"]["secrets"] + _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) + 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 scale in progress. Please monitor the update using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp scale 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) + return r + except Exception as e: + handle_raw_exception(e) def show_containerapp(cmd, name, resource_group_name): @@ -441,10 +418,7 @@ def delete_containerapp(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") try: - r = ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - if not r and not no_wait: - logger.warning('Containerapp successfully deleted') - return r + 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) @@ -452,8 +426,8 @@ def delete_containerapp(cmd, name, resource_group_name, no_wait=False): def create_managed_environment(cmd, name, resource_group_name, - logs_customer_id, - logs_key, + logs_customer_id=None, + logs_key=None, logs_destination="log-analytics", location=None, instrumentation_key=None, @@ -469,7 +443,10 @@ def create_managed_environment(cmd, 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") + _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 = LogAnalyticsConfiguration log_analytics_config_def["customerId"] = logs_customer_id @@ -581,9 +558,6 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") try: - r = ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - if not r and not no_wait: - logger.warning('Containerapp environment successfully deleted') - return r + 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) From e1f2bfc7e67fe22436832687c86df29cae3bd598 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 09:10:10 -0800 Subject: [PATCH 021/177] Use space delimiter for secrets and env variables --- src/containerapp/azext_containerapp/_params.py | 4 ++-- src/containerapp/azext_containerapp/_utils.py | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 184a3a0e100..e851bc3639d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -34,7 +34,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', type=str, options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Comma-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") + c.argument('env_vars', nargs='*', options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -58,7 +58,7 @@ def load_arguments(self, _): c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") - c.argument('secrets', type=str, options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Comma-separated values in 'key=value' format.") + c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") # Ingress with self.argument_context('containerapp', arg_group='Ingress') as c: diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 573b5ead3a5..33da031e78d 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -61,11 +61,10 @@ def _ensure_location_allowed(cmd, location, resource_provider, resource_type): pass -def parse_env_var_flags(env_string, is_update_containerapp=False): - env_pair_strings = env_string.split(',') +def parse_env_var_flags(env_list, is_update_containerapp=False): env_pairs = {} - for pair in env_pair_strings: + for pair in env_list: key_val = pair.split('=', 1) if len(key_val) != 2: if is_update_containerapp: @@ -92,11 +91,10 @@ def parse_env_var_flags(env_string, is_update_containerapp=False): return env_var_def -def parse_secret_flags(secret_string): - secret_pair_strings = secret_string.split(',') +def parse_secret_flags(secret_list): secret_pairs = {} - for pair in secret_pair_strings: + for pair in secret_list: key_val = pair.split('=', 1) if len(key_val) != 2: raise ValidationError("--secrets: must be in format \"=,=,...\"") From 8d36f9a4c7e2a8bbe3099be282ccb64e387dc0e9 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 25 Feb 2022 09:14:26 -0800 Subject: [PATCH 022/177] Verify sub is registered to Microsoft.ContainerRegistration if creating vnet enabled env, remove logs-type parameter --- src/containerapp/azext_containerapp/_params.py | 1 - src/containerapp/azext_containerapp/custom.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index e851bc3639d..7c66cd3c526 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -77,7 +77,6 @@ def load_arguments(self, _): c.argument('tags', arg_type=tags_type) with self.argument_context('containerapp env', arg_group='Log Analytics') as c: - c.argument('logs_destination', type=str, options_list=['--logs-dest']) 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.') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a2827b62eea..1a0425f2d2f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -428,7 +428,6 @@ def create_managed_environment(cmd, resource_group_name, logs_customer_id=None, logs_key=None, - logs_destination="log-analytics", location=None, instrumentation_key=None, infrastructure_subnet_resource_id=None, @@ -445,6 +444,10 @@ def create_managed_environment(cmd, _validate_subscription_registered(cmd, "Microsoft.App") _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") + # Microsoft.ContainerService RP registration is required for vnet enabled environments + if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: + _validate_subscription_registered(cmd, "Microsoft.ContainerService") + 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) @@ -453,7 +456,7 @@ def create_managed_environment(cmd, log_analytics_config_def["sharedKey"] = logs_key app_logs_config_def = AppLogsConfiguration - app_logs_config_def["destination"] = logs_destination + app_logs_config_def["destination"] = "log-analytics" app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def managed_env_def = ManagedEnvironment From 725a841ab8cc8661b6eb3cb2c7c653c07a373c08 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Feb 2022 21:46:42 -0800 Subject: [PATCH 023/177] Containerapp create --yaml --- src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/_params.py | 8 +- .../azext_containerapp/_sdk_models.py | 3390 +++++++++++++++++ src/containerapp/azext_containerapp/_utils.py | 66 + .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 223 +- 6 files changed, 3662 insertions(+), 28 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_sdk_models.py diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index d6a4b353e15..05c2f63b96e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -60,7 +60,7 @@ - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - -- yaml "C:/path/to/yaml/file.yml" + --yaml "C:/path/to/yaml/file.yml" """ helps['containerapp update'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 7c66cd3c526..16a44fe17d5 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -7,7 +7,7 @@ from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, - get_resource_name_completion_list, + get_resource_name_completion_list, file_type, get_three_state_flag, get_enum_type, tags_type) from azure.cli.core.commands.validators import get_default_location_from_resource_group @@ -27,14 +27,14 @@ def load_arguments(self, _): 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', '-e'], help="Name or resource ID of the containerapp's environment.") - c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') + c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") + c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -62,7 +62,7 @@ def load_arguments(self, _): # Ingress with self.argument_context('containerapp', arg_group='Ingress') as c: - c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") + c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py new file mode 100644 index 00000000000..9472034039d --- /dev/null +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -0,0 +1,3390 @@ +# 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. +# -------------------------------------------------------------------------- + +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 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'}, + '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.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 dapr: Dapr configuration for the Container App. + :type dapr: ~commondefinitions.models.Dapr + :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'}, + 'dapr': {'key': 'dapr', 'type': 'Dapr'}, + '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.dapr = kwargs.get('dapr', 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 index 33da031e78d..0478500f032 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from distutils.filelist import findall +from operator import is_ from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger @@ -272,3 +273,68 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): handle_raw_exception(e) containerapp_def["properties"]["configuration"]["secrets"] = secrets["value"] + + +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 _object_to_dict(obj): + import json + return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) + + +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" + ] + + 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] diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 998e41cf3ae..ef15c7236e2 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -33,6 +33,7 @@ def load_command_table(self, _): g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('test', 'create_or_update_containerapp_yaml') with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1a0425f2d2f..00b7caf0148 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -10,19 +10,187 @@ from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger -from msrestazure.tools import parse_resource_id +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 -from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, - Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) +from ._sdk_models import * +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) 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_list_of_strings, parse_env_var_flags, - _generate_log_analytics_if_not_provided, _get_existing_secrets) + _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) 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: + return yaml.safe_load(stream) + except (IOError, OSError) as ex: + if getattr(ex, 'errno', 0) == errno.ENOENT: + raise CLIError('{} does not exist'.format(file_name)) + raise + except (yaml.parser.ParserError, UnicodeDecodeError) as ex: + raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) + + +def create_deserializer(): + from msrest import Deserializer + import sys, inspect + + sdkClasses = inspect.getmembers(sys.modules["azext_containerapp._sdk_models"]) + deserializer = {} + + for sdkClass in sdkClasses: + deserializer[sdkClass[0]] = sdkClass[1] + + return Deserializer(deserializer) + + +def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name, is_update, no_wait=False): + yaml_containerapp = process_loaded_yaml(load_yaml_file(file_name)) + if type(yaml_containerapp) != dict: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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 as ex: + pass + + if is_update and current_containerapp_def is None: + raise ValidationError("The containerapp '{}' does not exist".format(name)) + + # 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + # 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'): + if is_update: + containerapp_def["properties"]['managedEnvironmentId'] = current_containerapp_def["properties"]['managedEnvironmentId'] + else: + raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + managed_env_id = containerapp_def["properties"]['managedEnvironmentId'] + if not managed_env_id: + raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + env_name = None + env_rg = None + env_info = None + + if (is_valid_resource_id(managed_env_id)): + parsed_managed_env = parse_resource_id(managed_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'] + + # Secrets + if is_update: + add_secrets = [] + if containerapp_def["properties"]["configuration"].get('secrets'): + add_secrets = containerapp_def["properties"]["configuration"]["secrets"] + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + if add_secrets: + _add_or_update_secrets(containerapp_def, add_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) + + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp {} in progress. Please monitor the {} using `az containerapp show -n {} -g {}`'.format( + "update" if is_update else "creation", + name, + resource_group_name)) + + return r + except Exception as e: + handle_raw_exception(e) + + def create_containerapp(cmd, name, resource_group_name, @@ -59,8 +227,12 @@ def create_containerapp(cmd, _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: - # TODO: Implement yaml - raise CLIError("--yaml is not yet implemented") + if image_name 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\ + location 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=False, no_wait=no_wait) if image_name is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') @@ -99,7 +271,7 @@ def create_containerapp(cmd, ingress_def = None if target_port is not None and ingress is not None: - ingress_def = Ingress + ingress_def = IngressModel ingress_def["external"] = external_ingress ingress_def["targetPort"] = target_port ingress_def["transport"] = transport @@ -110,7 +282,7 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: - registries_def = RegistryCredentials + registries_def = RegistryCredentialsModel registries_def["server"] = registry_server registries_def["username"] = registry_user @@ -118,7 +290,7 @@ def create_containerapp(cmd, secrets_def = [] registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) - config_def = Configuration + config_def = ConfigurationModel config_def["secrets"] = secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def @@ -126,17 +298,17 @@ def create_containerapp(cmd, scale_def = None if min_replicas is not None or max_replicas is not None: - scale_def = Scale + 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 = ContainerResources + resources_def = ContainerResourcesModel resources_def["cpu"] = cpu resources_def["memory"] = memory - container_def = Container + container_def = ContainerModel container_def["name"] = name container_def["image"] = image_name if env_vars is not None: @@ -150,13 +322,13 @@ def create_containerapp(cmd, dapr_def = None if dapr_enabled: - dapr_def = Dapr + dapr_def = DaprModel dapr_def["daprEnabled"] = True dapr_def["appId"] = dapr_app_id dapr_def["appPort"] = dapr_app_port dapr_def["appProtocol"] = dapr_app_protocol - template_def = Template + template_def = TemplateModel template_def["containers"] = [container_def] template_def["scale"] = scale_def template_def["dapr"] = dapr_def @@ -164,7 +336,7 @@ def create_containerapp(cmd, if revision_suffix is not None: template_def["revisionSuffix"] = revision_suffix - containerapp_def = ContainerApp + containerapp_def = ContainerAppModel containerapp_def["location"] = location containerapp_def["properties"]["managedEnvironmentId"] = managed_env containerapp_def["properties"]["configuration"] = config_def @@ -215,8 +387,12 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - # TODO: Implement yaml - raise CLIError("--yaml is not yet implemented") + if image_name 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=True, no_wait=no_wait) containerapp_def = None try: @@ -313,6 +489,7 @@ def update_containerapp(cmd, containerapp_def["properties"]["configuration"]["transport"] = transport _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) if update_map["registries"]: registries_def = None @@ -327,7 +504,7 @@ def update_containerapp(cmd, if not(registry_server is not None and registry_user is not None and registry_pass is not None): raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") - registry = RegistryCredentials + registry = RegistryCredentialsModel registry["server"] = registry_server registry["username"] = registry_user registries_def.append(registry) @@ -451,15 +628,15 @@ def create_managed_environment(cmd, 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 = LogAnalyticsConfiguration + log_analytics_config_def = LogAnalyticsConfigurationModel log_analytics_config_def["customerId"] = logs_customer_id log_analytics_config_def["sharedKey"] = logs_key - app_logs_config_def = AppLogsConfiguration + app_logs_config_def = AppLogsConfigurationModel app_logs_config_def["destination"] = "log-analytics" app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def - managed_env_def = ManagedEnvironment + managed_env_def = ManagedEnvironmentModel managed_env_def["location"] = location managed_env_def["properties"]["internalLoadBalancerEnabled"] = False managed_env_def["properties"]["appLogsConfiguration"] = app_logs_config_def @@ -469,7 +646,7 @@ def create_managed_environment(cmd, managed_env_def["properties"]["daprAIInstrumentationKey"] = instrumentation_key if infrastructure_subnet_resource_id or app_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: - vnet_config_def = VnetConfiguration + vnet_config_def = VnetConfigurationModel if infrastructure_subnet_resource_id is not None: if not app_subnet_resource_id: @@ -518,7 +695,7 @@ def update_managed_environment(cmd, _validate_subscription_registered(cmd, "Microsoft.App") - managed_env_def = ManagedEnvironment + managed_env_def = ManagedEnvironmentModel managed_env_def["tags"] = tags try: From f6479f1e6eacfacf824d21daff3634a9ba6adf08 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Feb 2022 22:25:33 -0800 Subject: [PATCH 024/177] Fix updating registry to do create or update --- src/containerapp/azext_containerapp/custom.py | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 00b7caf0148..56162f0e759 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -500,28 +500,41 @@ def update_containerapp(cmd, registries_def = containerapp_def["properties"]["configuration"]["registries"] - if len(registries_def) == 0: # Adding new registry + if not registry_server: + raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + + # Check if updating existing registry + updating_existing_registry = False + for r in registries_def: + if r['server'].lower() == registry_server.lower(): + updating_existing_registry = True + + if registry_user: + r["username"] = registry_user + if registry_pass: + r["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + r["username"], + r["server"], + registry_pass, + update_existing_secret=True) + + # If not updating existing registry, add as new registry + if not updating_existing_registry: if not(registry_server is not None and registry_user is not None and registry_pass is not None): raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") registry = RegistryCredentialsModel registry["server"] = registry_server registry["username"] = registry_user - registries_def.append(registry) - elif len(registries_def) == 1: # Modifying single registry - if registry_server is not None: - registries_def[0]["server"] = registry_server - if registry_user is not None: - registries_def[0]["username"] = registry_user - else: # Multiple registries - raise ValidationError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") - - if "secrets" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["secrets"] = [] - secrets_def = containerapp_def["properties"]["configuration"]["secrets"] - - registries_def[0]["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass, update_existing_secret=True) + registry["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + registry_user, + registry_server, + registry_pass, + 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) From c98d02ce611b22c5596d0035cab77fc539cd2f78 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 23 Feb 2022 08:17:32 -0800 Subject: [PATCH 025/177] Fix containerapp update command. Add image-name parameter to support multi container updates. Fix updating registries, containers and secrets --- .../azext_containerapp/_params.py | 9 +- src/containerapp/azext_containerapp/_utils.py | 25 +++++ src/containerapp/azext_containerapp/custom.py | 97 +++++++++++++------ 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 16a44fe17d5..a664c5bfcc4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -31,12 +31,13 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: - c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") - c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") - c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") + c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'.") + c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'.") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0478500f032..0a092694e59 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -291,6 +291,31 @@ def _add_or_update_secrets(containerapp_def, add_secrets): containerapp_def["properties"]["configuration"]["secrets"].append(new_secret) +def _add_or_update_env_vars(existing_env_vars, new_env_vars): + 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 "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: + existing_env_vars.append(new_env_var) + + def _object_to_dict(obj): import json return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 56162f0e759..4bc0b277861 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -33,7 +33,8 @@ 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_list_of_strings, 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) + _object_to_dict, _add_or_update_secrets, _remove_additional_attributes, _remove_readonly_attributes, + _add_or_update_env_vars) logger = get_logger(__name__) @@ -195,6 +196,7 @@ def create_containerapp(cmd, name, resource_group_name, yaml=None, + image=None, image_name=None, managed_env=None, min_replicas=None, @@ -227,14 +229,14 @@ def create_containerapp(cmd, _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: - if image_name or managed_env or min_replicas or max_replicas or target_port or ingress or\ + 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\ location 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=False, no_wait=no_wait) - if image_name is None: + if image is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') if managed_env is None: @@ -309,8 +311,8 @@ def create_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = name - container_def["image"] = image_name + container_def["name"] = image_name if image_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: @@ -359,6 +361,7 @@ def update_containerapp(cmd, name, resource_group_name, yaml=None, + image=None, image_name=None, min_replicas=None, max_replicas=None, @@ -387,7 +390,7 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - if image_name or min_replicas or max_replicas or target_port or ingress or\ + if image 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: @@ -408,7 +411,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image_name or env_vars or cpu or memory or startup_command or args + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command or args update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -422,26 +425,62 @@ def update_containerapp(cmd, containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix # Containers - if image_name is not None: - containerapp_def["properties"]["template"]["containers"][0]["image"] = image_name - if env_vars is not None: - containerapp_def["properties"]["template"]["containers"][0]["env"] = parse_env_var_flags(env_vars) - if startup_command is not None: - containerapp_def["properties"]["template"]["containers"][0]["command"] = parse_list_of_strings(startup_command) - if args is not None: - containerapp_def["properties"]["template"]["containers"][0]["args"] = parse_list_of_strings(startup_command) - if cpu is not None or memory is not None: - resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] - if resources: - if cpu is not None: - resources["cpu"] = cpu - if memory is not None: - resources["memory"] = memory - else: - resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] = { - "cpu": cpu, - "memory": memory - } + if update_map["container"]: + if not image_name: + 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() == image_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + if env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + if startup_command is not None: + c["command"] = parse_list_of_strings(startup_command) + if args is not None: + c["args"] = parse_list_of_strings(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"] = image_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"] = parse_list_of_strings(startup_command) + if args is not None: + container_def["args"] = parse_list_of_strings(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"]: @@ -489,7 +528,9 @@ def update_containerapp(cmd, containerapp_def["properties"]["configuration"]["transport"] = transport _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) + + if secrets is not None: + _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) if update_map["registries"]: registries_def = None From b581786418073a1e6607ec564cd105a5661fa819 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 23 Feb 2022 11:42:24 -0800 Subject: [PATCH 026/177] started update with --yaml. Need to do create or update for when an attribute is a list of items --- src/containerapp/azext_containerapp/_utils.py | 32 +++- src/containerapp/azext_containerapp/custom.py | 152 ++++++++++++++---- 2 files changed, 151 insertions(+), 33 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0a092694e59..afd1834589e 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -316,6 +316,17 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars): existing_env_vars.append(new_env_var) +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 return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) @@ -345,6 +356,7 @@ def _remove_additional_attributes(o): for key in o: _remove_additional_attributes(o[key]) + def _remove_readonly_attributes(containerapp_def): unneeded_properties = [ "id", @@ -355,7 +367,8 @@ def _remove_readonly_attributes(containerapp_def): "latestRevisionName", "latestRevisionFqdn", "customDomainVerificationId", - "outboundIpAddresses" + "outboundIpAddresses", + "fqdn" ] for unneeded_property in unneeded_properties: @@ -363,3 +376,20 @@ def _remove_readonly_attributes(containerapp_def): del containerapp_def[unneeded_property] elif unneeded_property in containerapp_def['properties']: del containerapp_def['properties'][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 + import collections + + for key, val in new_dict.items(): + if isinstance(val, collections.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 \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4bc0b277861..2ea394304af 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -34,7 +34,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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_env_vars, _add_or_update_tags, update_nested_dictionary) logger = get_logger(__name__) @@ -82,7 +82,7 @@ def create_deserializer(): return Deserializer(deserializer) -def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name, is_update, no_wait=False): +def update_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: raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') @@ -106,7 +106,7 @@ def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name except Exception as ex: pass - if is_update and current_containerapp_def is None: + if not current_containerapp_def: raise ValidationError("The containerapp '{}' does not exist".format(name)) # Deserialize the yaml into a ContainerApp object. Need this since we're not using SDK @@ -129,27 +129,129 @@ def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name # 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) + + ''' + # Not sure if update should replace items that are a list, or do createOrUpdate. This commented out section is the implementation for createOrUpdate. + # (If a property is a list, do createOrUpdate, rather than just replace with new list) + + if 'properties' in containerapp_def and 'template' in containerapp_def['properties']: + # Containers + if 'containers' in containerapp_def['properties']['template'] and containerapp_def['properties']['template']['containers']: + for new_container in containerapp_def['properties']['template']['containers']: + if "name" not in new_container or not new_container["name"]: + raise ValidationError("The container name is not specified.") + + # Check if updating existing container + updating_existing_container = False + for existing_container in current_containerapp_def["properties"]["template"]["containers"]: + if existing_container['name'].lower() == new_container['name'].lower(): + updating_existing_container = True + + if 'image' in new_container and new_container['image']: + existing_container['image'] = new_container['image'] + if 'env' in new_container and new_container['env']: + if 'env' not in existing_container or not existing_container['env']: + existing_container['env'] = [] + _add_or_update_env_vars(existing_container['env'], new_container['env']) + if 'command' in new_container and new_container['command']: + existing_container['command'] = new_container['command'] + if 'args' in new_container and new_container['args']: + existing_container['args'] = new_container['args'] + if 'resources' in new_container and new_container['resources']: + if 'cpu' in new_container['resources'] and new_container['resources']['cpu'] is not None: + existing_container['resources']['cpu'] = new_container['resources']['cpu'] + if 'memory' in new_container['resources'] and new_container['resources']['memory'] is not None: + existing_container['resources']['memory'] = new_container['resources']['memory'] + + # If not updating existing container, add as new container + if not updating_existing_container: + current_containerapp_def["properties"]["template"]["containers"].append(new_container) + + # Traffic Weights + + # Secrets + + # Registries + + # Scale rules + + # Source Controls + + ''' + 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: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + # 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'): - if is_update: - containerapp_def["properties"]['managedEnvironmentId'] = current_containerapp_def["properties"]['managedEnvironmentId'] - else: - raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') - - managed_env_id = containerapp_def["properties"]['managedEnvironmentId'] - if not managed_env_id: raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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(managed_env_id)): - parsed_managed_env = parse_resource_id(managed_env_id) + 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: @@ -167,25 +269,14 @@ def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name if not containerapp_def.get('location'): containerapp_def['location'] = env_info['location'] - # Secrets - if is_update: - add_secrets = [] - if containerapp_def["properties"]["configuration"].get('secrets'): - add_secrets = containerapp_def["properties"]["configuration"]["secrets"] - - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - if add_secrets: - _add_or_update_secrets(containerapp_def, add_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) if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: - logger.warning('Containerapp {} in progress. Please monitor the {} using `az containerapp show -n {} -g {}`'.format( - "update" if is_update else "creation", - name, - resource_group_name)) + 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: @@ -234,7 +325,7 @@ def create_containerapp(cmd, registry_user or registry_pass or dapr_enabled or dapr_app_port or dapr_app_id or\ location 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=False, no_wait=no_wait) + return create_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait) if image is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') @@ -395,7 +486,7 @@ def update_containerapp(cmd, 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=True, no_wait=no_wait) + 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: @@ -415,11 +506,8 @@ def update_containerapp(cmd, update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None - if update_map['container'] and len(containerapp_def['properties']['template']['containers']) > 1: - raise CLIError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") - if tags: - containerapp_def['tags'] = tags + _add_or_update_tags(containerapp_def, tags) if revision_suffix is not None: containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix From 1a8b598a44521109b98499d63f34f2f89a4aab78 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 09:18:12 -0800 Subject: [PATCH 027/177] use space delimiter for startup_command and args, instead of comma delimiter --- src/containerapp/azext_containerapp/_params.py | 4 ++-- src/containerapp/azext_containerapp/commands.py | 1 - src/containerapp/azext_containerapp/custom.py | 12 ++++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index a664c5bfcc4..740a139afb0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -36,8 +36,8 @@ def load_arguments(self, _): c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") - c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'.") - c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'.") + c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\".") + c.argument('args', nargs='*', options_list=['--args'], help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\".") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index ef15c7236e2..998e41cf3ae 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -33,7 +33,6 @@ def load_command_table(self, _): g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('test', 'create_or_update_containerapp_yaml') with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2ea394304af..a96b7ac2d6b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -407,9 +407,9 @@ def create_containerapp(cmd, if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) if startup_command is not None: - container_def["command"] = parse_list_of_strings(startup_command) + container_def["command"] = startup_command if args is not None: - container_def["args"] = parse_list_of_strings(args) + container_def["args"] = args if resources_def is not None: container_def["resources"] = resources_def @@ -530,9 +530,9 @@ def update_containerapp(cmd, c["env"] = [] _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) if startup_command is not None: - c["command"] = parse_list_of_strings(startup_command) + c["command"] = startup_command if args is not None: - c["args"] = parse_list_of_strings(args) + 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: @@ -562,9 +562,9 @@ def update_containerapp(cmd, if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) if startup_command is not None: - container_def["command"] = parse_list_of_strings(startup_command) + container_def["command"] = startup_command if args is not None: - container_def["args"] = parse_list_of_strings(args) + container_def["args"] = args if resources_def is not None: container_def["resources"] = resources_def From 51be4deb5549d19cefb2f23601875ecc085d1a0b Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 1 Mar 2022 07:34:07 -0800 Subject: [PATCH 028/177] Traffic weights --- .../azext_containerapp/_params.py | 1 + src/containerapp/azext_containerapp/_utils.py | 38 ++++++++++++++++++- src/containerapp/azext_containerapp/custom.py | 22 +++++++---- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 740a139afb0..4662a35bb1f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,6 +66,7 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format.") with self.argument_context('containerapp scale') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index afd1834589e..524024589dd 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -392,4 +392,40 @@ def update_nested_dictionary(orig_dict, new_dict): else: if new_dict[key] is not None: orig_dict[key] = new_dict[key] - return orig_dict \ No newline at end of file + return orig_dict + + +def _is_valid_weight(weight): + try: + n = int(weight) + if n >= 0 and n <= 100: + return True + return False + except ValueError: + return False + + +def _add_or_update_traffic_Weights(containerapp_def, list_weights): + if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: + 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') + + for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: + if existing_weight["revisionName"].lower() == new_weight[0].lower(): + is_existing = True + existing_weight["weight"] = int(key_val[1]) + + if not is_existing: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ + "revisionName": key_val[0], + "weight": int(key_val[1]) + }) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a96b7ac2d6b..31510ad80a7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -34,7 +34,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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) + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _add_or_update_traffic_Weights) logger = get_logger(__name__) @@ -459,7 +459,7 @@ def update_containerapp(cmd, ingress=None, target_port=None, transport=None, - # traffic_weights=None, + traffic_weights=None, revisions_mode=None, secrets=None, env_vars=None, @@ -499,7 +499,7 @@ def update_containerapp(cmd, update_map = {} update_map['secrets'] = secrets is not None - update_map['ingress'] = ingress or target_port or transport + update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command or args @@ -597,23 +597,31 @@ def update_containerapp(cmd, containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode if update_map["ingress"]: + if "ingress" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["ingress"] = {} + external_ingress = None if ingress is not None: if ingress.lower() == "internal": external_ingress = False elif ingress.lower() == "external": external_ingress = True - containerapp_def["properties"]["configuration"]["external"] = external_ingress + + if external_ingress is not None: + containerapp_def["properties"]["configuration"]["ingress"]["external"] = external_ingress if target_port is not None: - containerapp_def["properties"]["configuration"]["targetPort"] = target_port + containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - config = containerapp_def["properties"]["configuration"] + config = containerapp_def["properties"]["configuration"]["ingress"] if (config["targetPort"] is not None and config["external"] is None) or (config["targetPort"] is None and config["external"] is not None): raise ValidationError("Usage error: must specify --target-port with --ingress") if transport is not None: - containerapp_def["properties"]["configuration"]["transport"] = transport + containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport + + if traffic_weights is not None: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = _add_or_update_traffic_Weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) From 1b3bb9ce123e9cf9e6e65bac1e44949bd0ed4641 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 08:34:24 -0800 Subject: [PATCH 029/177] List and show revisions --- .../azext_containerapp/_clients.py | 49 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 18 +++++++ .../azext_containerapp/_params.py | 3 ++ src/containerapp/azext_containerapp/_utils.py | 10 ++++ .../azext_containerapp/commands.py | 21 ++++++++ src/containerapp/azext_containerapp/custom.py | 20 +++++++- 6 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 9575a1ced03..1a3a17bcc14 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -256,6 +256,55 @@ def list_secrets(cls, cmd, resource_group_name, name): 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() + class ManagedEnvironmentClient(): @classmethod diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 05c2f63b96e..e452af3eb04 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -138,6 +138,24 @@ az containerapp list -g MyResourceGroup """ +helps['containerapp revision show'] = """ + type: command + short-summary: Show details of a Containerapp's revision. + examples: + - name: Show details of a Containerapp's revision. + text: | + az containerapp revision show --revision-name MyContainerappRevision -g MyResourceGroup +""" + +helps['containerapp revision list'] = """ + type: command + short-summary: List details of a Containerapp's revisions. + examples: + - name: List a Containerapp's revisions. + text: | + az containerapp revision list -n MyContainerapp -g MyResourceGroup +""" + # Environment Commands helps['containerapp env'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 4662a35bb1f..913b4ee502d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -102,3 +102,6 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the managed Environment.') + + with self.argument_context('containerapp revision') as c: + c.argument('revision_name', type=str, help='Name of the revision') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 524024589dd..63006d1aae4 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -429,3 +429,13 @@ def _add_or_update_traffic_Weights(containerapp_def, list_weights): "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 diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 998e41cf3ae..da8269470af 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -25,6 +25,20 @@ def transform_containerapp_list_output(apps): return [transform_containerapp_output(a) for a in apps] +def transform_revision_output(rev): + props = ['name', 'replicas', 'active', 'createdTime'] + result = {k: rev[k] for k in rev if k in props} + + if 'latestRevisionFqdn' in rev['template']: + result['fqdn'] = rev['template']['latestRevisionFqdn'] + + 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') as g: g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) @@ -41,3 +55,10 @@ def load_command_table(self, _): g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_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 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_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 31510ad80a7..5014957291b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -34,7 +34,8 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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, _add_or_update_traffic_Weights) + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _add_or_update_traffic_Weights, + _get_app_from_revision) logger = get_logger(__name__) @@ -891,3 +892,20 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): 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 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) From 00602b60da2556ea7b442bbe02e94f00a7dedcf5 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 10:39:06 -0800 Subject: [PATCH 030/177] az containerapp revision restart, activate, deactivate --- .../azext_containerapp/_clients.py | 50 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 35 ++++++++++++- .../azext_containerapp/commands.py | 6 +-- src/containerapp/azext_containerapp/custom.py | 30 +++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 1a3a17bcc14..4d525bee181 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -305,6 +305,56 @@ def show_revision(cls, cmd, resource_group_name, container_app_name, name): 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 diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index e452af3eb04..b32ac7f7b90 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -138,6 +138,12 @@ az containerapp list -g MyResourceGroup """ +# Revision Commands +helps['containerapp revision'] = """ + type: group + short-summary: Commands to manage a Containerapp's revisions. +""" + helps['containerapp revision show'] = """ type: command short-summary: Show details of a Containerapp's revision. @@ -153,7 +159,34 @@ examples: - name: List a Containerapp's revisions. text: | - az containerapp revision list -n MyContainerapp -g MyResourceGroup + az containerapp revision list --revision-name MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision restart'] = """ + type: command + short-summary: Restart a Containerapps's revision. + examples: + - name: Restart a Containerapp's revision. + text: | + az containerapp revision restart --revision-name MyContainerappRevision -g MyResourceGroup +""" + +helps['containerapp revision activate'] = """ + type: command + short-summary: Activates Containerapp's revision. + examples: + - name: Activate a Containerapp's revision. + text: | + az containerapp revision activate --revision-name MyContainerappRevision -g MyResourceGroup +""" + +helps['containerapp revision deactivate'] = """ + type: command + short-summary: Deactivates Containerapp's revision. + examples: + - name: Deactivate a Containerapp's revision. + text: | + az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup """ # Environment Commands diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index da8269470af..20d7c332c0d 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -57,8 +57,8 @@ def load_command_table(self, _): g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, 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('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_command('restart', 'restart_revision') g.custom_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5014957291b..ae27c6474b7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -909,3 +909,33 @@ def show_revision(cmd, resource_group_name, revision_name, name=None): 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) + From d0341885809917df27c37f1d099fc517aae7b9d5 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Mar 2022 08:58:01 -0800 Subject: [PATCH 031/177] Add ability for users to clear args/command in az containerapp update --- .../azext_containerapp/_params.py | 4 +-- .../azext_containerapp/azext_metadata.json | 3 +- src/containerapp/azext_containerapp/custom.py | 29 ++++++++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 913b4ee502d..52453298085 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -36,8 +36,8 @@ def load_arguments(self, _): c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") - c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\".") - c.argument('args', nargs='*', options_list=['--args'], help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\".") + c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") + c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json index c2d0f4fe8d0..55c81bf3328 100644 --- a/src/containerapp/azext_containerapp/azext_metadata.json +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -1,5 +1,4 @@ { "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67", - "azext.maxCliCoreVersion": "2.33.0" + "azext.minCliCoreVersion": "2.0.67" } \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ae27c6474b7..00add15382d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,8 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from platform import platform -from turtle import update from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait @@ -503,7 +501,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command or args + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -516,7 +514,10 @@ def update_containerapp(cmd, # Containers if update_map["container"]: if not image_name: - raise ValidationError("Usage error: --image-name is required when adding or updating a container") + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + image_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 @@ -531,9 +532,15 @@ def update_containerapp(cmd, c["env"] = [] _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) if startup_command is not None: - c["command"] = startup_command + if isinstance(startup_command, list) and not startup_command: + c["command"] = None + else: + c["command"] = startup_command if args is not None: - c["args"] = args + 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: @@ -563,9 +570,15 @@ def update_containerapp(cmd, 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 isinstance(startup_command, list) and not startup_command: + container_def["command"] = None + else: + container_def["command"] = startup_command if args is not None: - container_def["args"] = args + 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 From 788bb3fa654bd30f1105a9292c18e32d9bc75867 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Mar 2022 12:46:57 -0800 Subject: [PATCH 032/177] Various fixes, traffic weights fixes --- .../azext_containerapp/__init__.py | 3 +-- .../azext_containerapp/_client_factory.py | 19 +------------------ src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 9 ++------- .../azext_containerapp/_validators.py | 15 --------------- .../azext_containerapp/azext_metadata.json | 2 +- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 8 ++------ src/containerapp/setup.py | 2 +- 10 files changed, 11 insertions(+), 53 deletions(-) diff --git a/src/containerapp/azext_containerapp/__init__.py b/src/containerapp/azext_containerapp/__init__.py index e19af22d9e8..f772766731c 100644 --- a/src/containerapp/azext_containerapp/__init__.py +++ b/src/containerapp/azext_containerapp/__init__.py @@ -12,10 +12,9 @@ class ContainerappCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - from azext_containerapp._client_factory import cf_containerapp containerapp_custom = CliCommandType( operations_tmpl='azext_containerapp.custom#{}', - client_factory=cf_containerapp) + client_factory=None) super(ContainerappCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=containerapp_custom) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index cc9da7661ec..f998486c63e 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -10,7 +10,7 @@ # pylint: disable=inconsistent-return-statements -def ex_handler_factory(creating_plan=False, no_throw=False): +def ex_handler_factory(no_throw=False): def _polish_bad_errors(ex): import json from knack.util import CLIError @@ -21,15 +21,6 @@ def _polish_bad_errors(ex): elif 'Message' in content: detail = content['Message'] - if creating_plan: - if 'Requested features are not supported in region' in detail: - detail = ("Plan with linux worker is not supported in current region. For " + - "supported regions, please refer to https://docs.microsoft.com/" - "azure/app-service-web/app-service-linux-intro") - elif 'Not enough available reserved instance servers to satisfy' in detail: - detail = ("Plan with Linux worker can only be created in a group " + - "which has never contained a Windows worker, and vice versa. " + - "Please use a new resource group. Original error:" + detail) ex = CLIError(detail) except Exception: # pylint: disable=broad-except pass @@ -81,11 +72,3 @@ 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 - -def cf_containerapp(cli_ctx, *_): - - from azure.cli.core.commands.client_factory import get_mgmt_service_client - # TODO: Replace CONTOSO with the appropriate label and uncomment - # from azure.mgmt.CONTOSO import CONTOSOManagementClient - # return get_mgmt_service_client(cli_ctx, CONTOSOManagementClient) - return None diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index b32ac7f7b90..ac9638014c7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -218,7 +218,7 @@ helps['containerapp env delete'] = """ type: command - short-summary: Deletes a Containerapp Environment. + short-summary: Delete a Containerapp Environment. examples: - name: Delete Containerapp Environment. text: az containerapp env delete -g MyResourceGroup -n MyContainerappEnvironment diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 52453298085..c38c32711c4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,7 +66,7 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") - c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format.") + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. 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, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 63006d1aae4..d0c5c996650 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -405,8 +405,8 @@ def _is_valid_weight(weight): return False -def _add_or_update_traffic_Weights(containerapp_def, list_weights): - if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: +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: @@ -419,11 +419,6 @@ def _add_or_update_traffic_Weights(containerapp_def, list_weights): if not _is_valid_weight(key_val[1]): raise ValidationError('Traffic weights must be integers between 0 and 100') - for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: - if existing_weight["revisionName"].lower() == new_weight[0].lower(): - is_existing = True - existing_weight["weight"] = int(key_val[1]) - if not is_existing: containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ "revisionName": key_val[0], diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 4b3286fa687..23ed260e360 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -7,21 +7,6 @@ from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) -def example_name_or_id_validator(cmd, namespace): - # Example of a storage account name or ID validator. - # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters - from azure.cli.core.commands.client_factory import get_subscription_id - from msrestazure.tools import is_valid_resource_id, resource_id - if namespace.storage_account: - if not is_valid_resource_id(namespace.RESOURCE): - namespace.storage_account = resource_id( - subscription=get_subscription_id(cmd.cli_ctx), - resource_group=namespace.resource_group_name, - namespace='Microsoft.Storage', - type='storageAccounts', - name=namespace.storage_account - ) - def _is_number(s): try: float(s) diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json index 55c81bf3328..001f223de90 100644 --- a/src/containerapp/azext_containerapp/azext_metadata.json +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.isPreview": true, "azext.minCliCoreVersion": "2.0.67" -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 20d7c332c0d..8fd840ccabd 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -6,7 +6,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType from msrestazure.tools import is_valid_resource_id, parse_resource_id -from azext_containerapp._client_factory import cf_containerapp, ex_handler_factory +from azext_containerapp._client_factory import ex_handler_factory def transform_containerapp_output(app): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 00add15382d..005bb52d9af 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -32,7 +32,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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, _add_or_update_traffic_Weights, + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_Weights, _get_app_from_revision) logger = get_logger(__name__) @@ -627,15 +627,11 @@ def update_containerapp(cmd, if target_port is not None: containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - config = containerapp_def["properties"]["configuration"]["ingress"] - if (config["targetPort"] is not None and config["external"] is None) or (config["targetPort"] is None and config["external"] is not None): - raise ValidationError("Usage error: must specify --target-port with --ingress") - if transport is not None: containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport if traffic_weights is not None: - containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = _add_or_update_traffic_Weights(containerapp_def, traffic_weights) + _update_traffic_Weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index b9f57ada671..be4cd26f637 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -57,4 +57,4 @@ packages=find_packages(), install_requires=DEPENDENCIES, package_data={'azext_containerapp': ['azext_metadata.json']}, -) \ No newline at end of file +) From a5acf0774bb641fbd28c35b1e67f89dab5de4a95 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 4 Mar 2022 10:49:08 -0800 Subject: [PATCH 033/177] Verify subnet subscription is registered to Microsoft.ContainerServices --- src/containerapp/azext_containerapp/_utils.py | 11 +++++++---- src/containerapp/azext_containerapp/custom.py | 7 ++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index d0c5c996650..0ed9d21bf43 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -23,15 +23,18 @@ def _get_location_from_resource_group(cli_ctx, resource_group_name): return group.location -def _validate_subscription_registered(cmd, resource_provider): +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, get_subscription_id(cmd.cli_ctx)) + 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( - resource_provider, resource_provider)) + 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: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 005bb52d9af..3ef91290a5f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -783,7 +783,12 @@ def create_managed_environment(cmd, # Microsoft.ContainerService RP registration is required for vnet enabled environments if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: - _validate_subscription_registered(cmd, "Microsoft.ContainerService") + if (is_valid_resource_id(app_subnet_resource_id)): + parsed_app_subnet_resource_id = parse_resource_id(app_subnet_resource_id) + subnet_subscription = parsed_app_subnet_resource_id["subscription"] + _validate_subscription_registered(cmd, "Microsoft.ContainerService", subnet_subscription) + else: + raise ValidationError('Subnet resource ID is invalid.') 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) From 9b66d1723ac843241162cbedef9403bb1fa01983 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:38:30 -0500 Subject: [PATCH 034/177] GitHub Actions Update (#17) * Added models. Finished transferring Calvin's previous work. * Updated wrong models. * Updated models in custom.py, added githubactionclient. * Updated envelope to be correct. * Small bug fixes. * Updated error handling. Fixed bugs. Initial working state. * Added better error handling. * Added error messages for tokens with inappropriate access rights. * Added back get_acr_cred. * Fixed problems from merge conflict. * Updated names of imports from ._models.py to fix pylance erros. * Removed random imports. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_clients.py | 84 +++++++- .../azext_containerapp/_github_oauth.py | 86 ++++++++ src/containerapp/azext_containerapp/_help.py | 47 +++++ .../azext_containerapp/_models.py | 32 +++ .../azext_containerapp/_params.py | 17 ++ src/containerapp/azext_containerapp/_utils.py | 9 +- .../azext_containerapp/commands.py | 5 + src/containerapp/azext_containerapp/custom.py | 199 +++++++++++++++++- 8 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_github_oauth.py diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 4d525bee181..8184e6d86e2 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -2,8 +2,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - -from ast import NotEq import json import time import sys @@ -523,3 +521,85 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) 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() + + #TODO + @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 diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py new file mode 100644 index 00000000000..3df73a6b1aa --- /dev/null +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -0,0 +1,86 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.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)) + + raise UnclassifiedUserFault('Activation did not happen in time. Please try again') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index ac9638014c7..33f196f133e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -244,3 +244,50 @@ text: | az containerapp env list -g MyResourceGroup """ +helps['containerapp github-action add'] = """ + type: command + short-summary: Adds GitHub Actions to the Containerapp + 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 Dockerhub 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: Removes GitHub Actions from the Containerapp + examples: + - name: Removes GitHub Actions, personal access token. + text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp + --token MyAccessToken + - name: Removes 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 Containerapp + examples: + - name: Show the GitHub Actions configuration on a Containerapp + text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp +""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index f0d068b1bbc..6e8947ee58c 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -180,3 +180,35 @@ }, "tags": None } + +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 +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c38c32711c4..ac3b640b40e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -103,5 +103,22 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the managed 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 "master" 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 url of the registry, 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', type=str, help='Name of the revision') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0ed9d21bf43..83b707640f5 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,7 +5,8 @@ from distutils.filelist import findall from operator import is_ -from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.azclierror import (ResourceNotFoundError, 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 @@ -159,6 +160,12 @@ 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" diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 8fd840ccabd..fed17d21da0 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -56,6 +56,11 @@ def load_command_table(self, _): # g.custom_command('update', 'update_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 github-action') as g: + g.custom_command('add', 'create_or_update_github_action', exception_handler=ex_handler_factory()) + g.custom_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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 3ef91290a5f..bac77b3ab61 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -5,15 +5,19 @@ from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger +from urllib.parse import urlparse + 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 +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient from ._sdk_models import * +from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, VnetConfiguration as VnetConfigurationModel, @@ -27,13 +31,13 @@ Dapr as DaprModel, ContainerResources as ContainerResourcesModel, Scale as ScaleModel, - Container as ContainerModel) + Container as ContainerModel, GitHubActionConfiguration, RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, SourceControl as SourceControlModel) 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_list_of_strings, 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) + _get_app_from_revision, raise_missing_token_suggestion) logger = get_logger(__name__) @@ -908,6 +912,195 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): handle_raw_exception(e) +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, UnknownObjectException + + 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 CLIError("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 CLIError(error_msg) + logger.warning('Verified GitHub repo and branch') + except BadCredentialsException: + raise CLIError("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.") + 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 CLIError(error_msg) + except CLIError as clierror: + raise clierror + except Exception as ex: + # 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 = client.get_source_control_info(resource_group_name, name).properties + 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\"') + 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"] = "master" + + 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 not registry_username or not registry_password: + # 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') + + 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, UnknownObjectException + + 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 CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) + except BadCredentialsException: + raise CLIError("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.") + 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 CLIError(error_msg) + except CLIError as clierror: + raise clierror + except Exception as ex: + # 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) From a12c86d21c42c39f44cc4fddc457f708fc896019 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 7 Mar 2022 16:03:33 -0800 Subject: [PATCH 035/177] Remove --location since location must be same as managed env --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/custom.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ac3b640b40e..c6d27b2d97f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -26,7 +26,7 @@ def load_arguments(self, _): 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', '-e'], help="Name or resource ID of the containerapp's environment.") + c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment'], help="Name or resource ID of the containerapp's environment.") c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bac77b3ab61..1ba0da1c6ef 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -312,15 +312,11 @@ def create_containerapp(cmd, dapr_app_protocol=None, # dapr_components=None, revision_suffix=None, - location=None, startup_command=None, args=None, 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", "containerApps") if yaml: if image or managed_env or min_replicas or max_replicas or target_port or ingress or\ @@ -350,13 +346,8 @@ def create_containerapp(cmd, if not managed_env_info: raise ValidationError("The environment '{}' does not exist. Specify a valid environment".format(managed_env)) - if not location: - location = managed_env_info["location"] - elif location.lower() != managed_env_info["location"].lower(): - raise ValidationError("The location \"{}\" of the containerapp must be the same as the Managed Environment location \"{}\"".format( - location, - managed_env_info["location"] - )) + location = managed_env_info["location"] + _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") external_ingress = None if ingress is not None: From d561c461f1bb6faad8e9f11e5b1736067484eba7 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:28:52 -0800 Subject: [PATCH 036/177] Add options for flag names: --env-vars and --registry-srever --- src/containerapp/azext_containerapp/_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c6d27b2d97f..d5c9428d0c6 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -56,7 +56,7 @@ def load_arguments(self, _): # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, e.g. myregistry.azurecr.io") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server', '--registry-login-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") From dd3d7995ff2dcb7c47fa03f210476944b094bd37 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:29:50 -0800 Subject: [PATCH 037/177] Empty string to clear env_vars --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 4 ++-- src/containerapp/azext_containerapp/custom.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index d5c9428d0c6..6bec838fb93 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 83b707640f5..16cac247433 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -73,8 +73,8 @@ def parse_env_var_flags(env_list, is_update_containerapp=False): key_val = pair.split('=', 1) if len(key_val) != 2: if is_update_containerapp: - raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") - raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\".") + 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:') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1ba0da1c6ef..45c557eb026 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -496,7 +496,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['container'] = image or image_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -523,9 +523,12 @@ def update_containerapp(cmd, if image is not None: c["image"] = image if env_vars is not None: - if "env" not in c or not c["env"]: + if isinstance(env_vars, list) and not env_vars: c["env"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + else: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) if startup_command is not None: if isinstance(startup_command, list) and not startup_command: c["command"] = None From ea87b775d53816b4b686a2238246414805ab8f7a Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:51:49 -0800 Subject: [PATCH 038/177] Default revisions_mode to single --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 45c557eb026..bdc83fdf2a6 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -298,7 +298,7 @@ def create_containerapp(cmd, target_port=None, transport="auto", ingress=None, - revisions_mode=None, + revisions_mode="single", secrets=None, env_vars=None, cpu=None, From 44edebbb0e436bed521b2675571ee7844aa78d01 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 14:17:14 -0800 Subject: [PATCH 039/177] Infer acr credentials if it is acr and credentials are not provided --- src/containerapp/azext_containerapp/_utils.py | 17 ++++++++++++++++- .../azext_containerapp/_validators.py | 11 ++++++----- src/containerapp/azext_containerapp/custom.py | 13 +++++++++++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 16cac247433..02b436597c8 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,8 +5,8 @@ from distutils.filelist import findall from operator import is_ +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (ResourceNotFoundError, 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 @@ -444,3 +444,18 @@ def _get_app_from_revision(revision): 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 url is required if using Azure Container Registry, otherwise Registry username and password are required.') + 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)) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 23ed260e360..c95d675cb00 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -52,19 +52,20 @@ 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: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if ".azurecr.io" not in namespace.registry_server: + raise ValidationError("Usage error: --registry-login-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: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if not namespace.registry_server or (not namespace.registry_pass and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-login-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_user or not namespace.registry_server: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if not namespace.registry_server or (not namespace.registry_user and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-login-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(): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bdc83fdf2a6..5da806288fb 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger @@ -37,7 +37,7 @@ _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) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials) logger = get_logger(__name__) @@ -370,6 +370,11 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel + + # Infer credentials if not supplied and its azurecr + if not registry_user or not registry_pass: + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + registries_def["server"] = registry_server registries_def["username"] = registry_user @@ -648,6 +653,10 @@ def update_containerapp(cmd, if not registry_server: raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + # Infer credentials if not supplied and its azurecr + if not registry_user or not registry_pass: + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + # Check if updating existing registry updating_existing_registry = False for r in registries_def: From 6ae589ccac73c81100f186581e5833bdd3f68b97 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 14:49:36 -0800 Subject: [PATCH 040/177] fix help msg --- src/containerapp/azext_containerapp/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 33f196f133e..f4d7713ce93 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -90,7 +90,7 @@ --registry-password mypassword - name: Update a Containerapp using a specified startup command and arguments text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ + az containerapp update -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage \\ --command "/bin/sh" --args "-c", "while true; do echo hello; sleep 10;done" From 47e49f3119bd6329450cd70a6275154d90d0576c Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 17:25:47 -0800 Subject: [PATCH 041/177] if image is hosted on acr, and no registry server is supplied, infer the registry server --- src/containerapp/azext_containerapp/custom.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5da806288fb..d346cc75f65 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -367,6 +367,11 @@ def create_containerapp(cmd, if secrets is not None: secrets_def = parse_secret_flags(secrets) + # If ACR image and registry_server is not supplied, infer it + if image and '.azurecr.io' in image: + if not registry_server: + registry_server = image.split('/')[0] + registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel @@ -496,6 +501,11 @@ def update_containerapp(cmd, if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) + # If ACR image and registry_server is not supplied, infer it + if image and '.azurecr.io' in image: + if not registry_server: + registry_server = image.split('/')[0] + update_map = {} update_map['secrets'] = secrets is not None update_map['ingress'] = ingress or target_port or transport or traffic_weights From 5b58d6fee397e23833a1e4fb1209364894dab2cb Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Fri, 11 Mar 2022 13:12:52 -0500 Subject: [PATCH 042/177] Added subgroups (Ingress, Registry, Secret) and updated revisions (#18) * Added ingress subgroup. * Added help for ingress. * Fixed ingress traffic help. * Added registry commands. * Updated registry remove util to clear secrets if none remaining. Added warning when updating existing registry. Added registry help. * Changed registry delete to remove. * Added error message if user tries to remove non assigned registry. * Changed registry add back to registry set. * Added secret subgroup commands. * Removed yaml support from secret set. * Changed secret add to secret set. Updated consistency between secret set and secret delete. Added secret help. Require at least one secret passed with --secrets for secret commands. * Changed param name for secret delete from --secrets to --secret-names. Updated help. * Changed registry remove to registry delete. * Fixed bug in registry delete. * Added revision mode set and revision copy. * Modified update_containerapp_yaml to support updating from non-current revision. Authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/_help.py | 171 +++++ .../azext_containerapp/_models.py | 2 +- .../azext_containerapp/_params.py | 20 +- src/containerapp/azext_containerapp/_utils.py | 17 + .../azext_containerapp/commands.py | 25 + src/containerapp/azext_containerapp/custom.py | 599 +++++++++++++++++- 6 files changed, 830 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index f4d7713ce93..6122a3d895a 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -189,6 +189,24 @@ az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup """ +helps['containerapp revision mode set'] = """ + type: command + short-summary: Set the revision mode of a Containerapp. + examples: + - name: Set the revision mode of a Containerapp. + text: | + az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi +""" + # Environment Commands helps['containerapp env'] = """ type: group @@ -244,6 +262,159 @@ text: | az containerapp env list -g MyResourceGroup """ + +# Ingress Commands +helps['containerapp ingress'] = """ + type: group + short-summary: Commands to manage Containerapp ingress. +""" + +helps['containerapp ingress traffic'] = """ + type: subgroup + short-summary: Commands to manage Containerapp ingress traffic. +""" + +helps['containerapp ingress show'] = """ + type: command + short-summary: Show details of a Containerapp ingress. + examples: + - name: Show the details of a Containerapp ingress. + text: | + az containerapp ingress show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress enable'] = """ + type: command + short-summary: Enable Containerapp ingress. + examples: + - name: Enable Containerapp ingress. + 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 Containerapp ingress. + examples: + - name: Disable Containerapp ingress. + text: | + az containerapp ingress disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress traffic'] = """ + type: group + short-summary: Commands to manage Containerapp ingress traffic. +""" + +helps['containerapp ingress traffic set'] = """ + type: command + short-summary: Set Containerapp ingress traffic. + examples: + - name: Set Containerapp ingress traffic. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=100 +""" + +helps['containerapp ingress traffic show'] = """ + type: command + short-summary: Show Containerapp ingress traffic. + examples: + - name: Show Containerapp ingress traffic. + text: | + az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup +""" + +# Registry Commands +helps['containerapp registry'] = """ + type: group + short-summary: Commands to manage Containerapp registries. +""" + +helps['containerapp registry show'] = """ + type: command + short-summary: Show details of a Containerapp registry. + examples: + - name: Show the details of a Containerapp registry. + text: | + az containerapp registry show -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +helps['containerapp registry list'] = """ + type: command + short-summary: List registries assigned to a Containerapp. + examples: + - name: Show the details of a Containerapp registry. + text: | + az containerapp registry list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp registry set'] = """ + type: command + short-summary: Add or update a Containerapp registry. + examples: + - name: Add a registry to a Containerapp. + text: | + az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + - name: Update a Containerapp registry. + text: | + az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword + +""" + +helps['containerapp registry delete'] = """ + type: command + short-summary: Delete a registry from a Containerapp. + examples: + - name: Delete a registry from a Containerapp. + text: | + az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +# Secret Commands +helps['containerapp secret'] = """ + type: group + short-summary: Commands to manage Containerapp secrets. +""" + +helps['containerapp secret show'] = """ + type: command + short-summary: Show details of a Containerapp secret. + examples: + - name: Show the details of a Containerapp 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 Containerapp. + examples: + - name: List the secrets of a Containerapp. + text: | + az containerapp secret list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp secret delete'] = """ + type: command + short-summary: Delete secrets from a Containerapp. + examples: + - name: Delete secrets from a Containerapp. + text: | + az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 +""" + +helps['containerapp secret set'] = """ + type: command + short-summary: Create/update Containerapp secrets. + examples: + - name: Add a secret to a Containerapp. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName=MySecretValue + - name: Update a Containerapp secret. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue +""" + helps['containerapp github-action add'] = """ type: command short-summary: Adds GitHub Actions to the Containerapp diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 6e8947ee58c..6440c677635 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -137,7 +137,7 @@ "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight "customDomains": None, # [CustomDomain] - # "allowInsecure": None + "allowInsecure": None # Boolean } RegistryCredentials = { diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 6bec838fb93..545f6b8d05a 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -121,4 +121,22 @@ def load_arguments(self, _): 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', type=str, help='Name of the revision') + 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="Ingress type that allows either internal or external traffic to the Containerapp.") + 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 Containerapp. 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 containerapp. Space-separated values in 'key=value' format.") + + with self.argument_context('containerapp secret delete') as c: + c.argument('secret_names', nargs='+', help="A list of secret(s) for the containerapp. Space-separated secret values names.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 02b436597c8..a4e11f220fd 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -300,6 +300,23 @@ def _add_or_update_secrets(containerapp_def, add_secrets): 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 i in range(0, len(containerapp_def["properties"]["configuration"]["secrets"])): + existing_secret = containerapp_def["properties"]["configuration"]["secrets"][i] + if existing_secret["name"].lower() == secret_name.lower(): + containerapp_def["properties"]["configuration"]["secrets"].pop(i) + break def _add_or_update_env_vars(existing_env_vars, new_env_vars): for new_env_var in new_env_vars: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index fed17d21da0..2ea2e48b04c 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -67,3 +67,28 @@ def load_command_table(self, _): 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_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()) + + with self.command_group('containerapp revision mode') as g: + g.custom_command('set', '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_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_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_command('show', 'show_registry') + g.custom_command('list', 'list_registry') + g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp secret') as g: + g.custom_command('list', 'list_secrets') + g.custom_command('show', 'show_secret') + g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) + g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d346cc75f65..6908f9d6371 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -13,6 +13,8 @@ from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError +from azure.cli.command_modules.appservice.custom import _get_acr_cred +from urllib.parse import urlparse from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient @@ -37,7 +39,7 @@ _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) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret) logger = get_logger(__name__) @@ -85,7 +87,7 @@ def create_deserializer(): return Deserializer(deserializer) -def update_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait=False): +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: raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') @@ -112,6 +114,14 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= 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) + 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() @@ -1159,3 +1169,588 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): except CLIError as e: handle_raw_exception(e) +def copy_revision(cmd, + name, + resource_group_name, + from_revision=None, + #label=None, + yaml=None, + image=None, + image_name=None, + min_replicas=None, + max_replicas=None, + env_vars=None, + cpu=None, + memory=None, + revision_suffix=None, + startup_command=None, + traffic_weights=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if not from_revision: + from_revision = containerapp_def["properties"]["latestRevisionName"] + + if yaml: + if image or min_replicas or max_replicas or\ + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + 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) + + containerapp_def["properties"]["template"] = r["properties"]["template"] + + update_map = {} + update_map['ingress'] = traffic_weights + update_map['scale'] = min_replicas or max_replicas + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['configuration'] = update_map['ingress'] + + 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 image_name: + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + image_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() == image_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + if env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + 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"] = image_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: + 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 + + # Configuration + if update_map["ingress"]: + if "ingress" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["ingress"] = {} + + 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) + + 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 CLIError("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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"] + except: + raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) + +def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, allow_insecure=False, 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 CLIError("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) + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + containerapp_def["properties"]["configuration"]["ingress"] = None + + _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("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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["ingress"] + except: + raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") + + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] + except: + raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") + +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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + for r in registries_def: + if r['server'].lower() == server.lower(): + return r + raise CLIError("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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + +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 CLIError("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') + + # 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) + # Should this be false? ^ + + 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 delete_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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + registries_def = None + registry = None + + try: + containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + wasRemoved = False + for i in range(0, len(registries_def)): + r = registries_def[i] + 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 CLIError("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 as e: + return + +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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] + except: + raise CLIError("The containerapp {} has no assigned secrets.".format(name)) + +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 CLIError("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 CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + +def delete_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 CLIError("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 CLIError("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, + #secrets=None, + #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 CLIError("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 CLIError("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) + + From 3fa18dff53a13a6e28ead909add285abcbb659b3 Mon Sep 17 00:00:00 2001 From: Calvin Date: Fri, 11 Mar 2022 10:13:37 -0800 Subject: [PATCH 043/177] More p0 fixes (#20) * Remove --registry-login-server, only allow --registry-server * Rename --environment-variables to --env-vars * If no image is supplied, use default quickstart image --- src/containerapp/azext_containerapp/_help.py | 8 ++++---- src/containerapp/azext_containerapp/_params.py | 4 ++-- src/containerapp/azext_containerapp/_validators.py | 6 +++--- src/containerapp/azext_containerapp/custom.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 6122a3d895a..0720d816793 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -26,7 +26,7 @@ az containerapp create -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage -e MyContainerappEnv \\ --secrets mysecret=escapefromtarkov,anothersecret=isadifficultgame \\ - --environment-variables myenvvar=foo,anotherenvvar=bar \\ + --env-vars myenvvar=foo,anotherenvvar=bar \\ --query properties.configuration.ingress.fqdn - name: Create a Containerapp that only accepts internal traffic text: | @@ -39,7 +39,7 @@ az containerapp create -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage -e MyContainerappEnv \\ --secrets mypassword=verysecurepassword \\ - --registry-login-server MyRegistryServerAddress \\ + --registry-server MyRegistryServerAddress \\ --registry-username MyUser \\ --registry-password mypassword \\ --query properties.configuration.ingress.fqdn @@ -75,7 +75,7 @@ text: | az containerapp update -n MyContainerapp -g MyResourceGroup \\ --secrets mysecret=secretfoo,anothersecret=secretbar - --environment-variables myenvvar=foo,anotherenvvar=secretref:mysecretname + --env-vars myenvvar=foo,anotherenvvar=secretref:mysecretname - name: Update a Containerapp's ingress setting to internal text: | az containerapp update -n MyContainerapp -g MyResourceGroup \\ @@ -85,7 +85,7 @@ az containerapp update -n MyContainerapp -g MyResourceGroup \\ --image MyNewContainerImage \\ --secrets mypassword=verysecurepassword \\ - --registry-login-server MyRegistryServerAddress \\ + --registry-server MyRegistryServerAddress \\ --registry-username MyUser \\ --registry-password mypassword - name: Update a Containerapp using a specified startup command and arguments diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 545f6b8d05a..e6fb2908f67 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") + c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -56,7 +56,7 @@ def load_arguments(self, _): # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server', '--registry-login-server'], help="The url of the registry, e.g. myregistry.azurecr.io") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index c95d675cb00..916d9eb5b57 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -53,19 +53,19 @@ def validate_registry_server(namespace): 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-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + 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-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + 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-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + 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(): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 6908f9d6371..f814103c875 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -336,8 +336,8 @@ def create_containerapp(cmd, 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 image is None: - raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') + 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') @@ -671,7 +671,7 @@ def update_containerapp(cmd, registries_def = containerapp_def["properties"]["configuration"]["registries"] if not registry_server: - raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") # Infer credentials if not supplied and its azurecr if not registry_user or not registry_pass: @@ -696,7 +696,7 @@ def update_containerapp(cmd, # If not updating existing registry, add as new registry if not updating_existing_registry: if not(registry_server is not None and registry_user is not None and registry_pass is not None): - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") + raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required when adding a registry") registry = RegistryCredentialsModel registry["server"] = registry_server From c72c29ed5162275204d27842f75142bc48743bf2 Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Mon, 14 Mar 2022 09:01:49 -0700 Subject: [PATCH 044/177] Update help text (#21) * Update help text * Update punctuation * master -> main --- src/containerapp/azext_containerapp/_help.py | 267 ++++++++---------- .../azext_containerapp/_params.py | 58 ++-- src/containerapp/azext_containerapp/custom.py | 2 +- 3 files changed, 143 insertions(+), 184 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 0720d816793..4f6fd755cc7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -9,92 +9,47 @@ helps['containerapp'] = """ type: group - short-summary: Commands to manage Containerapps. + short-summary: Commands to manage Azure Container Apps. """ helps['containerapp create'] = """ type: command - short-summary: Create a Containerapp. + short-summary: Create a container app. examples: - - name: Create a Containerapp + - name: Create a container app and retrieve its fully qualified domain name. text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ + --image myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --ingress external --target-port 80 \\ --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with secrets and environment variables + - name: Create a container app with resource requirements and replica count limits. text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --secrets mysecret=escapefromtarkov,anothersecret=isadifficultgame \\ - --env-vars myenvvar=foo,anotherenvvar=bar \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp that only accepts internal traffic - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --ingress internal \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp using an image from a private registry - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --secrets mypassword=verysecurepassword \\ - --registry-server MyRegistryServerAddress \\ - --registry-username MyUser \\ - --registry-password mypassword \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with a specified startup command and arguments - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --command "/bin/sh" \\ - --args "-c", "while true; do echo hello; sleep 10;done" \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with a minimum resource and replica requirements + --image myregistry.azurecr.io/my-app:v1.0 --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 MyContainerImage -e MyContainerappEnv \\ - --cpu 0.5 --memory 1.0Gi \\ - --min-replicas 4 --max-replicas 8 \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples + --image myregistry.azurecr.io/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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --yaml "C:/path/to/yaml/file.yml" + --environment MyContainerappEnv \\ + --yaml "path/to/yaml/file.yml" """ helps['containerapp update'] = """ type: command - short-summary: Update a Containerapp. + short-summary: Update a container app. In multiple revisions mode, create a new revision based on the latest revision. examples: - - name: Update a Containerapp's container image - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --image MyNewContainerImage - - name: Update a Containerapp with secrets and environment variables - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --secrets mysecret=secretfoo,anothersecret=secretbar - --env-vars myenvvar=foo,anotherenvvar=secretref:mysecretname - - name: Update a Containerapp's ingress setting to internal + - name: Update a container app's container image. text: | az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --ingress internal - - name: Update a Containerapp using an image from a private registry - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --image MyNewContainerImage \\ - --secrets mypassword=verysecurepassword \\ - --registry-server MyRegistryServerAddress \\ - --registry-username MyUser \\ - --registry-password mypassword - - name: Update a Containerapp using a specified startup command and arguments - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage \\ - --command "/bin/sh" - --args "-c", "while true; do echo hello; sleep 10;done" - - name: Update a Containerapp with a minimum resource and replica requirements + --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 \\ @@ -103,37 +58,37 @@ helps['containerapp delete'] = """ type: command - short-summary: Delete a Containerapp. + short-summary: Delete a container app. examples: - - name: Delete a Containerapp. + - name: Delete a container app. text: az containerapp delete -g MyResourceGroup -n MyContainerapp """ helps['containerapp scale'] = """ type: command - short-summary: Set the min and max replicas for a Containerapp. + short-summary: Set the min and max replicas for a container app (latest revision in multiple revisions mode). examples: - - name: Scale a Containerapp. + - name: Scale a container's latest revision. text: az containerapp scale -g MyResourceGroup -n MyContainerapp --min-replicas 1 --max-replicas 2 """ helps['containerapp show'] = """ type: command - short-summary: Show details of a Containerapp. + short-summary: Show details of a container app. examples: - - name: Show the details of a Containerapp. + - name: Show the details of a container app. text: | az containerapp show -n MyContainerapp -g MyResourceGroup """ helps['containerapp list'] = """ type: command - short-summary: List Containerapps. + short-summary: List container apps. examples: - - name: List Containerapps by subscription. + - name: List container apps in the current subscription. text: | az containerapp list - - name: List Containerapps by resource group. + - name: List container apps by resource group. text: | az containerapp list -g MyResourceGroup """ @@ -141,61 +96,62 @@ # Revision Commands helps['containerapp revision'] = """ type: group - short-summary: Commands to manage a Containerapp's revisions. + short-summary: Commands to manage revisions. """ helps['containerapp revision show'] = """ type: command - short-summary: Show details of a Containerapp's revision. + short-summary: Show details of a revision. examples: - - name: Show details of a Containerapp's revision. + - name: Show details of a revision. text: | - az containerapp revision show --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision show -n MyContainerapp -g MyResourceGroup \\ + --revision-name MyContainerappRevision """ helps['containerapp revision list'] = """ type: command - short-summary: List details of a Containerapp's revisions. + short-summary: List a container app's revisions. examples: - - name: List a Containerapp's revisions. + - name: List a container app's revisions. text: | - az containerapp revision list --revision-name MyContainerapp -g MyResourceGroup + az containerapp revision list -n MyContainerapp -g MyResourceGroup """ helps['containerapp revision restart'] = """ type: command - short-summary: Restart a Containerapps's revision. + short-summary: Restart a revision. examples: - - name: Restart a Containerapp's revision. + - name: Restart a revision. text: | - az containerapp revision restart --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ helps['containerapp revision activate'] = """ type: command - short-summary: Activates Containerapp's revision. + short-summary: Activate a revision. examples: - - name: Activate a Containerapp's revision. + - name: Activate a revision. text: | - az containerapp revision activate --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision activate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ helps['containerapp revision deactivate'] = """ type: command - short-summary: Deactivates Containerapp's revision. + short-summary: Deactivate a revision. examples: - - name: Deactivate a Containerapp's revision. + - name: Deactivate a revision. text: | - az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision deactivate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ helps['containerapp revision mode set'] = """ type: command - short-summary: Set the revision mode of a Containerapp. + short-summary: Set the revision mode of a container app. examples: - - name: Set the revision mode of a Containerapp. + - name: Set a container app to single revision mode. text: | - az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup + az containerapp revision mode set-n MyContainerapp -g MyResourceGroup --mode Single """ helps['containerapp revision copy'] = """ @@ -204,61 +160,62 @@ examples: - name: Create a revision based on a previous revision. text: | - az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi + az containerapp revision copy -n MyContainerapp -g MyResourceGroup \\ + --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ # Environment Commands helps['containerapp env'] = """ type: group - short-summary: Commands to manage Containerapp environments. + short-summary: Commands to manage Container Apps environments. """ helps['containerapp env create'] = """ type: command - short-summary: Create a Containerapp environment. + short-summary: Create a Container Apps environment. examples: - - name: Create a Containerapp Environment with an autogenerated Log Analytics + - 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 a Containerapp Environment with Log Analytics + --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 + --location "Canada Central" """ helps['containerapp env update'] = """ type: command - short-summary: Update a Containerapp environment. Currently Unsupported. + short-summary: Update a Container Apps environment. Currently Unsupported. """ helps['containerapp env delete'] = """ type: command - short-summary: Delete a Containerapp Environment. + short-summary: Delete a Container Apps environment. examples: - - name: Delete Containerapp Environment. - text: az containerapp env delete -g MyResourceGroup -n MyContainerappEnvironment + - 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 Containerapp environment. + short-summary: Show details of a Container Apps environment. examples: - - name: Show the details of a Containerapp Environment. + - 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 Containerapp environments by subscription or resource group. + short-summary: List Container Apps environments by subscription or resource group. examples: - - name: List Containerapp Environments by subscription. + - name: List environments in the current subscription. text: | az containerapp env list - - name: List Containerapp Environments by resource group. + - name: List environments by resource group. text: | az containerapp env list -g MyResourceGroup """ @@ -266,60 +223,64 @@ # Ingress Commands helps['containerapp ingress'] = """ type: group - short-summary: Commands to manage Containerapp ingress. + short-summary: Commands to manage ingress and traffic-splitting. """ helps['containerapp ingress traffic'] = """ type: subgroup - short-summary: Commands to manage Containerapp ingress traffic. + short-summary: Commands to manage traffic-splitting. """ helps['containerapp ingress show'] = """ type: command - short-summary: Show details of a Containerapp ingress. + short-summary: Show details of a container app's ingress. examples: - - name: Show the details of a Containerapp ingress. + - 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 Containerapp ingress. + short-summary: Enable ingress for a container app. examples: - - name: Enable Containerapp ingress. + - 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 + 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 Containerapp ingress. + short-summary: Disable ingress for a container app. examples: - - name: Disable Containerapp ingress. + - 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 Containerapp ingress traffic. + short-summary: Commands to manage traffic-splitting. """ helps['containerapp ingress traffic set'] = """ type: command - short-summary: Set Containerapp ingress traffic. + short-summary: Configure traffic-splitting for a container app. examples: - - name: Set Containerapp ingress traffic. + - 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 Containerapp ingress traffic. + short-summary: Show traffic-splitting configuration for a container app. examples: - - name: Show Containerapp ingress traffic. + - name: Show a container app's ingress traffic configuration. text: | az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup """ @@ -327,45 +288,43 @@ # Registry Commands helps['containerapp registry'] = """ type: group - short-summary: Commands to manage Containerapp registries. + short-summary: Commands to manage container registry information. """ helps['containerapp registry show'] = """ type: command - short-summary: Show details of a Containerapp registry. + short-summary: Show details of a container registry. examples: - - name: Show the details of a Containerapp registry. + - 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 registries assigned to a Containerapp. + short-summary: List container registries configured in a container app. examples: - - name: Show the details of a Containerapp registry. + - 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 Containerapp registry. + short-summary: Add or update a container registry's details. examples: - - name: Add a registry to a Containerapp. - text: | - az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io - - name: Update a Containerapp registry. + - 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 + az containerapp registry set -n MyContainerapp -g MyResourceGroup \\ + --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword """ helps['containerapp registry delete'] = """ type: command - short-summary: Delete a registry from a Containerapp. + short-summary: Remove a container registry's details. examples: - - name: Delete a registry from a Containerapp. + - name: Remove a registry from a Containerapp. text: | az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ @@ -373,51 +332,51 @@ # Secret Commands helps['containerapp secret'] = """ type: group - short-summary: Commands to manage Containerapp secrets. + short-summary: Commands to manage secrets. """ helps['containerapp secret show'] = """ type: command - short-summary: Show details of a Containerapp secret. + short-summary: Show details of a secret. examples: - - name: Show the details of a Containerapp secret. + - 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 Containerapp. + short-summary: List the secrets of a container app. examples: - - name: List the secrets of a Containerapp. + - name: List the secrets of a container app. text: | az containerapp secret list -n MyContainerapp -g MyResourceGroup """ helps['containerapp secret delete'] = """ type: command - short-summary: Delete secrets from a Containerapp. + short-summary: Delete secrets from a container app. examples: - - name: Delete secrets from a Containerapp. + - name: Delete secrets from a container app. text: | az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 """ helps['containerapp secret set'] = """ type: command - short-summary: Create/update Containerapp secrets. + short-summary: Create/update secrets. examples: - - name: Add a secret to a Containerapp. + - name: Add secrets to a container app. text: | - az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName=MySecretValue - - name: Update a Containerapp secret. + 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 add'] = """ type: command - short-summary: Adds GitHub Actions to the Containerapp + 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 @@ -433,7 +392,7 @@ --service-principal-tenant-id 00000000-0000-0000-0000-00000000 --service-principal-client-secret ClientSecret --login-with-github - - name: Add GitHub Actions, using Dockerhub and log in to GitHub flow to retrieve personal access token. + - 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 @@ -445,20 +404,20 @@ helps['containerapp github-action delete'] = """ type: command - short-summary: Removes GitHub Actions from the Containerapp + short-summary: Remove a previously configured Container Apps GitHub Actions workflow from a repository. examples: - - name: Removes GitHub Actions, personal access token. + - name: Remove GitHub Actions using a personal access token. text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp --token MyAccessToken - - name: Removes GitHub Actions, using log in to GitHub flow to retrieve personal access token. + - 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 Containerapp + short-summary: Show the GitHub Actions configuration on a container app. examples: - - name: Show the GitHub Actions configuration on a Containerapp + - name: Show the GitHub Actions configuration on a Containerapp. text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index e6fb2908f67..b15851f2b66 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -26,54 +26,54 @@ def load_arguments(self, _): 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 containerapp's environment.") - c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') + 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 (Creates new revision)') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") - c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") + c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") - c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") + c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") + 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='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale with self.argument_context('containerapp', arg_group='Scale (Creates new revision)') as c: - c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") # Dapr - with self.argument_context('containerapp', arg_group='Dapr (Creates new revision)') as c: + 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()) - c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") + c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="The port Dapr uses to talk to the application.") c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") - c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="The protocol Dapr uses to talk to the application.") c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: - c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") - c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the container app.") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--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, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") + c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") - c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + 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, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") with self.argument_context('containerapp env') as c: - c.argument('name', name_type, help='Name of the containerapp environment') + 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) @@ -94,21 +94,21 @@ def load_arguments(self, _): 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 managed environment.') + 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 managed Environment.') + 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 managed Environment.') + 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 "master" if not specified.') + 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 url of the registry, e.g. myregistry.azurecr.io') + 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') @@ -128,15 +128,15 @@ def load_arguments(self, _): 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="Ingress type that allows either internal or external traffic to the Containerapp.") + 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 Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + 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 containerapp. Space-separated values in 'key=value' format.") + 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 delete') as c: - c.argument('secret_names', nargs='+', help="A list of secret(s) for the containerapp. Space-separated secret values names.") + c.argument('secret_names', nargs='+', help="A list of secret(s) for the container app. Space-separated secret values names.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f814103c875..c3277dad616 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1013,7 +1013,7 @@ def create_or_update_github_action(cmd, if branch: source_control_info["properties"]["branch"] = branch if not source_control_info["properties"]["branch"]: - source_control_info["properties"]["branch"] = "master" + source_control_info["properties"]["branch"] = "main" azure_credentials = None From c09fe399d95870f756c6cf7efac18f8e3de3b916 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 12:00:12 -0700 Subject: [PATCH 045/177] New 1.0.1 version --- src/containerapp/HISTORY.rst | 6 ++++++ src/containerapp/setup.py | 2 +- src/index.json | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 8c34bccfff8..f58df889075 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.1.1 +++++++ +* Various fixes for az containerapp create, update +* Added github actions support +* Added subgroups for ingress, registry, revision, secret + 0.1.0 ++++++ * Initial release. \ No newline at end of file diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index be4cd26f637..fa1b93b7448 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.0' +VERSION = '0.1.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index 32019fe8325..401afbb4f91 100644 --- a/src/index.json +++ b/src/index.json @@ -12235,6 +12235,48 @@ "sha256Digest": "9a796d5187571990d27feb9efeedde38c194f13ea21cbf9ec06131196bfd821d" } ], + "containerapp": [ + { + "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.1-py2.py3-none-any.whl", + "filename": "containerapp-0.1.1-py2.py3-none-any.whl", + "metadata": { + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "extensions": { + "python.details": { + "contacts": [ + { + "email": "azpycli@microsoft.com", + "name": "Microsoft Corporation", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azure-cli-extensions" + } + } + }, + "extras": [], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "containerapp", + "run_requires": [ + { + "requires": [ + "azure-cli-core" + ] + } + ], + "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", + "version": "0.1.1" + }, + "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" + } + ], "cosmosdb-preview": [ { "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/cosmosdb_preview-0.1.0-py2.py3-none-any.whl", From d264bbf221a824297018065136a8c5c844ca0605 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 14 Mar 2022 17:00:17 -0400 Subject: [PATCH 046/177] Added identity commands + --assign-identity flag to containerapp create (#8) * Added identity show and assign. * Finisheed identity remove. * Added helps, updated identity remove to work with identity names instead of requiring identity resource ids. * Moved helper function to utils. * Require --identities flag when removing identities. * Added message for assign identity with no specified identity. * Added --assign-identity flag to containerapp create. * Moved assign-identity flag to containerapp create. * Fixed small logic error on remove identities when passing duplicate identities. Added warnings for certain edge cases. * Updated param definition for identity assign --identity default. * Added identity examples in help. * Made sure secrets were not removed when assigning identities. Added tolerance for [system] passed with capital letters. * Fixed error from merge. Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/_help.py | 36 ++++ .../azext_containerapp/_params.py | 9 + src/containerapp/azext_containerapp/_utils.py | 10 + .../azext_containerapp/commands.py | 7 + src/containerapp/azext_containerapp/custom.py | 202 +++++++++++++++++- 5 files changed, 261 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 4f6fd755cc7..724335e8711 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -220,6 +220,42 @@ az containerapp env list -g MyResourceGroup """ +# Identity Commands +helps['containerapp identity'] = """ + type: group + short-summary: Manage service (managed) identities for a containerapp +""" + +helps['containerapp identity assign'] = """ + type: command + short-summary: Assign a managed identity to a containerapp + long-summary: Managed identities can be user-assigned or system-assigned + examples: + - name: Assign system identity. + text: | + az containerapp identity assign + - name: Assign system and user identity. + text: | + az containerapp identity assign --identities [system] myAssignedId +""" + +helps['containerapp identity remove'] = """ + type: command + short-summary: Remove a managed identity from a containerapp + examples: + - name: Remove system identity. + text: | + az containerapp identity remove [system] + - name: Remove system and user identity. + text: | + az containerapp identity remove --identities [system] myAssignedId +""" + +helps['containerapp identity show'] = """ + type: command + short-summary: Show the containerapp's identity details +""" + # Ingress Commands helps['containerapp ingress'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index b15851f2b66..8435659f4d0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,6 +66,9 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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('assign_identity', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity.") 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: @@ -103,6 +106,12 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Container Apps Environment.') + with self.argument_context('containerapp identity') as c: + c.argument('identities', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity.") + + with self.argument_context('containerapp identity assign') as c: + c.argument('identities', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity. Default is '[system]'.") + 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') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index a4e11f220fd..54994a71578 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -284,6 +284,16 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): 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"]: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 2ea2e48b04c..9a83db9df7e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -56,6 +56,13 @@ def load_command_table(self, _): # g.custom_command('update', 'update_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 identity') as g: + g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('show', 'show_managed_identity') + + 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_command('show', 'show_github_action', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c3277dad616..db5bdb00db2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -33,13 +33,19 @@ Dapr as DaprModel, ContainerResources as ContainerResourcesModel, Scale as ScaleModel, - Container as ContainerModel, GitHubActionConfiguration, RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, SourceControl as SourceControlModel) + 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_list_of_strings, 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) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret, + _ensure_identity_resource_id) logger = get_logger(__name__) @@ -325,7 +331,8 @@ def create_containerapp(cmd, startup_command=None, args=None, tags=None, - no_wait=False): + no_wait=False, + assign_identity=[]): _validate_subscription_registered(cmd, "Microsoft.App") if yaml: @@ -403,6 +410,28 @@ def create_containerapp(cmd, config_def["ingress"] = ingress_def config_def["registries"] = [registries_def] if registries_def is not None else None + # Identity actions + identity_def = ManagedServiceIdentityModel + identity_def["type"] = "None" + + assign_system_identity = '[system]' in assign_identity + assign_user_identities = [x for x in assign_identity if x != '[system]'] + + if assign_system_identity and assign_user_identities: + identity_def["type"] = "SystemAssigned, UserAssigned" + elif assign_system_identity: + identity_def["type"] = "SystemAssigned" + elif assign_user_identities: + identity_def["type"] = "UserAssigned" + + if assign_user_identities: + identity_def["userAssignedIdentities"] = {} + subscription_id = get_subscription_id(cmd.cli_ctx) + + for r in assign_user_identities: + r = _ensure_identity_resource_id(subscription_id, resource_group_name, r) + identity_def["userAssignedIdentities"][r] = {} + scale_def = None if min_replicas is not None or max_replicas is not None: scale_def = ScaleModel @@ -445,6 +474,7 @@ def create_containerapp(cmd, containerapp_def = ContainerAppModel containerapp_def["location"] = location + containerapp_def["identity"] = identity_def containerapp_def["properties"]["managedEnvironmentId"] = managed_env containerapp_def["properties"]["configuration"] = config_def containerapp_def["properties"]["template"] = template_def @@ -935,6 +965,172 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): 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 CLIError("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 CLIError("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 CLIError("The containerapp {} has no system or user assigned identities.".format(name)) + + if remove_system_identity: + if containerapp_def["identity"]["type"] == "UserAssigned": + raise CLIError("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 id in remove_user_identities: + given_id = id + id = _ensure_identity_resource_id(subscription_id, resource_group_name, id) + wasRemoved = False + + for old_user_identity in containerapp_def["identity"]["userAssignedIdentities"]: + if old_user_identity.lower() == id.lower(): + containerapp_def["identity"]["userAssignedIdentities"].pop(old_user_identity) + wasRemoved = True + break + + if not wasRemoved: + raise CLIError("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, From 77c742d8bb080ab16036a73f9da4f2ffadd6183e Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:53:35 -0400 Subject: [PATCH 047/177] Dapr Commands (#23) * Added ingress subgroup. * Added help for ingress. * Fixed ingress traffic help. * Added registry commands. * Updated registry remove util to clear secrets if none remaining. Added warning when updating existing registry. Added registry help. * Changed registry delete to remove. * Added error message if user tries to remove non assigned registry. * Changed registry add back to registry set. * Added secret subgroup commands. * Removed yaml support from secret set. * Changed secret add to secret set. Updated consistency between secret set and secret delete. Added secret help. Require at least one secret passed with --secrets for secret commands. * Changed param name for secret delete from --secrets to --secret-names. Updated help. * Changed registry remove to registry delete. * Fixed bug in registry delete. * Added revision mode set and revision copy. * Added dapr enable and dapr disable. Need to test more. * Added list, show, set dapr component. Added dapr enable, disable. * Added delete dapr delete. * Added helps and param text. * Changed dapr delete to dapr remove to match with dapr set. * Commented out managed identity for whl file. * Uncommented. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_clients.py | 119 +++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 78 +++++++++++ .../azext_containerapp/_models.py | 20 ++- .../azext_containerapp/_params.py | 7 + src/containerapp/azext_containerapp/_utils.py | 17 +++ .../azext_containerapp/commands.py | 9 ++ src/containerapp/azext_containerapp/custom.py | 122 +++++++++++++++++- 7 files changed, 369 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 8184e6d86e2..5a1e597523e 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -603,3 +603,122 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): 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): + #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{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, "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/_help.py b/src/containerapp/azext_containerapp/_help.py index 724335e8711..3a91fb32aca 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -164,6 +164,24 @@ --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ +helps['containerapp revision mode set'] = """ + type: command + short-summary: Set the revision mode of a Containerapp. + examples: + - name: Set the revision mode of a Containerapp. + text: | + az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi +""" + # Environment Commands helps['containerapp env'] = """ type: group @@ -456,4 +474,64 @@ 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 Containerapp dapr. +""" + +helps['containerapp dapr enable'] = """ + type: command + short-summary: Enable dapr for a Containerapp. + examples: + - name: Enable dapr for a Containerapp. + 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 Containerapp. + examples: + - name: Disable dapr for a Containerapp. + text: | + az containerapp dapr disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp dapr list'] = """ + type: command + short-summary: List dapr components for a Containerapp environment. + examples: + - name: List dapr components for a Containerapp environment. + text: | + az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment +""" + +helps['containerapp dapr show'] = """ + type: command + short-summary: Show the details of a dapr component. + examples: + - name: Show the details of a dapr component. + text: | + az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" + +helps['containerapp dapr set'] = """ + type: command + short-summary: Create or update a dapr component. + examples: + - name: Create a dapr component. + text: | + az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName +""" + +helps['containerapp dapr remove'] = """ + type: command + short-summary: Remove a dapr componenet from a Containerapp environment. + examples: + - name: Remove a dapr componenet from a Containerapp environment. + text: | + az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 6440c677635..14d8e1a8fb3 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -181,6 +181,24 @@ "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, @@ -211,4 +229,4 @@ "clientSecret": None, # str "tenantId": None, #str "subscriptionId": None #str -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 8435659f4d0..ab1ebe848bd 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -149,3 +149,10 @@ def load_arguments(self, _): with self.argument_context('containerapp secret delete') 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 dapr') 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="Tells 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', help="The dapr component environment name.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 54994a71578..14816a07915 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -414,6 +414,23 @@ def _remove_readonly_attributes(containerapp_def): 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 diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 9a83db9df7e..95e165d7e63 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -99,3 +99,12 @@ def load_command_table(self, _): g.custom_command('show', 'show_secret') g.custom_command('delete', 'delete_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()) + g.custom_command('list', 'list_dapr_components') + g.custom_command('show', 'show_dapr_component') + g.custom_command('set', 'create_or_update_dapr_component') + g.custom_command('remove', 'remove_dapr_component') + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index db5bdb00db2..13d42b6be6b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient from ._sdk_models import * from ._github_oauth import get_github_access_token from ._models import ( @@ -45,7 +45,7 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes) logger = get_logger(__name__) @@ -1949,4 +1949,122 @@ def set_secrets(cmd, name, resource_group_name, 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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + if 'dapr' not in containerapp_def['properties']: + containerapp_def['properties']['dapr'] = {} + + if dapr_app_id: + containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id + + if dapr_app_port: + containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port + + if dapr_app_protocol: + containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol + + containerapp_def['properties']['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"]['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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + containerapp_def['properties']['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"]['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: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + #daprcomponent_def = _object_to_dict(daprcomponent_def) + 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: + raise CLIError("Dapr component not found.") + + 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) From 33947816e25d30f3db1f98c0ce8a6b72887fb7a8 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 13:20:04 -0700 Subject: [PATCH 048/177] Rename --image-name to --container-name --- .../azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/custom.py | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ab1ebe848bd..cc2040b4a13 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -32,7 +32,7 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") - c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the container.") + c.argument('container_name', type=str, options_list=['--container-name'], help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 13d42b6be6b..aab9338a524 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -307,7 +307,7 @@ def create_containerapp(cmd, resource_group_name, yaml=None, image=None, - image_name=None, + container_name=None, managed_env=None, min_replicas=None, max_replicas=None, @@ -445,7 +445,7 @@ def create_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name if image_name else name + 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) @@ -497,7 +497,7 @@ def update_containerapp(cmd, resource_group_name, yaml=None, image=None, - image_name=None, + container_name=None, min_replicas=None, max_replicas=None, ingress=None, @@ -551,7 +551,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None + update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -563,16 +563,16 @@ def update_containerapp(cmd, # Containers if update_map["container"]: - if not image_name: + if not container_name: if len(containerapp_def["properties"]["template"]["containers"]) == 1: - image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + 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() == image_name.lower(): + if c["name"].lower() == container_name.lower(): updating_existing_container = True if image is not None: @@ -618,7 +618,7 @@ def update_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name + container_def["name"] = container_name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) @@ -1372,7 +1372,7 @@ def copy_revision(cmd, #label=None, yaml=None, image=None, - image_name=None, + container_name=None, min_replicas=None, max_replicas=None, env_vars=None, @@ -1416,7 +1416,7 @@ def copy_revision(cmd, update_map = {} update_map['ingress'] = traffic_weights update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None update_map['configuration'] = update_map['ingress'] if tags: @@ -1427,16 +1427,16 @@ def copy_revision(cmd, # Containers if update_map["container"]: - if not image_name: + if not container_name: if len(containerapp_def["properties"]["template"]["containers"]) == 1: - image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + 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() == image_name.lower(): + if c["name"].lower() == container_name.lower(): updating_existing_container = True if image is not None: @@ -1479,7 +1479,7 @@ def copy_revision(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name + container_def["name"] = container_name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) From d406c0b5fc5bff2ec6a0374c8bd4f5acef47bd67 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 13:42:48 -0700 Subject: [PATCH 049/177] Remove allowInsecure since it was messing with the api parsing --- src/containerapp/azext_containerapp/_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 14d8e1a8fb3..b356adaa2a8 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -136,8 +136,7 @@ "targetPort": None, "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight - "customDomains": None, # [CustomDomain] - "allowInsecure": None # Boolean + "customDomains": None # [CustomDomain] } RegistryCredentials = { From 551ea0d8ab1dc8d8c54591347c5283cc3e38e6d2 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 15:11:33 -0700 Subject: [PATCH 050/177] Fix for env var being empty string --- src/containerapp/azext_containerapp/custom.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index aab9338a524..dc7bdcd5d05 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -394,7 +394,7 @@ def create_containerapp(cmd, registries_def = RegistryCredentialsModel # Infer credentials if not supplied and its azurecr - if not registry_user or not registry_pass: + 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 @@ -541,6 +541,14 @@ def update_containerapp(cmd, if not containerapp_def: raise CLIError("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"] = "" + # If ACR image and registry_server is not supplied, infer it if image and '.azurecr.io' in image: if not registry_server: @@ -704,7 +712,7 @@ def update_containerapp(cmd, raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") # Infer credentials if not supplied and its azurecr - if not registry_user or not registry_pass: + if registry_user is None or registry_pass is None: registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) # Check if updating existing registry @@ -1221,7 +1229,7 @@ def create_or_update_github_action(cmd, azure_credentials["subscriptionId"] = get_subscription_id(cmd.cli_ctx) # Registry - if not registry_username or not registry_password: + 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') @@ -1413,6 +1421,14 @@ def copy_revision(cmd, 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['ingress'] = traffic_weights update_map['scale'] = min_replicas or max_replicas From f7cd94db60b9e31ca3043529a01f502e3e6edc86 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:23:46 -0700 Subject: [PATCH 051/177] Rename to --dapr-instrumentation-key, only infer ACR credentials if --registry-server is provided --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 12 +++++++++++- src/containerapp/azext_containerapp/custom.py | 14 ++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index cc2040b4a13..c592ed5363d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -86,7 +86,7 @@ def load_arguments(self, _): 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=['--instrumentation-key'], help='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') + c.argument('instrumentation_key', options_list=['--dapr-instrumentation-key'], help='Azure Monitor 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. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 14816a07915..fa3ee8f2a50 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -493,7 +493,7 @@ def _get_app_from_revision(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 url is required if using Azure Container Registry, otherwise Registry username and password are required.') + 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] @@ -503,3 +503,13 @@ def _infer_acr_credentials(cmd, registry_server): 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)) + + +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/custom.py b/src/containerapp/azext_containerapp/custom.py index dc7bdcd5d05..527c5c56998 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -45,7 +45,7 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists) logger = get_logger(__name__) @@ -384,11 +384,6 @@ def create_containerapp(cmd, if secrets is not None: secrets_def = parse_secret_flags(secrets) - # If ACR image and registry_server is not supplied, infer it - if image and '.azurecr.io' in image: - if not registry_server: - registry_server = image.split('/')[0] - registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel @@ -549,11 +544,6 @@ def update_containerapp(cmd, if "value" not in e: e["value"] = "" - # If ACR image and registry_server is not supplied, infer it - if image and '.azurecr.io' in image: - if not registry_server: - registry_server = image.split('/')[0] - update_map = {} update_map['secrets'] = secrets is not None update_map['ingress'] = ingress or target_port or transport or traffic_weights @@ -712,7 +702,7 @@ def update_containerapp(cmd, raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") # Infer credentials if not supplied and its azurecr - if registry_user is None or registry_pass is None: + if (registry_user is None or registry_pass is None) and not _registry_exists(containerapp_def, registry_server): registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) # Check if updating existing registry From fa4be8ac69b804aad6633af439254ef3802eba31 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:24:58 -0700 Subject: [PATCH 052/177] Remove az containerapp scale --- .../azext_containerapp/commands.py | 1 - src/containerapp/azext_containerapp/custom.py | 33 ------------------- 2 files changed, 34 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 95e165d7e63..8ee1f082671 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -44,7 +44,6 @@ def load_command_table(self, _): g.custom_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()) - g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 527c5c56998..c026aecfb6d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -749,39 +749,6 @@ def update_containerapp(cmd, handle_raw_exception(e) -def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_replicas=None, no_wait=False): - 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 CLIError("The containerapp '{}' does not exist".format(name)) - - 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 scale 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") From e5ddac42a65e0da2baf1ba8c24d5c0f0cf45bfdb Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:30:57 -0700 Subject: [PATCH 053/177] Fix delete containerapp errors --- .../azext_containerapp/_clients.py | 22 +++---------------- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 4 ++-- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5a1e597523e..108ee5b004f 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -128,7 +128,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= return r.json() @classmethod - def delete(cls, cmd, resource_group_name, name, no_wait=False): + def delete(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) @@ -142,24 +142,8 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): 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') + if r.status_code == 202: + logger.warning('Containerapp successfully deleted') return @classmethod diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 8ee1f082671..4a8142c43a3 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -45,7 +45,7 @@ def load_command_table(self, _): 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c026aecfb6d..4454eb58477 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -773,11 +773,11 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) -def delete_containerapp(cmd, name, resource_group_name, no_wait=False): +def delete_containerapp(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") try: - return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) except CLIError as e: handle_raw_exception(e) From b0c6f9557c990f04580de466f68c4acf6ce095ea Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:53:08 -0700 Subject: [PATCH 054/177] Remove ingress, dapr flags from az containerapp update/revision copy --- src/containerapp/azext_containerapp/_utils.py | 2 +- src/containerapp/azext_containerapp/custom.py | 102 ++++-------------- 2 files changed, 23 insertions(+), 81 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index fa3ee8f2a50..297ce4904ba 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -459,7 +459,7 @@ def _is_valid_weight(weight): return False -def _update_traffic_Weights(containerapp_def, list_weights): +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"] = [] diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4454eb58477..1fcb7c2176b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -43,7 +43,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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, + _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) @@ -495,10 +495,6 @@ def update_containerapp(cmd, container_name=None, min_replicas=None, max_replicas=None, - ingress=None, - target_port=None, - transport=None, - traffic_weights=None, revisions_mode=None, secrets=None, env_vars=None, @@ -507,11 +503,6 @@ def update_containerapp(cmd, registry_server=None, registry_user=None, registry_pass=None, - dapr_enabled=None, - dapr_app_port=None, - dapr_app_id=None, - dapr_app_protocol=None, - # dapr_components=None, revision_suffix=None, startup_command=None, args=None, @@ -520,9 +511,9 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - if image or min_replicas or max_replicas or target_port or ingress or\ + if image or min_replicas or max_replicas 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\ + registry_user or registry_pass 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) @@ -546,12 +537,10 @@ def update_containerapp(cmd, update_map = {} update_map['secrets'] = secrets is not None - update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None - update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol - update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None + update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -644,46 +633,10 @@ def update_containerapp(cmd, if max_replicas is not None: containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - # Dapr - if update_map["dapr"]: - if "dapr" not in containerapp_def["properties"]["template"]: - containerapp_def["properties"]["template"]["dapr"] = {} - if dapr_enabled is not None: - containerapp_def["properties"]["template"]["dapr"]["daprEnabled"] = dapr_enabled - if dapr_app_id is not None: - containerapp_def["properties"]["template"]["dapr"]["appId"] = dapr_app_id - if dapr_app_port is not None: - containerapp_def["properties"]["template"]["dapr"]["appPort"] = dapr_app_port - if dapr_app_protocol is not None: - containerapp_def["properties"]["template"]["dapr"]["appProtocol"] = dapr_app_protocol - # Configuration if revisions_mode is not None: containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode - if update_map["ingress"]: - if "ingress" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["ingress"] = {} - - external_ingress = None - if ingress is not None: - if ingress.lower() == "internal": - external_ingress = False - elif ingress.lower() == "external": - external_ingress = True - - if external_ingress is not None: - containerapp_def["properties"]["configuration"]["ingress"]["external"] = external_ingress - - if target_port is not None: - containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - - if transport is not None: - containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport - - if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) if secrets is not None: @@ -1331,24 +1284,23 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): handle_raw_exception(e) def copy_revision(cmd, - name, - resource_group_name, - from_revision=None, - #label=None, - yaml=None, - image=None, - container_name=None, - min_replicas=None, - max_replicas=None, - env_vars=None, - cpu=None, - memory=None, - revision_suffix=None, - startup_command=None, - traffic_weights=None, - args=None, - tags=None, - no_wait=False): + name, + resource_group_name, + from_revision=None, + #label=None, + yaml=None, + image=None, + container_name=None, + min_replicas=None, + max_replicas=None, + env_vars=None, + 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 from_revision: @@ -1387,10 +1339,8 @@ def copy_revision(cmd, e["value"] = "" update_map = {} - update_map['ingress'] = traffic_weights update_map['scale'] = min_replicas or max_replicas update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None - update_map['configuration'] = update_map['ingress'] if tags: _add_or_update_tags(containerapp_def, tags) @@ -1480,14 +1430,6 @@ def copy_revision(cmd, if max_replicas is not None: containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - # Configuration - if update_map["ingress"]: - if "ingress" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["ingress"] = {} - - if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) try: @@ -1621,7 +1563,7 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) + _update_traffic_weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) From 8e30e916e2510ba2bd817a079bb640454bd56cff Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:53:15 -0700 Subject: [PATCH 055/177] Fix revision list -o table --- src/containerapp/azext_containerapp/commands.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 4a8142c43a3..40e422bb532 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -27,10 +27,13 @@ def transform_containerapp_list_output(apps): def transform_revision_output(rev): props = ['name', 'replicas', 'active', 'createdTime'] - result = {k: rev[k] for k in rev if k in props} + result = {k: rev['properties'][k] for k in rev['properties'] if k in props} - if 'latestRevisionFqdn' in rev['template']: - result['fqdn'] = rev['template']['latestRevisionFqdn'] + if 'name' in rev: + result['name'] = rev['name'] + + if 'fqdn' in rev['properties']['template']: + result['fqdn'] = rev['properties']['template']['fqdn'] return result From 013507f97d0793403ab0171b4613de48d5955541 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:54:29 -0700 Subject: [PATCH 056/177] Help text fix --- src/containerapp/azext_containerapp/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 3a91fb32aca..228343f5dee 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -479,7 +479,7 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage Containerapp dapr. + short-summary: Commands to manage dapr. """ helps['containerapp dapr enable'] = """ From 32ed50b20ee85b1fbb3a94454db8ff8e65fa8478 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:59:07 -0700 Subject: [PATCH 057/177] Bump extension to 0.1.2 --- src/containerapp/HISTORY.rst | 6 ++++++ src/containerapp/setup.py | 2 +- src/index.json | 40 ++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index f58df889075..8400a3f0baf 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.1.2 +++++++ +* Various fixes for bugs found +* Dapr subgroup +* Managed Identity + 0.1.1 ++++++ * Various fixes for az containerapp create, update diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index fa1b93b7448..96524e9ab67 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.1' +VERSION = '0.1.2' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index 401afbb4f91..d57ae6de96e 100644 --- a/src/index.json +++ b/src/index.json @@ -12275,6 +12275,46 @@ "version": "0.1.1" }, "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" + }, + { + "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.2-py2.py3-none-any.whl", + "filename": "containerapp-0.1.2-py2.py3-none-any.whl", + "metadata": { + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "extensions": { + "python.details": { + "contacts": [ + { + "email": "azpycli@microsoft.com", + "name": "Microsoft Corporation", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azure-cli-extensions" + } + } + }, + "extras": [], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "containerapp", + "run_requires": [ + { + "requires": [ + "azure-cli-core" + ] + } + ], + "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", + "version": "0.1.2" + }, + "sha256Digest": "b1d4cc823f761cfb5469f8d53a9fa04bdc1493c3c5d5f3a90333876287e7b2f8" } ], "cosmosdb-preview": [ From 5c994462b377308a5329116eba6a5430656e413f Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Tue, 15 Mar 2022 14:33:36 -0700 Subject: [PATCH 058/177] Update managed identities and Dapr help text (#25) * Update managed identities and Dapr help text * Update Dapr flags * Add secretref note --- src/containerapp/azext_containerapp/_help.py | 43 +++++++++++-------- .../azext_containerapp/_params.py | 12 +++--- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 228343f5dee..3a5fa25e5dc 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -241,13 +241,13 @@ # Identity Commands helps['containerapp identity'] = """ type: group - short-summary: Manage service (managed) identities for a containerapp + short-summary: Commands to manage managed identities. """ helps['containerapp identity assign'] = """ type: command - short-summary: Assign a managed identity to a containerapp - long-summary: Managed identities can be user-assigned or system-assigned + short-summary: Assign managed identity to a container app. + long-summary: Managed identities can be user-assigned or system-assigned. examples: - name: Assign system identity. text: | @@ -259,7 +259,7 @@ helps['containerapp identity remove'] = """ type: command - short-summary: Remove a managed identity from a containerapp + short-summary: Remove a managed identity from a container app. examples: - name: Remove system identity. text: | @@ -271,7 +271,7 @@ helps['containerapp identity show'] = """ type: command - short-summary: Show the containerapp's identity details + short-summary: Show managed identities of a container app. """ # Ingress Commands @@ -428,6 +428,11 @@ 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. @@ -479,59 +484,59 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage dapr. + short-summary: Commands to manage Dapr. """ helps['containerapp dapr enable'] = """ type: command - short-summary: Enable dapr for a Containerapp. + short-summary: Enable Dapr for a container app. examples: - - name: Enable dapr for a Containerapp. + - 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 Containerapp. + short-summary: Disable Dapr for a container app. examples: - - name: Disable dapr for a Containerapp. + - name: Disable Dapr for a container app. text: | az containerapp dapr disable -n MyContainerapp -g MyResourceGroup """ helps['containerapp dapr list'] = """ type: command - short-summary: List dapr components for a Containerapp environment. + short-summary: List Dapr components. examples: - - name: List dapr components for a Containerapp environment. + - name: List Dapr components for a Container Apps environment. text: | az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment """ helps['containerapp dapr show'] = """ type: command - short-summary: Show the details of a dapr component. + short-summary: Show the details of a Dapr component. examples: - - name: Show the details of a dapr component. + - name: Show the details of a Dapr component. text: | az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ helps['containerapp dapr set'] = """ type: command - short-summary: Create or update a dapr component. + short-summary: Create or update a Dapr component. examples: - - name: Create a dapr component. + - name: Create a Dapr component. text: | - az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName + az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml my-component.yaml --name MyDaprName """ helps['containerapp dapr remove'] = """ type: command - short-summary: Remove a dapr componenet from a Containerapp environment. + short-summary: Remove a Dapr component. examples: - - name: Remove a dapr componenet from a Containerapp environment. + - name: Remove a Dapr component. text: | az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c592ed5363d..85ee7f4239e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('container_name', type=str, options_list=['--container-name'], help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") + c.argument('env_vars', nargs='*', options_list=['--env-vars'], 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='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -151,8 +151,8 @@ def load_arguments(self, _): 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 dapr') 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="Tells 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', help="The dapr component environment name.") + c.argument('dapr_app_id', help="The Dapr app id.") + c.argument('dapr_app_port', help="The port Dapr uses to talk to the application.") + c.argument('dapr_app_protocol', help="The protocol Dapr uses to talk to the application. Allowed values: grpc, http.") + c.argument('dapr_component_name', help="The Dapr component name.") + c.argument('environment_name', help="The Container Apps environment name.") From ddc07c0d918a2c2b7c1bee2cdc4ca7fa4f691282 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:51:13 -0400 Subject: [PATCH 059/177] Env var options + various bug fixes (#26) * Moved dapr arguments to env as a subgroup. * Added env variable options. * Changed revision mode set to revision set-mode. * Added env var options to revision copy. * Fixed revision copy bug related to env secret refs. * Changed registry and secret delete to remove. Added registry param helps. Removed replica from table output and added trafficWeight. * Updating warning text. * Updated warning text once more. * Made name optional for revision copy if from-revision flag is passed. Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/_help.py | 102 +++++++------ .../azext_containerapp/_params.py | 30 +++- src/containerapp/azext_containerapp/_utils.py | 27 +++- .../azext_containerapp/commands.py | 21 ++- src/containerapp/azext_containerapp/custom.py | 138 ++++++++++++++---- 5 files changed, 214 insertions(+), 104 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 3a5fa25e5dc..cb5126c3f61 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -145,13 +145,13 @@ az containerapp revision deactivate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ -helps['containerapp revision mode set'] = """ +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 mode set-n MyContainerapp -g MyResourceGroup --mode Single + az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single """ helps['containerapp revision copy'] = """ @@ -164,15 +164,6 @@ --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ -helps['containerapp revision mode set'] = """ - type: command - short-summary: Set the revision mode of a Containerapp. - examples: - - name: Set the revision mode of a Containerapp. - text: | - az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup -""" - helps['containerapp revision copy'] = """ type: command short-summary: Create a revision based on a previous revision. @@ -238,6 +229,47 @@ az containerapp env list -g MyResourceGroup """ +helps['containerapp env dapr-component'] = """ + type: group + short-summary: Commands to manage Container App environment dapr components. +""" + +helps['containerapp env dapr-component list'] = """ + type: command + short-summary: List dapr components for a Containerapp environment. + examples: + - name: List dapr components for a Containerapp environment. + text: | + az containerapp env dapr-component list -g MyResourceGroup --environment-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 MyDaprComponenetName --environment-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 --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName +""" + +helps['containerapp env dapr-component remove'] = """ + type: command + short-summary: Remove a dapr componenet from a Containerapp environment. + examples: + - name: Remove a dapr componenet from a Containerapp environment. + text: | + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" + # Identity Commands helps['containerapp identity'] = """ type: group @@ -374,13 +406,13 @@ """ -helps['containerapp registry delete'] = """ +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 delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + az containerapp registry remove -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ # Secret Commands @@ -407,13 +439,13 @@ az containerapp secret list -n MyContainerapp -g MyResourceGroup """ -helps['containerapp secret delete'] = """ +helps['containerapp secret remove'] = """ type: command - short-summary: Delete secrets from a container app. + short-summary: Remove secrets from a container app. examples: - - name: Delete secrets from a container app. + - name: Remove secrets from a container app. text: | - az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 + az containerapp secret remove -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 """ helps['containerapp secret set'] = """ @@ -504,39 +536,3 @@ text: | az containerapp dapr disable -n MyContainerapp -g MyResourceGroup """ - -helps['containerapp dapr list'] = """ - type: command - short-summary: List Dapr components. - examples: - - name: List Dapr components for a Container Apps environment. - text: | - az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment -""" - -helps['containerapp dapr show'] = """ - type: command - short-summary: Show the details of a Dapr component. - examples: - - name: Show the details of a Dapr component. - text: | - az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment -""" - -helps['containerapp dapr set'] = """ - type: command - short-summary: Create or update a Dapr component. - examples: - - name: Create a Dapr component. - text: | - az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml my-component.yaml --name MyDaprName -""" - -helps['containerapp dapr remove'] = """ - type: command - short-summary: Remove a Dapr component. - examples: - - name: Remove a Dapr component. - text: | - az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment -""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 85ee7f4239e..1d3e3b6dc27 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -40,6 +40,13 @@ def load_arguments(self, _): c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') + # Env vars + with self.argument_context('containerapp', arg_group='Environment variables (Creates new revision)') as c: + c.argument('set_env_vars', options_list=['--set-env-vars, --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 (Creates new revision)') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") @@ -147,12 +154,21 @@ def load_arguments(self, _): 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 delete') as c: + 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 dapr') as c: - c.argument('dapr_app_id', help="The Dapr app id.") - c.argument('dapr_app_port', help="The port Dapr uses to talk to the application.") - c.argument('dapr_app_protocol', help="The protocol Dapr uses to talk to the application. Allowed values: grpc, http.") - c.argument('dapr_component_name', help="The Dapr component name.") - c.argument('environment_name', help="The Container Apps environment name.") + 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="Tells 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') + diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 297ce4904ba..1c5a10e5d29 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -116,6 +116,12 @@ def parse_secret_flags(secret_list): 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 store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass, update_existing_secret=False): if registry_pass.startswith("secretref:"): @@ -328,7 +334,7 @@ def _remove_secret(containerapp_def, secret_name): containerapp_def["properties"]["configuration"]["secrets"].pop(i) break -def _add_or_update_env_vars(existing_env_vars, new_env_vars): +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 @@ -336,6 +342,8 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars): 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"])) if "value" in new_env_var: existing_env_var["value"] = new_env_var["value"] @@ -350,8 +358,25 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars): # 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"])) 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 in range(0, len(existing_env_vars)): + existing_env_var = existing_env_vars[i] + 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)) def _add_or_update_tags(containerapp_def, tags): if 'tags' not in containerapp_def: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 40e422bb532..9fd58c7575c 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -26,7 +26,7 @@ def transform_containerapp_list_output(apps): def transform_revision_output(rev): - props = ['name', 'replicas', 'active', 'createdTime'] + props = ['name', 'active', 'createdTime', 'trafficWeight'] result = {k: rev['properties'][k] for k in rev['properties'] if k in props} if 'name' in rev: @@ -50,7 +50,6 @@ def load_command_table(self, _): g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) - with self.command_group('containerapp env') as g: g.custom_command('show', 'show_managed_environment') g.custom_command('list', 'list_managed_environments') @@ -58,13 +57,17 @@ def load_command_table(self, _): # g.custom_command('update', 'update_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_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 identity') as g: g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('show', 'show_managed_identity') - 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_command('show', 'show_github_action', exception_handler=ex_handler_factory()) @@ -77,9 +80,7 @@ def load_command_table(self, _): g.custom_command('restart', 'restart_revision') g.custom_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()) - - with self.command_group('containerapp revision mode') as g: - g.custom_command('set', 'set_revision_mode', 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()) @@ -94,19 +95,15 @@ def load_command_table(self, _): g.custom_command('set', 'set_registry', exception_handler=ex_handler_factory()) g.custom_command('show', 'show_registry') g.custom_command('list', 'list_registry') - g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) + 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_command('show', 'show_secret') - g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) + 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()) - g.custom_command('list', 'list_dapr_components') - g.custom_command('show', 'show_dapr_component') - g.custom_command('set', 'create_or_update_dapr_component') - g.custom_command('remove', 'remove_dapr_component') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1fcb7c2176b..961fbd500f0 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -45,7 +45,7 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, _update_revision_env_secretrefs) logger = get_logger(__name__) @@ -126,8 +126,10 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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() @@ -497,7 +499,10 @@ def update_containerapp(cmd, max_replicas=None, revisions_mode=None, secrets=None, - env_vars=None, + set_env_vars=None, + remove_env_vars=None, + replace_env_vars=None, + remove_all_env_vars=False, cpu=None, memory=None, registry_server=None, @@ -512,7 +517,7 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or secrets or env_vars or cpu or memory or registry_server or\ + revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ registry_user or registry_pass 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') @@ -539,7 +544,7 @@ def update_containerapp(cmd, update_map['secrets'] = secrets is not None update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None + 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 update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None if tags: @@ -564,13 +569,28 @@ def update_containerapp(cmd, if image is not None: c["image"] = image - if env_vars is not None: - if isinstance(env_vars, list) and not env_vars: + + if set_env_vars is not None: + if "env" not in c or not c["env"]: c["env"] = [] - else: - if "env" not in c or not c["env"]: - c["env"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + # 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 @@ -607,8 +627,23 @@ def update_containerapp(cmd, container_def = ContainerModel container_def["name"] = container_name container_def["image"] = image - if env_vars is not None: - container_def["env"] = parse_env_var_flags(env_vars) + 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 @@ -1284,16 +1319,19 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): handle_raw_exception(e) def copy_revision(cmd, - name, resource_group_name, from_revision=None, #label=None, + name=None, yaml=None, image=None, container_name=None, min_replicas=None, max_replicas=None, - env_vars=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, @@ -1303,12 +1341,16 @@ def copy_revision(cmd, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") - if not from_revision: - from_revision = containerapp_def["properties"]["latestRevisionName"] + 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\ - env_vars or cpu or memory 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) @@ -1322,13 +1364,15 @@ def copy_revision(cmd, if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) - 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) + 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) - containerapp_def["properties"]["template"] = r["properties"]["template"] + _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"]: @@ -1340,7 +1384,7 @@ def copy_revision(cmd, update_map = {} update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None + 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) @@ -1364,10 +1408,28 @@ def copy_revision(cmd, if image is not None: c["image"] = image - if env_vars is not None: + + 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"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + # 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 @@ -1404,8 +1466,22 @@ def copy_revision(cmd, container_def = ContainerModel container_def["name"] = container_name container_def["image"] = image - if env_vars is not None: - container_def["env"] = parse_env_var_flags(env_vars) + + 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 @@ -1706,7 +1782,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password except Exception as e: handle_raw_exception(e) -def delete_registry(cmd, name, resource_group_name, server, no_wait=False): +def remove_registry(cmd, name, resource_group_name, server, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1789,7 +1865,7 @@ def show_secret(cmd, name, resource_group_name, secret_name): return secret raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) -def delete_secrets(cmd, name, resource_group_name, secret_names, no_wait = False): +def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait = False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None From 5fe8e5f45adabf05049ecebc1b8957141b4f8d90 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Fri, 18 Mar 2022 13:05:12 -0400 Subject: [PATCH 060/177] Fixed style issues, various bug fixes (#27) * Moved dapr arguments to env as a subgroup. * Added env variable options. * Changed revision mode set to revision set-mode. * Added env var options to revision copy. * Fixed revision copy bug related to env secret refs. * Changed registry and secret delete to remove. Added registry param helps. Removed replica from table output and added trafficWeight. * Updating warning text. * Updated warning text once more. * Made name optional for revision copy if from-revision flag is passed. * Fixed whitespace style issues. * Styled clients and utils to pass pylint. * Finished client.py pylint fixes. * Fixed pylint issues. * Fixed flake8 commands and custom. * Fixed flake issues in src. * Added license header to _sdk_models. * Added confirmation for containerapp delete. Co-authored-by: Haroon Feisal --- .../azext_containerapp/__init__.py | 3 +- .../azext_containerapp/_client_factory.py | 4 +- .../azext_containerapp/_clients.py | 33 +- .../azext_containerapp/_github_oauth.py | 6 +- src/containerapp/azext_containerapp/_help.py | 11 +- .../azext_containerapp/_models.py | 101 ++--- .../azext_containerapp/_params.py | 16 +- .../azext_containerapp/_sdk_models.py | 10 +- src/containerapp/azext_containerapp/_utils.py | 142 ++++--- .../azext_containerapp/_validators.py | 16 +- .../azext_containerapp/commands.py | 17 +- src/containerapp/azext_containerapp/custom.py | 397 ++++++++---------- 12 files changed, 383 insertions(+), 373 deletions(-) diff --git a/src/containerapp/azext_containerapp/__init__.py b/src/containerapp/azext_containerapp/__init__.py index f772766731c..dcff6d86def 100644 --- a/src/containerapp/azext_containerapp/__init__.py +++ b/src/containerapp/azext_containerapp/__init__.py @@ -2,6 +2,7 @@ # 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 @@ -16,7 +17,7 @@ def __init__(self, cli_ctx=None): operations_tmpl='azext_containerapp.custom#{}', client_factory=None) super(ContainerappCommandsLoader, self).__init__(cli_ctx=cli_ctx, - custom_command_type=containerapp_custom) + custom_command_type=containerapp_custom) def load_command_table(self, args): from azext_containerapp.commands import load_command_table diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index f998486c63e..9a249cdbe7e 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -2,6 +2,7 @@ # 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 @@ -13,7 +14,6 @@ def ex_handler_factory(no_throw=False): def _polish_bad_errors(ex): import json - from knack.util import CLIError try: content = json.loads(ex.response.content) if 'message' in content: @@ -63,11 +63,13 @@ 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 diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 108ee5b004f..2dc138a6031 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -2,11 +2,12 @@ # 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 sys import api_version 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 @@ -15,8 +16,8 @@ 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 +POLLING_TIMEOUT = 60 # how many seconds before exiting +POLLING_SECONDS = 2 # how many seconds between requests class PollingAnimation(): @@ -37,7 +38,7 @@ def flush(self): sys.stdout.write("\033[K") -def poll(cmd, request_url, poll_if_status): +def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-return-statements try: start = time.time() end = time.time() + POLLING_TIMEOUT @@ -53,19 +54,17 @@ def poll(cmd, request_url, poll_if_status): r = send_raw_request(cmd.cli_ctx, "GET", request_url) r2 = r.json() - if not "properties" in r2 or not "provisioningState" in r2["properties"] or not r2["properties"]["provisioningState"].lower() == poll_if_status: + 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: + except Exception as e: # pylint: disable=broad-except animation.flush() - if poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete - return - - raise e + if not poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete + raise e class ContainerAppClient(): @@ -144,7 +143,6 @@ def delete(cls, cmd, resource_group_name, name): if r.status_code == 202: logger.warning('Containerapp successfully deleted') - return @classmethod def show(cls, cmd, resource_group_name, name): @@ -222,7 +220,6 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) @classmethod def list_secrets(cls, cmd, resource_group_name, name): - secrets = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -338,6 +335,7 @@ def deactivate_revision(cls, cmd, resource_group_name, container_app_name, name) 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): @@ -413,7 +411,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) if no_wait: - return # API doesn't return JSON (it returns no content) + 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( @@ -506,6 +504,7 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) return env_list + class GitHubActionClient(): @classmethod def create_or_update(cls, cmd, resource_group_name, name, github_action_envelope, headers, no_wait=False): @@ -552,7 +551,6 @@ def show(cls, cmd, resource_group_name, name): r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() - #TODO @classmethod def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager @@ -569,7 +567,7 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): 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) + 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( @@ -588,10 +586,10 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): 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): - #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -639,7 +637,7 @@ def delete(cls, cmd, resource_group_name, environment_name, name, no_wait=False) r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) if no_wait: - return # API doesn't return JSON (it returns no content) + 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( @@ -705,4 +703,3 @@ def list(cls, cmd, resource_group_name, environment_name, formatter=lambda x: x) 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 index 3df73a6b1aa..659d43afc39 100644 --- a/src/containerapp/azext_containerapp/_github_oauth.py +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -2,6 +2,7 @@ # 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 @@ -22,6 +23,7 @@ "workflow" ] + def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument if scope_list: for scope in scope_list: @@ -81,6 +83,6 @@ def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-arg 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)) + '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') \ No newline at end of file + 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 index cb5126c3f61..a4a71960f02 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -151,7 +151,7 @@ examples: - name: Set a container app to single revision mode. text: | - az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single + az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single """ helps['containerapp revision copy'] = """ @@ -368,7 +368,7 @@ examples: - name: Show a container app's ingress traffic configuration. text: | - az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup + az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup """ # Registry Commands @@ -392,7 +392,7 @@ examples: - name: List container registries configured in a container app. text: | - az containerapp registry list -n MyContainerapp -g MyResourceGroup + az containerapp registry list -n MyContainerapp -g MyResourceGroup """ helps['containerapp registry set'] = """ @@ -403,7 +403,6 @@ text: | az containerapp registry set -n MyContainerapp -g MyResourceGroup \\ --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword - """ helps['containerapp registry remove'] = """ @@ -454,10 +453,10 @@ examples: - name: Add secrets to a container app. text: | - az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName1=MySecretValue1 MySecretName2=MySecretValue2 + 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 + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue """ helps['containerapp github-action'] = """ diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index b356adaa2a8..d00798765c5 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -3,6 +3,7 @@ # 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, @@ -17,7 +18,7 @@ "tags": None, "properties": { "daprAIInstrumentationKey": None, - "vnetConfiguration": None, # VnetConfiguration + "vnetConfiguration": None, # VnetConfiguration "internalLoadBalancerEnabled": None, "appLogsConfiguration": None } @@ -63,15 +64,15 @@ "name": None, "command": None, "args": None, - "env": None, # [EnvironmentVar] - "resources": None, # ContainerResources - "volumeMounts": None, # [VolumeMount] + "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 + "storageType": "EmptyDir", # AzureFile or EmptyDir + "storageName": None # None for EmptyDir, otherwise name of storage resource } ScaleRuleAuth = { @@ -82,25 +83,25 @@ QueueScaleRule = { "queueName": None, "queueLength": None, - "auth": None # ScaleRuleAuth + "auth": None # ScaleRuleAuth } CustomScaleRule = { "type": None, "metadata": {}, - "auth": None # ScaleRuleAuth + "auth": None # ScaleRuleAuth } HttpScaleRule = { "metadata": {}, - "auth": None # ScaleRuleAuth + "auth": None # ScaleRuleAuth } ScaleRule = { "name": None, - "azureQueue": None, # QueueScaleRule - "customScaleRule": None, # CustomScaleRule - "httpScaleRule": None, # HttpScaleRule + "azureQueue": None, # QueueScaleRule + "customScaleRule": None, # CustomScaleRule + "httpScaleRule": None, # HttpScaleRule } Secret = { @@ -111,7 +112,7 @@ Scale = { "minReplicas": None, "maxReplicas": None, - "rules": [] # list of ScaleRule + "rules": [] # list of ScaleRule } TrafficWeight = { @@ -126,7 +127,7 @@ CustomDomain = { "name": None, - "bindingType": None, # BindingType + "bindingType": None, # BindingType "certificateId": None } @@ -134,9 +135,9 @@ "fqdn": None, "external": False, "targetPort": None, - "transport": None, # 'auto', 'http', 'http2' - "traffic": None, # TrafficWeight - "customDomains": None # [CustomDomain] + "transport": None, # 'auto', 'http', 'http2' + "traffic": None, # TrafficWeight + "customDomains": None # [CustomDomain] } RegistryCredentials = { @@ -147,17 +148,17 @@ Template = { "revisionSuffix": None, - "containers": None, # [Container] + "containers": None, # [Container] "scale": Scale, "dapr": Dapr, - "volumes": None # [Volume] + "volumes": None # [Volume] } Configuration = { - "secrets": None, # [Secret] - "activeRevisionsMode": None, # 'multiple' or 'single' - "ingress": None, # Ingress - "registries": None # [RegistryCredentials] + "secrets": None, # [Secret] + "activeRevisionsMode": None, # 'multiple' or 'single' + "ingress": None, # Ingress + "registries": None # [RegistryCredentials] } UserAssignedIdentity = { @@ -165,26 +166,26 @@ } ManagedServiceIdentity = { - "type": None, # 'None', 'SystemAssigned', 'UserAssigned', 'SystemAssigned,UserAssigned' - "userAssignedIdentities": None # {string: UserAssignedIdentity} + "type": None, # 'None', 'SystemAssigned', 'UserAssigned', 'SystemAssigned,UserAssigned' + "userAssignedIdentities": None # {string: UserAssignedIdentity} } ContainerApp = { "location": None, - "identity": None, # ManagedServiceIdentity + "identity": None, # ManagedServiceIdentity "properties": { "managedEnvironmentId": None, - "configuration": None, # Configuration - "template": None # Template + "configuration": None, # Configuration + "template": None # Template }, "tags": None } DaprComponent = { "properties": { - "componentType": None, #String + "componentType": None, # String "version": None, - "ignoreErrors": None, + "ignoreErrors": None, "initTimeout": None, "secrets": None, "metadata": None, @@ -193,39 +194,39 @@ } DaprMetadata = { - "key": None, #str - "value": None, #str - "secret_ref": None #str + "key": None, # str + "value": None, # str + "secret_ref": None # str } SourceControl = { "properties": { - "repoUrl": None, - "branch": None, - "githubActionConfiguration": None # [GitHubActionConfiguration] + "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": 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 + "registryUrl": None, # str + "registryUserName": None, # str + "registryPassword": None # str } AzureCredentials = { - "clientId": None, # str - "clientSecret": None, # str - "tenantId": None, #str - "subscriptionId": None #str + "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 index 1d3e3b6dc27..169b65edbe5 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -2,18 +2,19 @@ # 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 +# pylint: disable=line-too-long, too-many-statements, consider-using-f-string from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, - get_resource_name_completion_list, file_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 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']) @@ -73,7 +74,7 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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('assign_identity', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity.") 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'") @@ -101,7 +102,7 @@ def load_arguments(self, _): 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') + 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.') @@ -135,7 +136,7 @@ def load_arguments(self, _): 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.') @@ -162,7 +163,7 @@ def load_arguments(self, _): c.argument('dapr_app_port', help="The port of your app.") c.argument('dapr_app_protocol', help="Tells 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.") + 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.") @@ -171,4 +172,3 @@ def load_arguments(self, _): 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') - diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py index 9472034039d..b34325cdb9c 100644 --- a/src/containerapp/azext_containerapp/_sdk_models.py +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -1,9 +1,15 @@ +# -------------------------------------------------------------------------------------------- +# 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 @@ -196,8 +202,8 @@ class ProxyResource(Resource): 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, **kwargs): - super(ProxyResource, self).__init__(**kwargs) + # def __init__(self, **kwargs): + # super(ProxyResource, self).__init__(**kwargs) class AuthConfig(ProxyResource): diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 1c5a10e5d29..b1b3fa9bf9a 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -2,15 +2,14 @@ # 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 distutils.filelist import findall -from operator import is_ +from urllib.parse import urlparse from azure.cli.command_modules.appservice.custom import (_get_acr_cred) -from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError, RequiredArgumentMissingError) +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 urllib.parse import urlparse 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 @@ -38,7 +37,7 @@ def _validate_subscription_registered(cmd, resource_provider, subscription_id=No subscription_id, resource_provider, resource_provider)) except ValidationError as ex: raise ex - except Exception: + except Exception: # pylint: disable=broad-except pass @@ -62,7 +61,7 @@ def _ensure_location_allowed(cmd, location, resource_provider, resource_type): location, resource_provider, resource_type)) except ValidationError as ex: raise ex - except Exception: + except Exception: # pylint: disable=broad-except pass @@ -76,7 +75,7 @@ def parse_env_var_flags(env_list, is_update_containerapp=False): 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])) + 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 @@ -104,7 +103,7 @@ def parse_secret_flags(secret_list): 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])) + 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 = [] @@ -116,13 +115,23 @@ def parse_secret_flags(secret_list): return secret_var_def + def _update_revision_env_secretrefs(containers, name): for container in containers: - if "env" in container: + 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 @@ -139,33 +148,34 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ 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)) - secrets_list.append({ - "name": registry_secret_name, - "value": registry_pass - }) + 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 + 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. " @@ -173,6 +183,7 @@ def raise_missing_token_suggestion(): "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 @@ -184,20 +195,23 @@ def _get_default_log_analytics_location(cmd): if res and getattr(res, 'resource_type', "") == "workspaces": res_locations = getattr(res, 'locations', []) - if len(res_locations): + if len(res_locations) > 0: location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") if location: return location - except Exception: + except Exception: # pylint: disable=broad-except return default_location return default_location + # Generate random 4 character string def _new_tiny_guid(): - import random, string + 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 @@ -229,7 +243,7 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc log_analytics_location = location try: _ensure_location_allowed(cmd, log_analytics_location, "Microsoft.OperationalInsights", "workspaces") - except Exception: + except Exception: # pylint: disable=broad-except log_analytics_location = _get_default_log_analytics_location(cmd) from azure.cli.core.commands import LongRunningOperation @@ -237,7 +251,7 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc 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)) + 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) @@ -248,10 +262,10 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc 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") + 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 + 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) @@ -285,11 +299,12 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): secrets = [] try: secrets = ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) - except Exception as e: + 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): @@ -301,6 +316,7 @@ def _ensure_identity_resource_id(subscription_id, resource_group, resource): 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"] = [] @@ -312,28 +328,31 @@ def _add_or_update_secrets(containerapp_def, add_secrets): 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): + 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 i in range(0, len(containerapp_def["properties"]["configuration"]["secrets"])): - existing_secret = containerapp_def["properties"]["configuration"]["secrets"][i] + 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(i) + 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: @@ -343,7 +362,7 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars, is_add=False): 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"])) + 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"] @@ -359,16 +378,17 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars, is_add=False): # 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"])) + 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 in range(0, len(existing_env_vars)): - existing_env_var = existing_env_vars[i] + 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) @@ -376,7 +396,25 @@ def _remove_env_vars(existing_env_vars, remove_env_vars): # 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)) + 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: @@ -439,6 +477,7 @@ def _remove_readonly_attributes(containerapp_def): elif unneeded_property in containerapp_def['properties']: del containerapp_def['properties'][unneeded_property] + def _remove_dapr_readonly_attributes(daprcomponent_def): unneeded_properties = [ "id", @@ -457,13 +496,14 @@ def _remove_dapr_readonly_attributes(daprcomponent_def): 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 import collections for key, val in new_dict.items(): if isinstance(val, collections.Mapping): - tmp = update_nested_dictionary(orig_dict.get(key, { }), val) + tmp = update_nested_dictionary(orig_dict.get(key, {}), val) orig_dict[key] = tmp elif isinstance(val, list): if new_dict[key]: @@ -477,7 +517,7 @@ def update_nested_dictionary(orig_dict, new_dict): def _is_valid_weight(weight): try: n = int(weight) - if n >= 0 and n <= 100: + if 0 <= n <= 100: return True return False except ValueError: @@ -527,7 +567,7 @@ def _infer_acr_credentials(cmd, registry_server): 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)) + 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): diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 916d9eb5b57..e7fe0435a11 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -2,9 +2,9 @@ # 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 unicodedata import name -from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) +from azure.cli.core.azclierror import (ValidationError) def _is_number(s): @@ -14,6 +14,7 @@ def _is_number(s): except ValueError: return False + def validate_memory(namespace): memory = namespace.memory @@ -26,13 +27,15 @@ def validate_memory(namespace): 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: - raise ValidationError("Usage error: --cpu must be a number eg. \"0.5\"") + 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 @@ -48,6 +51,7 @@ def validate_managed_env_name_or_id(cmd, namespace): name=namespace.managed_env ) + def validate_registry_server(namespace): if "create" in namespace.command.lower(): if namespace.registry_server: @@ -55,24 +59,28 @@ def validate_registry_server(namespace): 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: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 9fd58c7575c..87a892201a8 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# pylint: disable=line-too-long -from azure.cli.core.commands import CliCommandType -from msrestazure.tools import is_valid_resource_id, parse_resource_id +# 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 @@ -15,7 +15,7 @@ def transform_containerapp_output(app): try: result['fqdn'] = app['properties']['configuration']['ingress']['fqdn'] - except Exception: + except: result['fqdn'] = None return result @@ -48,7 +48,7 @@ def load_command_table(self, _): 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_containerapp', confirmation=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: g.custom_command('show', 'show_managed_environment') @@ -70,8 +70,8 @@ def load_command_table(self, _): 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_command('show', 'show_github_action', exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_github_action', exception_handler=ex_handler_factory()) + g.custom_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') @@ -86,7 +86,7 @@ def load_command_table(self, _): g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) g.custom_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_command('show', 'show_ingress_traffic') @@ -106,4 +106,3 @@ def load_command_table(self, _): 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 index 961fbd500f0..d19ff49ea69 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2,23 +2,21 @@ # 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, ResourceNotFoundError, ValidationError) +from azure.cli.core.azclierror import (RequiredArgumentMissingError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger -from urllib.parse import urlparse from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError -from azure.cli.command_modules.appservice.custom import _get_acr_cred -from urllib.parse import urlparse from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient -from ._sdk_models import * +# from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, @@ -33,19 +31,19 @@ Dapr as DaprModel, ContainerResources as ContainerResourcesModel, Scale as ScaleModel, - Container as ContainerModel, - GitHubActionConfiguration, - RegistryInfo as RegistryInfoModel, - AzureCredentials as AzureCredentialsModel, + 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_list_of_strings, 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) + 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__) @@ -70,19 +68,20 @@ def load_yaml_file(file_name): import errno try: - with open(file_name) as stream: + 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 CLIError('{} does not exist'.format(file_name)) + raise CLIError('{} does not exist'.format(file_name)) from ex raise except (yaml.parser.ParserError, UnicodeDecodeError) as ex: - raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) + raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) from ex def create_deserializer(): from msrest import Deserializer - import sys, inspect + import sys + import inspect sdkClasses = inspect.getmembers(sys.modules["azext_containerapp._sdk_models"]) deserializer = {} @@ -95,7 +94,7 @@ def create_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: + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') if not yaml_containerapp.get('name'): @@ -114,7 +113,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev containerapp_def = None try: current_containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except Exception as ex: + except Exception: pass if not current_containerapp_def: @@ -126,17 +125,16 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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 @@ -158,54 +156,6 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev _remove_additional_attributes(current_containerapp_def) _remove_readonly_attributes(current_containerapp_def) - ''' - # Not sure if update should replace items that are a list, or do createOrUpdate. This commented out section is the implementation for createOrUpdate. - # (If a property is a list, do createOrUpdate, rather than just replace with new list) - - if 'properties' in containerapp_def and 'template' in containerapp_def['properties']: - # Containers - if 'containers' in containerapp_def['properties']['template'] and containerapp_def['properties']['template']['containers']: - for new_container in containerapp_def['properties']['template']['containers']: - if "name" not in new_container or not new_container["name"]: - raise ValidationError("The container name is not specified.") - - # Check if updating existing container - updating_existing_container = False - for existing_container in current_containerapp_def["properties"]["template"]["containers"]: - if existing_container['name'].lower() == new_container['name'].lower(): - updating_existing_container = True - - if 'image' in new_container and new_container['image']: - existing_container['image'] = new_container['image'] - if 'env' in new_container and new_container['env']: - if 'env' not in existing_container or not existing_container['env']: - existing_container['env'] = [] - _add_or_update_env_vars(existing_container['env'], new_container['env']) - if 'command' in new_container and new_container['command']: - existing_container['command'] = new_container['command'] - if 'args' in new_container and new_container['args']: - existing_container['args'] = new_container['args'] - if 'resources' in new_container and new_container['resources']: - if 'cpu' in new_container['resources'] and new_container['resources']['cpu'] is not None: - existing_container['resources']['cpu'] = new_container['resources']['cpu'] - if 'memory' in new_container['resources'] and new_container['resources']['memory'] is not None: - existing_container['resources']['memory'] = new_container['resources']['memory'] - - # If not updating existing container, add as new container - if not updating_existing_container: - current_containerapp_def["properties"]["template"]["containers"].append(new_container) - - # Traffic Weights - - # Secrets - - # Registries - - # Scale rules - - # Source Controls - - ''' 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) @@ -222,7 +172,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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: + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') if not yaml_containerapp.get('name'): @@ -244,7 +194,7 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= containerapp_def = deserializer('ContainerApp', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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 @@ -271,7 +221,7 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= env_rg = None env_info = None - if (is_valid_resource_id(env_id)): + 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'] @@ -334,14 +284,14 @@ def create_containerapp(cmd, args=None, tags=None, no_wait=False, - assign_identity=[]): + assign_identity=None): _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\ - location or startup_command or args or tags: + 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) @@ -351,6 +301,9 @@ def create_containerapp(cmd, if managed_env is None: raise RequiredArgumentMissingError('Usage error: --environment is required if not using --yaml') + if assign_identity is None: + assign_identity = [] + # Validate managed environment parsed_managed_env = parse_resource_id(managed_env) managed_env_name = parsed_managed_env['name'] @@ -416,19 +369,19 @@ def create_containerapp(cmd, if assign_system_identity and assign_user_identities: identity_def["type"] = "SystemAssigned, UserAssigned" - elif assign_system_identity: + elif assign_system_identity: identity_def["type"] = "SystemAssigned" - elif assign_user_identities: + elif assign_user_identities: identity_def["type"] = "UserAssigned" if assign_user_identities: identity_def["userAssignedIdentities"] = {} subscription_id = get_subscription_id(cmd.cli_ctx) - + for r in assign_user_identities: r = _ensure_identity_resource_id(subscription_id, resource_group_name, r) - identity_def["userAssignedIdentities"][r] = {} - + identity_def["userAssignedIdentities"][r] = {} # pylint: disable=unsupported-assignment-operation + scale_def = None if min_replicas is not None or max_replicas is not None: scale_def = ScaleModel @@ -517,9 +470,9 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ - registry_user or registry_pass or\ - startup_command or args or tags: + revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ + registry_user or registry_pass 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) @@ -771,20 +724,20 @@ def delete_containerapp(cmd, name, resource_group_name): 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, - app_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): + name, + resource_group_name, + logs_customer_id=None, + logs_key=None, + location=None, + instrumentation_key=None, + infrastructure_subnet_resource_id=None, + app_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) @@ -793,7 +746,7 @@ def create_managed_environment(cmd, # Microsoft.ContainerService RP registration is required for vnet enabled environments if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: - if (is_valid_resource_id(app_subnet_resource_id)): + if is_valid_resource_id(app_subnet_resource_id): parsed_app_subnet_resource_id = parse_resource_id(app_subnet_resource_id) subnet_subscription = parsed_app_subnet_resource_id["subscription"] _validate_subscription_registered(cmd, "Microsoft.ContainerService", subnet_subscription) @@ -862,28 +815,12 @@ def create_managed_environment(cmd, def update_managed_environment(cmd, - name, - resource_group_name, - tags=None, - no_wait=False): + name, + resource_group_name, + tags=None, + no_wait=False): raise CLIError('Containerapp env update is not yet supported.') - _validate_subscription_registered(cmd, "Microsoft.App") - - managed_env_def = ManagedEnvironmentModel - managed_env_def["tags"] = tags - - try: - r = ManagedEnvironmentClient.update( - 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 update in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) - - return r - except Exception as e: - handle_raw_exception(e) - def show_managed_environment(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -925,9 +862,9 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ 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_system_identity = '[system]' in identities assign_user_identities = [x for x in identities if x != '[system]'] containerapp_def = None @@ -944,7 +881,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) # If identity not returned - try: + try: containerapp_def["identity"] containerapp_def["identity"]["type"] except: @@ -956,30 +893,30 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ # Assign correct type try: - if containerapp_def["identity"]["type"] != "None": + 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: + else: if assign_system_identity and assign_user_identities: containerapp_def["identity"]["type"] = "SystemAssigned,UserAssigned" - elif assign_system_identity: + elif assign_system_identity: containerapp_def["identity"]["type"] = "SystemAssigned" - elif assign_user_identities: + elif assign_user_identities: containerapp_def["identity"]["type"] = "UserAssigned" - except: - # Always returns "type": "None" when CA has no previous identities + except: + # Always returns "type": "None" when CA has no previous identities pass - + if assign_user_identities: - try: + try: containerapp_def["identity"]["userAssignedIdentities"] - except: + 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") @@ -987,7 +924,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ containerapp_def["identity"]["userAssignedIdentities"][r] logger.warning("User identity {} is already assigned to containerapp".format(old_id)) except: - containerapp_def["identity"]["userAssignedIdentities"][r] = {} + 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) @@ -997,7 +934,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ 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") @@ -1010,7 +947,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= 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: @@ -1024,7 +961,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) # If identity not returned - try: + try: containerapp_def["identity"] containerapp_def["identity"]["type"] except: @@ -1041,17 +978,17 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= if remove_user_identities: subscription_id = get_subscription_id(cmd.cli_ctx) - try: + try: containerapp_def["identity"]["userAssignedIdentities"] - except: + except: containerapp_def["identity"]["userAssignedIdentities"] = {} - for id in remove_user_identities: - given_id = id - id = _ensure_identity_resource_id(subscription_id, resource_group_name, id) + 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() == id.lower(): + if old_user_identity.lower() == remove_id.lower(): containerapp_def["identity"]["userAssignedIdentities"].pop(old_user_identity) wasRemoved = True break @@ -1062,14 +999,14 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= 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") @@ -1080,10 +1017,12 @@ def show_managed_identity(cmd, name, resource_group_name): try: return r["identity"] - except: + except: r["identity"] = {} r["identity"]["type"] = "None" return r["identity"] + + def create_or_update_github_action(cmd, name, resource_group_name, @@ -1109,13 +1048,13 @@ def create_or_update_github_action(cmd, try: # Verify github repo from github import Github, GithubException - from github.GithubException import BadCredentialsException, UnknownObjectException + 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 @@ -1129,32 +1068,31 @@ def create_or_update_github_action(cmd, 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 CLIError(error_msg) + raise CLIError(error_msg) from e logger.warning('Verified GitHub repo and branch') - except BadCredentialsException: + except BadCredentialsException as e: raise CLIError("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.") + "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 CLIError(error_msg) + raise CLIError(error_msg) from e except CLIError as clierror: raise clierror - except Exception as ex: + 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 = client.get_source_control_info(resource_group_name, name).properties 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\"') + 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 @@ -1185,7 +1123,7 @@ def create_or_update_github_action(cmd, 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') + 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 @@ -1201,13 +1139,13 @@ def create_or_update_github_action(cmd, 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) + 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) @@ -1217,7 +1155,7 @@ def show_github_action(cmd, name, resource_group_name): 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: + try: github_action_config = GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) except Exception as e: handle_raw_exception(e) @@ -1236,13 +1174,13 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ try: # Verify github repo from github import Github, GithubException - from github.GithubException import BadCredentialsException, UnknownObjectException + 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 @@ -1250,21 +1188,21 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ github_repo = g.get_repo(repo) if not github_repo.permissions.push or not github_repo.permissions.maintain: raise CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) - except BadCredentialsException: + except BadCredentialsException as e: raise CLIError("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.") + "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 CLIError(error_msg) + raise CLIError(error_msg) from e except CLIError as clierror: raise clierror - except Exception as ex: + 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: @@ -1309,6 +1247,7 @@ def activate_revision(cmd, resource_group_name, revision_name, name=None): 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) @@ -1318,10 +1257,11 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): except CLIError as e: handle_raw_exception(e) + def copy_revision(cmd, resource_group_name, from_revision=None, - #label=None, + # label=None, name=None, yaml=None, image=None, @@ -1351,7 +1291,7 @@ def copy_revision(cmd, 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: + 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) @@ -1519,6 +1459,7 @@ def copy_revision(cmd, 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") @@ -1539,9 +1480,10 @@ def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): 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: + except Exception as e: handle_raw_exception(e) + def show_ingress(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1556,10 +1498,11 @@ def show_ingress(cmd, name, resource_group_name): try: return containerapp_def["properties"]["configuration"]["ingress"] - except: - raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) + except Exception as e: + raise CLIError("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): +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 @@ -1585,7 +1528,7 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, 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) @@ -1594,9 +1537,10 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, 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"] - except Exception as e: + 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") @@ -1614,13 +1558,14 @@ def disable_ingress(cmd, name, resource_group_name, no_wait=False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) try: - r = ContainerAppClient.create_or_update( + 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: + 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") @@ -1635,8 +1580,8 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait try: containerapp_def["properties"]["configuration"]["ingress"] - except: - raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") + except Exception as e: + raise CLIError("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) @@ -1647,9 +1592,10 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait 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: + except Exception as e: handle_raw_exception(e) + def show_ingress_traffic(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1664,8 +1610,9 @@ def show_ingress_traffic(cmd, name, resource_group_name): try: return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] - except: - raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") + except Exception as e: + raise CLIError("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") @@ -1681,8 +1628,8 @@ def show_registry(cmd, name, resource_group_name, server): try: containerapp_def["properties"]["configuration"]["registries"] - except: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) + except Exception as e: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) from e registries_def = containerapp_def["properties"]["configuration"]["registries"] @@ -1691,6 +1638,7 @@ def show_registry(cmd, name, resource_group_name, server): return r raise CLIError("The containerapp {} does not have specified registry assigned.".format(name)) + def list_registry(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1705,8 +1653,9 @@ def list_registry(cmd, name, resource_group_name): try: return containerapp_def["properties"]["configuration"]["registries"] - except: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) + except Exception as e: + raise CLIError("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") @@ -1741,7 +1690,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password 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') + 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 @@ -1770,10 +1719,9 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password server, password, update_existing_secret=True) - # Should this be false? ^ 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) @@ -1782,6 +1730,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password 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") @@ -1797,18 +1746,17 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) registries_def = None - registry = None try: containerapp_def["properties"]["configuration"]["registries"] - except: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) + except Exception as e: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) from e registries_def = containerapp_def["properties"]["configuration"]["registries"] wasRemoved = False - for i in range(0, len(registries_def)): - r = registries_def[i] + 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"]) @@ -1827,8 +1775,9 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): logger.warning("Registry successfully removed.") return r["properties"]["configuration"]["registries"] # No registries to return, so return nothing - except Exception as e: - return + except Exception: + pass + def list_secrets(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1844,8 +1793,9 @@ def list_secrets(cmd, name, resource_group_name): try: return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] - except: - raise CLIError("The containerapp {} has no assigned secrets.".format(name)) + except Exception as e: + raise CLIError("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") @@ -1865,7 +1815,8 @@ def show_secret(cmd, name, resource_group_name, secret_name): return secret raise CLIError("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): + +def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1900,10 +1851,10 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait = False except Exception as e: handle_raw_exception(e) -def set_secrets(cmd, name, resource_group_name, secrets, - #secrets=None, - #yaml=None, - no_wait = False): + +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: @@ -1911,7 +1862,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, # if not secrets: # secrets = [] - + # if yaml: # yaml_secrets = load_yaml_file(yaml).split(' ') # try: @@ -1940,6 +1891,7 @@ def set_secrets(cmd, name, resource_group_name, 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") @@ -1956,13 +1908,13 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= if 'dapr' not in containerapp_def['properties']: containerapp_def['properties']['dapr'] = {} - + if dapr_app_id: containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id - + if dapr_app_port: containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port - + if dapr_app_protocol: containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol @@ -1975,7 +1927,8 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= except Exception as e: handle_raw_exception(e) -def disable_dapr(cmd, name, resource_group_name, no_wait=False): + +def disable_dapr(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1998,21 +1951,24 @@ def disable_dapr(cmd, name, resource_group_name, no_wait=False): 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: + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') # Deserialize the yaml into a DaprComponent object. Need this since we're not using SDK @@ -2022,9 +1978,8 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, daprcomponent_def = deserializer('DaprComponent', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') from ex - #daprcomponent_def = _object_to_dict(daprcomponent_def) 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 @@ -2044,13 +1999,14 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, 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: + try: DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) - except: - raise CLIError("Dapr component not found.") + except Exception as e: + raise CLIError("Dapr component not found.") from e try: r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) @@ -2058,4 +2014,3 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ return r except Exception as e: handle_raw_exception(e) - From 87a6101a2f27d5a4d243c04feb0b02e139b23bdc Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Fri, 18 Mar 2022 14:52:03 -0400 Subject: [PATCH 061/177] Update src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py Co-authored-by: Xing Zhou --- .../tests/latest/test_containerapp_scenario.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py index f18855ca4eb..8605f1fe426 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -6,7 +6,7 @@ import os import unittest -from azure_devtools.scenario_tests import AllowLargeResponse +from azure.cli.testsdk.scenario_tests import AllowLargeResponse from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) From 8f4b5c66e4da8a0f47a2275a0382d72b1e247684 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:09:30 -0400 Subject: [PATCH 062/177] Specific Error Types + Bugfixes (Help, remove app-subnet-resource-id, removed env-var alias, added help text for --name) (#28) * Moved dapr arguments to env as a subgroup. * Added env variable options. * Changed revision mode set to revision set-mode. * Added env var options to revision copy. * Fixed revision copy bug related to env secret refs. * Changed registry and secret delete to remove. Added registry param helps. Removed replica from table output and added trafficWeight. * Updating warning text. * Updated warning text once more. * Made name optional for revision copy if from-revision flag is passed. * Fixed whitespace style issues. * Styled clients and utils to pass pylint. * Finished client.py pylint fixes. * Fixed pylint issues. * Fixed flake8 commands and custom. * Fixed flake issues in src. * Added license header to _sdk_models. * Added confirmation for containerapp delete. * Update helps for identity, revision. Removed env-var alias for set-env-vars. Added name param help. * Removed app-subnet-resource-id. * Updated infrastructure subnet param help. * Check if containerapp resource exists before attempting to delete. * Added check before deleting managed env. * Changed error types to be more specific. * Removed check before deletion. Removed comments. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_client_factory.py | 11 +- src/containerapp/azext_containerapp/_help.py | 18 ++- .../azext_containerapp/_params.py | 6 +- src/containerapp/azext_containerapp/custom.py | 150 ++++++++---------- 4 files changed, 87 insertions(+), 98 deletions(-) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 9a249cdbe7e..4e8ad424138 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -6,8 +6,7 @@ from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.profiles import ResourceType - -from knack.util import CLIError +from azure.cli.core.azclierror import CLIInternalError # pylint: disable=inconsistent-return-statements @@ -21,7 +20,7 @@ def _polish_bad_errors(ex): elif 'Message' in content: detail = content['Message'] - ex = CLIError(detail) + ex = CLIInternalError(detail) except Exception: # pylint: disable=broad-except pass if no_throw: @@ -45,13 +44,13 @@ def handle_raw_exception(e): if 'code' in jsonError and 'message' in jsonError: code = jsonError['code'] message = jsonError['message'] - raise CLIError('({}) {}'.format(code, message)) + raise CLIInternalError('({}) {}'.format(code, message)) elif "Message" in jsonError: message = jsonError["Message"] - raise CLIError(message) + raise CLIInternalError(message) elif "message" in jsonError: message = jsonError["message"] - raise CLIError(message) + raise CLIInternalError(message) raise e diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a4a71960f02..a306f2f6bd7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -124,7 +124,7 @@ examples: - name: Restart a revision. text: | - az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision + az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision MyContainerappRevision """ helps['containerapp revision activate'] = """ @@ -133,7 +133,7 @@ examples: - name: Activate a revision. text: | - az containerapp revision activate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision + az containerapp revision activate -g MyResourceGroup --revision MyContainerappRevision """ helps['containerapp revision deactivate'] = """ @@ -142,7 +142,7 @@ examples: - name: Deactivate a revision. text: | - az containerapp revision deactivate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision + az containerapp revision deactivate -g MyResourceGroup --revision MyContainerappRevision """ helps['containerapp revision set-mode'] = """ @@ -158,10 +158,15 @@ type: command short-summary: Create a revision based on a previous revision. examples: - - name: Create a revision based on a previous revision. + - 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 + """ helps['containerapp revision copy'] = """ @@ -231,7 +236,7 @@ helps['containerapp env dapr-component'] = """ type: group - short-summary: Commands to manage Container App environment dapr components. + short-summary: Commmands to manage dapr components on the Container App environment. """ helps['containerapp env dapr-component list'] = """ @@ -284,6 +289,9 @@ - name: Assign system identity. text: | az containerapp identity assign + - name: Assign user identity. + text: | + az containerapp identity assign --identities myAssignedId - name: Assign system and user identity. text: | az containerapp identity assign --identities [system] myAssignedId diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 169b65edbe5..0179e1f77f7 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -21,7 +21,7 @@ def load_arguments(self, _): with self.argument_context('containerapp') as c: # Base arguments - c.argument('name', name_type, metavar='NAME', id_part='name') + 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)) @@ -43,7 +43,7 @@ def load_arguments(self, _): # Env vars with self.argument_context('containerapp', arg_group='Environment variables (Creates new revision)') as c: - c.argument('set_env_vars', options_list=['--set-env-vars, --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('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.") @@ -97,7 +97,7 @@ def load_arguments(self, _): c.argument('instrumentation_key', options_list=['--dapr-instrumentation-key'], help='Azure Monitor 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. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + 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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d19ff49ea69..657b5995e03 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -6,9 +6,8 @@ from urllib.parse import urlparse from azure.cli.command_modules.appservice.custom import (_get_acr_cred) -from azure.cli.core.azclierror import (RequiredArgumentMissingError, ValidationError) +from azure.cli.core.azclierror import (RequiredArgumentMissingError, ValidationError, ResourceNotFoundError, CLIInternalError, InvalidArgumentValueError) from azure.cli.core.commands.client_factory import get_subscription_id -from knack.util import CLIError from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id @@ -72,10 +71,10 @@ def load_yaml_file(file_name): return yaml.safe_load(stream) except (IOError, OSError) as ex: if getattr(ex, 'errno', 0) == errno.ENOENT: - raise CLIError('{} does not exist'.format(file_name)) from ex + raise ValidationError('{} does not exist'.format(file_name)) from ex raise except (yaml.parser.ParserError, UnicodeDecodeError) as ex: - raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) from ex + raise ValidationError('Error parsing {} ({})'.format(file_name, str(ex))) from ex def create_deserializer(): @@ -123,7 +122,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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: + except CLIInternalError as e: handle_raw_exception(e) _update_revision_env_secretrefs(r["properties"]["template"]["containers"], name) current_containerapp_def["properties"]["template"] = r["properties"]["template"] @@ -483,7 +482,7 @@ def update_containerapp(cmd, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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"]: @@ -695,7 +694,7 @@ def show_containerapp(cmd, name, resource_group_name): try: return ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -710,7 +709,7 @@ def list_containerapp(cmd, resource_group_name=None): containerapps = ContainerAppClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return containerapps - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -719,7 +718,7 @@ def delete_containerapp(cmd, name, resource_group_name): try: return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -731,7 +730,6 @@ def create_managed_environment(cmd, location=None, instrumentation_key=None, infrastructure_subnet_resource_id=None, - app_subnet_resource_id=None, docker_bridge_cidr=None, platform_reserved_cidr=None, platform_reserved_dns_ip=None, @@ -744,15 +742,6 @@ def create_managed_environment(cmd, _validate_subscription_registered(cmd, "Microsoft.App") _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") - # Microsoft.ContainerService RP registration is required for vnet enabled environments - if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: - if is_valid_resource_id(app_subnet_resource_id): - parsed_app_subnet_resource_id = parse_resource_id(app_subnet_resource_id) - subnet_subscription = parsed_app_subnet_resource_id["subscription"] - _validate_subscription_registered(cmd, "Microsoft.ContainerService", subnet_subscription) - else: - raise ValidationError('Subnet resource ID is invalid.') - 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) @@ -773,19 +762,12 @@ def create_managed_environment(cmd, if instrumentation_key is not None: managed_env_def["properties"]["daprAIInstrumentationKey"] = instrumentation_key - if infrastructure_subnet_resource_id or app_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: + 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: - if not app_subnet_resource_id: - raise ValidationError('App subnet resource ID needs to be supplied with infrastructure subnet resource ID.') vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id - if app_subnet_resource_id is not None: - if not infrastructure_subnet_resource_id: - raise ValidationError('Infrastructure subnet resource ID needs to be supplied with app subnet resource ID.') - vnet_config_def["runtimeSubnetId"] = app_subnet_resource_id - if docker_bridge_cidr is not None: vnet_config_def["dockerBridgeCidr"] = docker_bridge_cidr @@ -798,8 +780,8 @@ def create_managed_environment(cmd, managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def if internal_only: - if not infrastructure_subnet_resource_id or not app_subnet_resource_id: - raise ValidationError('Infrastructure subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') + 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: @@ -819,7 +801,7 @@ def update_managed_environment(cmd, resource_group_name, tags=None, no_wait=False): - raise CLIError('Containerapp env update is not yet supported.') + raise CLIInternalError('Containerapp env update is not yet supported.') def show_managed_environment(cmd, name, resource_group_name): @@ -827,7 +809,7 @@ def show_managed_environment(cmd, name, resource_group_name): try: return ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -842,7 +824,7 @@ def list_managed_environments(cmd, resource_group_name=None): managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return managed_envs - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -851,7 +833,7 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): try: return ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -876,7 +858,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -956,7 +938,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -969,11 +951,11 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= containerapp_def["identity"]["type"] = "None" if containerapp_def["identity"]["type"] == "None": - raise CLIError("The containerapp {} has no system or user assigned identities.".format(name)) + raise InvalidArgumentValueError("The containerapp {} has no system or user assigned identities.".format(name)) if remove_system_identity: if containerapp_def["identity"]["type"] == "UserAssigned": - raise CLIError("The containerapp {} has no system assigned identities.".format(name)) + 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: @@ -994,7 +976,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= break if not wasRemoved: - raise CLIError("The containerapp does not have specified user identity '{}' assigned, so it cannot be removed.".format(given_id)) + 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 @@ -1012,7 +994,7 @@ def show_managed_identity(cmd, name, resource_group_name): try: r = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) try: @@ -1061,25 +1043,25 @@ def create_or_update_github_action(cmd, try: github_repo = g.get_repo(repo) if not github_repo.permissions.push or not github_repo.permissions.maintain: - raise CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) + 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 CLIError(error_msg) from e + raise CLIInternalError(error_msg) from e logger.warning('Verified GitHub repo and branch') except BadCredentialsException as e: - raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " + 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 CLIError(error_msg) from e - except CLIError as clierror: + raise CLIInternalError(error_msg) from e + except CLIInternalError 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 @@ -1187,17 +1169,17 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ try: github_repo = g.get_repo(repo) if not github_repo.permissions.push or not github_repo.permissions.maintain: - raise CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) + raise ValidationError("The token does not have appropriate access rights to repository {}.".format(repo)) except BadCredentialsException as e: - raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " + 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 CLIError(error_msg) from e - except CLIError as clierror: + raise CLIInternalError(error_msg) from e + except CLIInternalError 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 @@ -1214,7 +1196,7 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ 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: + except CLIInternalError as e: handle_raw_exception(e) @@ -1224,7 +1206,7 @@ def show_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1234,7 +1216,7 @@ def restart_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.restart_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1244,7 +1226,7 @@ def activate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.activate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1254,7 +1236,7 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.deactivate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1302,12 +1284,12 @@ def copy_revision(cmd, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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: + except CLIInternalError as e: # Error handle the case where revision not found? handle_raw_exception(e) @@ -1470,7 +1452,7 @@ def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = mode.lower() @@ -1494,12 +1476,12 @@ def show_ingress(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: return containerapp_def["properties"]["configuration"]["ingress"] except Exception as e: - raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) from 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 @@ -1512,7 +1494,7 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) external_ingress = None if type is not None: @@ -1551,7 +1533,7 @@ def disable_ingress(cmd, name, resource_group_name, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) containerapp_def["properties"]["configuration"]["ingress"] = None @@ -1576,12 +1558,12 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: containerapp_def["properties"]["configuration"]["ingress"] except Exception as e: - raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") from 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) @@ -1606,12 +1588,12 @@ def show_ingress_traffic(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] except Exception as e: - raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") from 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): @@ -1624,19 +1606,19 @@ def show_registry(cmd, name, resource_group_name, server): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: containerapp_def["properties"]["configuration"]["registries"] except Exception as e: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) from 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 CLIError("The containerapp {} does not have specified registry assigned.".format(name)) + raise InvalidArgumentValueError("The containerapp {} does not have specified registry assigned.".format(name)) def list_registry(cmd, name, resource_group_name): @@ -1649,12 +1631,12 @@ def list_registry(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: return containerapp_def["properties"]["configuration"]["registries"] except Exception as e: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) from 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): @@ -1667,7 +1649,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1741,7 +1723,7 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1750,7 +1732,7 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): try: containerapp_def["properties"]["configuration"]["registries"] except Exception as e: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) from e + raise ValidationError("The containerapp {} has no assigned registries.".format(name)) from e registries_def = containerapp_def["properties"]["configuration"]["registries"] @@ -1764,7 +1746,7 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): break if not wasRemoved: - raise CLIError("Containerapp does not have registry server {} assigned.".format(server)) + 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") @@ -1789,12 +1771,12 @@ def list_secrets(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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 CLIError("The containerapp {} has no assigned secrets.".format(name)) from e + raise ValidationError("The containerapp {} has no assigned secrets.".format(name)) from e def show_secret(cmd, name, resource_group_name, secret_name): @@ -1807,13 +1789,13 @@ def show_secret(cmd, name, resource_group_name, secret_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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 CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + 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): @@ -1826,7 +1808,7 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1838,7 +1820,7 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): wasRemoved = True break if not wasRemoved: - raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + 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) @@ -1868,7 +1850,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, # try: # parse_secret_flags(yaml_secrets) # except: - # raise CLIError("YAML secrets must be a list of secrets in key=value format, delimited by new line.") + # 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()) @@ -1879,7 +1861,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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)) @@ -1902,7 +1884,7 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1938,7 +1920,7 @@ def disable_dapr(cmd, name, resource_group_name, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -2006,7 +1988,7 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ try: DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) except Exception as e: - raise CLIError("Dapr component not found.") from e + raise ResourceNotFoundError("Dapr component not found.") from e try: r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) From 6f135aaaf8d282e12c1e4fbbfa79cda001fddf7e Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 10:45:42 -0700 Subject: [PATCH 063/177] Reset to 0.1.0 version, remove unneeded options-list --- src/containerapp/HISTORY.rst | 14 +--- .../azext_containerapp/_params.py | 38 ++++----- src/containerapp/setup.py | 2 +- src/index.json | 82 ------------------- 4 files changed, 21 insertions(+), 115 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 8400a3f0baf..1c139576ba0 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,18 +3,6 @@ Release History =============== -0.1.2 -++++++ -* Various fixes for bugs found -* Dapr subgroup -* Managed Identity - -0.1.1 -++++++ -* Various fixes for az containerapp create, update -* Added github actions support -* Added subgroups for ingress, registry, revision, secret - 0.1.0 ++++++ -* Initial release. \ No newline at end of file +* Initial release. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 0179e1f77f7..96ec69e5d8b 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -33,13 +33,13 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') 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, options_list=['--container-name'], help="Name of the container.") - c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") - c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars'], 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('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='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') + 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 (Creates new revision)') as c: @@ -50,29 +50,29 @@ def load_arguments(self, _): # Scale with self.argument_context('containerapp', arg_group='Scale (Creates new revision)') as c: - c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") + 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()) - c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="The port Dapr uses to talk to the application.") - c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") - c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="The protocol Dapr uses to talk to the application.") - c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") + 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.") + c.argument('dapr_components', help="The name of a yaml file containing a list of dapr components.") # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: - c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the container app.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server'], help="The container registry server hostname, e.g. myregistry.azurecr.io.") + 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, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") - c.argument('target_port', type=int, validator=validate_target_port, options_list=['--target-port'], help="The application port used for ingress traffic.") + 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: @@ -80,8 +80,8 @@ def load_arguments(self, _): 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, options_list=['--min-replicas'], help="The minimum number of replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") + 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.') @@ -94,7 +94,7 @@ def load_arguments(self, _): 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='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') + 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.') diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 96524e9ab67..be4cd26f637 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.2' +VERSION = '0.1.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index d57ae6de96e..32019fe8325 100644 --- a/src/index.json +++ b/src/index.json @@ -12235,88 +12235,6 @@ "sha256Digest": "9a796d5187571990d27feb9efeedde38c194f13ea21cbf9ec06131196bfd821d" } ], - "containerapp": [ - { - "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.1-py2.py3-none-any.whl", - "filename": "containerapp-0.1.1-py2.py3-none-any.whl", - "metadata": { - "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67", - "extensions": { - "python.details": { - "contacts": [ - { - "email": "azpycli@microsoft.com", - "name": "Microsoft Corporation", - "role": "author" - } - ], - "document_names": { - "description": "DESCRIPTION.rst" - }, - "project_urls": { - "Home": "https://github.com/Azure/azure-cli-extensions" - } - } - }, - "extras": [], - "generator": "bdist_wheel (0.30.0)", - "license": "MIT", - "metadata_version": "2.0", - "name": "containerapp", - "run_requires": [ - { - "requires": [ - "azure-cli-core" - ] - } - ], - "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", - "version": "0.1.1" - }, - "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" - }, - { - "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.2-py2.py3-none-any.whl", - "filename": "containerapp-0.1.2-py2.py3-none-any.whl", - "metadata": { - "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67", - "extensions": { - "python.details": { - "contacts": [ - { - "email": "azpycli@microsoft.com", - "name": "Microsoft Corporation", - "role": "author" - } - ], - "document_names": { - "description": "DESCRIPTION.rst" - }, - "project_urls": { - "Home": "https://github.com/Azure/azure-cli-extensions" - } - } - }, - "extras": [], - "generator": "bdist_wheel (0.30.0)", - "license": "MIT", - "metadata_version": "2.0", - "name": "containerapp", - "run_requires": [ - { - "requires": [ - "azure-cli-core" - ] - } - ], - "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", - "version": "0.1.2" - }, - "sha256Digest": "b1d4cc823f761cfb5469f8d53a9fa04bdc1493c3c5d5f3a90333876287e7b2f8" - } - ], "cosmosdb-preview": [ { "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/cosmosdb_preview-0.1.0-py2.py3-none-any.whl", From 1977e33368ef04a85254087798f4b19b066fdfc4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 10:59:15 -0700 Subject: [PATCH 064/177] Update min cli core version --- src/containerapp/azext_containerapp/azext_metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json index 001f223de90..cf7b8927a07 100644 --- a/src/containerapp/azext_containerapp/azext_metadata.json +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67" + "azext.minCliCoreVersion": "2.15.0" } From c7ed3ca08f8a2c24c0621738fb8a3238fa94970f Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:21:25 -0400 Subject: [PATCH 065/177] Fixed style issues. (#30) Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/custom.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 657b5995e03..06a9c922c3b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1054,8 +1054,8 @@ def create_or_update_github_action(cmd, 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 + "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']: @@ -1172,8 +1172,8 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ 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 + "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']: From 35017f1c6025e9d079d7da39b9dfaf2431da81c7 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 11:42:48 -0700 Subject: [PATCH 066/177] Fix linter issues --- src/containerapp/azext_containerapp/_help.py | 24 +++++-------------- .../azext_containerapp/_params.py | 7 ++++-- .../azext_containerapp/commands.py | 1 - 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a306f2f6bd7..2c6a5009069 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -64,14 +64,6 @@ text: az containerapp delete -g MyResourceGroup -n MyContainerapp """ -helps['containerapp scale'] = """ - type: command - short-summary: Set the min and max replicas for a container app (latest revision in multiple revisions mode). - examples: - - name: Scale a container's latest revision. - text: az containerapp scale -g MyResourceGroup -n MyContainerapp --min-replicas 1 --max-replicas 2 -""" - helps['containerapp show'] = """ type: command short-summary: Show details of a container app. @@ -106,7 +98,7 @@ - name: Show details of a revision. text: | az containerapp revision show -n MyContainerapp -g MyResourceGroup \\ - --revision-name MyContainerappRevision + --revision MyContainerappRevision """ helps['containerapp revision list'] = """ @@ -200,10 +192,6 @@ --location "Canada Central" """ -helps['containerapp env update'] = """ - type: command - short-summary: Update a Container Apps environment. Currently Unsupported. -""" helps['containerapp env delete'] = """ type: command @@ -245,7 +233,7 @@ examples: - name: List dapr components for a Containerapp environment. text: | - az containerapp env dapr-component list -g MyResourceGroup --environment-name MyEnvironment + az containerapp env dapr-component list -g MyResourceGroup --name MyEnvironment """ helps['containerapp env dapr-component show'] = """ @@ -254,7 +242,7 @@ examples: - name: Show the details of a dapr component. text: | - az containerapp env dapr-component show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment + az containerapp env dapr-component show -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment """ helps['containerapp env dapr-component set'] = """ @@ -263,7 +251,7 @@ examples: - name: Create a dapr component. text: | - az containerapp env dapr-component set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName + az containerapp env dapr-component set -g MyResourceGroup --name MyEnv --yaml MyYAMLPath --dapr-component-name MyDaprComponentName """ helps['containerapp env dapr-component remove'] = """ @@ -272,7 +260,7 @@ examples: - name: Remove a dapr componenet from a Containerapp environment. text: | - az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --name MyEnvironment """ # Identity Commands @@ -303,7 +291,7 @@ examples: - name: Remove system identity. text: | - az containerapp identity remove [system] + az containerapp identity remove --identities [system] - name: Remove system and user identity. text: | az containerapp identity remove --identities [system] myAssignedId diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 96ec69e5d8b..e2006b28187 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -2,7 +2,7 @@ # 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 +# pylint: disable=line-too-long, too-many-statements, consider-using-f-string, option-length-too-long from knack.arguments import CLIArgumentType @@ -55,7 +55,7 @@ def load_arguments(self, _): # 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()) + 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.") @@ -155,6 +155,9 @@ def load_arguments(self, _): 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.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 87a892201a8..f2f67098d34 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -54,7 +54,6 @@ def load_command_table(self, _): g.custom_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('update', 'update_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: From cf0345e701fc4631945bd5e1ab5f4c0bafef1c10 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 11:50:11 -0700 Subject: [PATCH 067/177] Use custom-show-command --- .../azext_containerapp/commands.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index f2f67098d34..4cff20cf47e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -44,32 +44,32 @@ def transform_revision_list_output(revs): def load_command_table(self, _): with self.command_group('containerapp') as g: - g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) + 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', confirmation=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: - g.custom_command('show', 'show_managed_environment') + 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_command('show', 'show_dapr_component') + 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 identity') as g: g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('show', 'show_managed_identity') + g.custom_show_command('show', 'show_managed_identity') 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_command('show', 'show_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: @@ -77,28 +77,28 @@ def load_command_table(self, _): 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_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) + 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_command('show', 'show_ingress') + 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_command('show', 'show_ingress_traffic') + 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_command('show', 'show_registry') + 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_command('show', 'show_secret') + 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()) From bddcd61d22d9a3878c2f1c86fbaa976a3638d455 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 21 Mar 2022 15:02:54 -0400 Subject: [PATCH 068/177] Removed --ids from revision, secret, registry list. --- src/containerapp/azext_containerapp/_params.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index e2006b28187..4c7d90c5e11 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -175,3 +175,12 @@ def load_arguments(self, _): 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) From 9a1354f45264bf4c867e07c2b8c319dcc7ec2941 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 12:30:29 -0700 Subject: [PATCH 069/177] Add linter exclusions --- linter_exclusions.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) 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: From a37559353121b98050ed3b276fa84db83c6a3b77 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 12:38:28 -0700 Subject: [PATCH 070/177] Fix polling on delete containerapp --- .../azext_containerapp/_clients.py | 27 ++++++++++++++++--- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 4 +-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 2dc138a6031..ada66cacf0d 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -63,7 +63,9 @@ def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-retu except Exception as e: # pylint: disable=broad-except animation.flush() - if not poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete + delete_statuses = ["scheduledfordelete", "cancelled"] + + if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete raise e @@ -127,7 +129,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= return r.json() @classmethod - def delete(cls, cmd, resource_group_name, name): + 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) @@ -141,8 +143,25 @@ def delete(cls, cmd, resource_group_name, name): r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) - if r.status_code == 202: - logger.warning('Containerapp successfully deleted') + 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): diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 4cff20cf47e..bdc2b14cb1b 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -48,7 +48,7 @@ def load_command_table(self, _): 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', confirmation=True, exception_handler=ex_handler_factory()) + 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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 06a9c922c3b..aadaccde746 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -713,11 +713,11 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) -def delete_containerapp(cmd, name, resource_group_name): +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) + return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) except CLIInternalError as e: handle_raw_exception(e) From 956875ccf84ced79a5c57fe4c25b5112b89007e1 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 12:52:44 -0700 Subject: [PATCH 071/177] Fix error handling --- src/containerapp/azext_containerapp/custom.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index aadaccde746..ae119aa5c92 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -6,7 +6,13 @@ 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, CLIInternalError, InvalidArgumentValueError) +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 @@ -122,7 +128,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev if from_revision: try: r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) - except CLIInternalError as e: + 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"] @@ -694,7 +700,7 @@ def show_containerapp(cmd, name, resource_group_name): try: return ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -709,7 +715,7 @@ def list_containerapp(cmd, resource_group_name=None): containerapps = ContainerAppClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return containerapps - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -718,7 +724,7 @@ def delete_containerapp(cmd, name, resource_group_name, no_wait=False): try: return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -809,7 +815,7 @@ def show_managed_environment(cmd, name, resource_group_name): try: return ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -824,7 +830,7 @@ def list_managed_environments(cmd, resource_group_name=None): managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return managed_envs - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -833,7 +839,7 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): try: return ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -994,7 +1000,7 @@ def show_managed_identity(cmd, name, resource_group_name): try: r = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) try: @@ -1061,7 +1067,7 @@ def create_or_update_github_action(cmd, if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) raise CLIInternalError(error_msg) from e - except CLIInternalError as clierror: + 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 @@ -1179,7 +1185,7 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) raise CLIInternalError(error_msg) from e - except CLIInternalError as clierror: + 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 @@ -1196,7 +1202,7 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ def list_revisions(cmd, name, resource_group_name): try: return ContainerAppClient.list_revisions(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1206,7 +1212,7 @@ def show_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1216,7 +1222,7 @@ def restart_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.restart_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1226,7 +1232,7 @@ def activate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.activate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1236,7 +1242,7 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.deactivate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1289,7 +1295,7 @@ def copy_revision(cmd, if from_revision: try: r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) - except CLIInternalError as e: + except CLIError as e: # Error handle the case where revision not found? handle_raw_exception(e) From 7606bae3b8c9d7aac1119d602f068c7c4335de59 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 13:09:04 -0700 Subject: [PATCH 072/177] Add Container App Service --- src/service_name.json | 5 +++++ 1 file changed, 5 insertions(+) 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", From 7ceb9c319e5d687e164b61424a7fc5a6cbd01e06 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 13:17:20 -0700 Subject: [PATCH 073/177] Fix flake linter --- src/containerapp/azext_containerapp/_clients.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index ada66cacf0d..77cf596c8bf 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -65,7 +65,7 @@ def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-retu delete_statuses = ["scheduledfordelete", "cancelled"] - if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete + if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete raise e @@ -144,7 +144,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) if no_wait: - return # API doesn't return JSON (it returns no content) + 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( @@ -162,7 +162,6 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): pass logger.warning('Containerapp successfully deleted') - @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager From 9683a858dbaef500f8a7b53ef231cb42abda6883 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 07:57:12 -0700 Subject: [PATCH 074/177] Fix help text --- src/containerapp/azext_containerapp/_help.py | 29 ++++++++++--------- .../azext_containerapp/_params.py | 11 ++++--- src/containerapp/azext_containerapp/custom.py | 1 - 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 2c6a5009069..8fefb932f53 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -9,7 +9,7 @@ helps['containerapp'] = """ type: group - short-summary: Commands to manage Azure Container Apps. + short-summary: Manage Azure Container Apps. """ helps['containerapp create'] = """ @@ -21,17 +21,18 @@ 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 myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --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 myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples @@ -224,43 +225,43 @@ helps['containerapp env dapr-component'] = """ type: group - short-summary: Commmands to manage dapr components on the Container App environment. + 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 a Containerapp environment. + short-summary: List Dapr components for an environment. examples: - - name: List dapr components for a Containerapp environment. + - 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. + short-summary: Show the details of a Dapr component. examples: - - name: Show the details of a dapr component. + - 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. + short-summary: Create or update a Dapr component. examples: - - name: Create a dapr component. + - 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 componenet from a Containerapp environment. + short-summary: Remove a Dapr component from an environment. examples: - - name: Remove a dapr componenet from a Containerapp environment. + - name: Remove a Dapr component from a Container Apps environment. text: | - az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --name MyEnvironment + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment """ # Identity Commands @@ -511,7 +512,7 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage Dapr. + short-summary: Commands to manage Dapr. To manage Dapr components, see `az containerapp env dapr-component`. """ helps['containerapp dapr enable'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 4c7d90c5e11..52ec7310492 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -31,7 +31,7 @@ def load_arguments(self, _): 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 (Creates new revision)') as c: + 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") @@ -42,14 +42,14 @@ def load_arguments(self, _): 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 (Creates new revision)') as c: + 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 (Creates new revision)') as c: + 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.") @@ -59,7 +59,6 @@ def load_arguments(self, _): 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.") - c.argument('dapr_components', help="The name of a yaml file containing a list of dapr components.") # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: @@ -162,10 +161,10 @@ def load_arguments(self, _): 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_id', help="The Dapr app ID.") c.argument('dapr_app_port', help="The port of your app.") c.argument('dapr_app_protocol', help="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") - c.argument('dapr_component_name', help="The dapr component name.") + 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: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ae119aa5c92..11840025404 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -283,7 +283,6 @@ def create_containerapp(cmd, dapr_app_port=None, dapr_app_id=None, dapr_app_protocol=None, - # dapr_components=None, revision_suffix=None, startup_command=None, args=None, From 19b808aa3bd47bb7abbc80c8ee55d994818c13f4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 08:26:19 -0700 Subject: [PATCH 075/177] Mark extension as preview --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 52ec7310492..c70de7f12ac 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -163,7 +163,7 @@ def load_arguments(self, _): 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="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") + 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.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index bdc2b14cb1b..97da07cefc0 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -43,7 +43,7 @@ def transform_revision_list_output(revs): def load_command_table(self, _): - with self.command_group('containerapp') as g: + 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()) From 0c2318b8d2d8672a3d44907bc5d6e8bd6d302a4d Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 08:56:33 -0700 Subject: [PATCH 076/177] Add python 3.9 and 3.10 as supported --- src/containerapp/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index be4cd26f637..e23b0011367 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -29,6 +29,8 @@ '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', ] From de4b8a876f85f16d988d3a07b535bb501c057877 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 09:44:53 -0700 Subject: [PATCH 077/177] Remove registries and secrets from az containerapp update, in favor of registry and secret subgroup --- src/containerapp/azext_containerapp/custom.py | 62 +------------------ 1 file changed, 2 insertions(+), 60 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 11840025404..4a7ffd95912 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -455,16 +455,12 @@ def update_containerapp(cmd, min_replicas=None, max_replicas=None, revisions_mode=None, - secrets=None, set_env_vars=None, remove_env_vars=None, replace_env_vars=None, remove_all_env_vars=False, cpu=None, memory=None, - registry_server=None, - registry_user=None, - registry_pass=None, revision_suffix=None, startup_command=None, args=None, @@ -474,8 +470,7 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ - registry_user or registry_pass or\ + revisions_mode 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) @@ -498,11 +493,9 @@ def update_containerapp(cmd, e["value"] = "" update_map = {} - update_map['secrets'] = secrets is not None - update_map['registries'] = registry_server or registry_user or registry_pass 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 - update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None + update_map['configuration'] = revisions_mode is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -631,57 +624,6 @@ def update_containerapp(cmd, _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - if secrets is not None: - _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) - - if update_map["registries"]: - 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 registry_server: - raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") - - # Infer credentials if not supplied and its azurecr - if (registry_user is None or registry_pass is None) and not _registry_exists(containerapp_def, registry_server): - registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) - - # Check if updating existing registry - updating_existing_registry = False - for r in registries_def: - if r['server'].lower() == registry_server.lower(): - updating_existing_registry = True - - if registry_user: - r["username"] = registry_user - if registry_pass: - r["passwordSecretRef"] = store_as_secret_and_return_secret_ref( - containerapp_def["properties"]["configuration"]["secrets"], - r["username"], - r["server"], - registry_pass, - update_existing_secret=True) - - # If not updating existing registry, add as new registry - if not updating_existing_registry: - if not(registry_server is not None and registry_user is not None and registry_pass is not None): - raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required when adding a registry") - - registry = RegistryCredentialsModel - registry["server"] = registry_server - registry["username"] = registry_user - registry["passwordSecretRef"] = store_as_secret_and_return_secret_ref( - containerapp_def["properties"]["configuration"]["secrets"], - registry_user, - registry_server, - registry_pass, - 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) From ef0cbc1ec08826f60bed6be8ba934ddbe82bb394 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 10:07:08 -0700 Subject: [PATCH 078/177] Fix YAML not working --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4a7ffd95912..633e50544c9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -21,7 +21,7 @@ from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient -# from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import +from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, From 943cafd1ae2f8304e07df98d238bc24968a4a080 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 10:39:36 -0700 Subject: [PATCH 079/177] Move import to inside deserialize function --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 633e50544c9..a5d40c3c2d9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -21,7 +21,6 @@ from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient -from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, @@ -84,6 +83,7 @@ def load_yaml_file(file_name): def create_deserializer(): + from ._sdk_models import ContainerApp # pylint: disable=unused-import from msrest import Deserializer import sys import inspect From 51bc543338f82584b6c74ff5296453b76e6749aa Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 22 Mar 2022 15:24:54 -0400 Subject: [PATCH 080/177] Ingress enable --transport default. Secret list returns empty array. Secret update prints message saying user needs to restart their apps. Added show-values flag to secret list. Fixed yaml datetime field issues, replaced x00 values that also came up during testing. --- src/containerapp/azext_containerapp/custom.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a5d40c3c2d9..5f354d81960 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -47,13 +47,15 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _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 yaml_containerapp.get('systemData'): + yaml_containerapp['systemData'] = {} if not yaml_containerapp.get('properties'): yaml_containerapp['properties'] = {} @@ -73,7 +75,7 @@ def load_yaml_file(file_name): try: with open(file_name) as stream: # pylint: disable=unspecified-encoding - return yaml.safe_load(stream) + return yaml.safe_load(stream.read().replace('\x00', '')) except (IOError, OSError) as ex: if getattr(ex, 'errno', 0) == errno.ENOENT: raise ValidationError('{} does not exist'.format(file_name)) from ex @@ -1431,7 +1433,7 @@ def show_ingress(cmd, name, resource_group_name): 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 +def enable_ingress(cmd, name, resource_group_name, type, target_port, transport="auto", allow_insecure=False, no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1708,22 +1710,28 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): pass -def list_secrets(cmd, name, resource_group_name): +def list_secrets(cmd, name, resource_group_name, show_values=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) + r = 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 not show_values: + try: + return r["properties"]["configuration"]["secrets"] + except: + return [] 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 + except Exception: + return [] + # raise ValidationError("The containerapp {} has no assigned secrets.".format(name)) from e def show_secret(cmd, name, resource_group_name, secret_name): @@ -1816,6 +1824,7 @@ def set_secrets(cmd, name, resource_group_name, 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) + logger.warning("Containerapp must be restarted in order for secret changes to take effect.") return r["properties"]["configuration"]["secrets"] except Exception as e: handle_raw_exception(e) From 37030adc542ab3e38264b29a01b284e29479f716 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 23 Mar 2022 12:26:37 -0400 Subject: [PATCH 081/177] Fixed dapr in create. --- src/containerapp/azext_containerapp/custom.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5f354d81960..44a8f05cfc5 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -256,6 +256,11 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= name, resource_group_name )) + if "ingress" in containerapp_def["properties"]["configuration"] and "fqdn" in containerapp_def["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) @@ -420,10 +425,11 @@ def create_containerapp(cmd, dapr_def["appPort"] = dapr_app_port dapr_def["appProtocol"] = dapr_app_protocol + config_def["dapr"] = dapr_def + template_def = TemplateModel template_def["containers"] = [container_def] template_def["scale"] = scale_def - template_def["dapr"] = dapr_def if revision_suffix is not None: template_def["revisionSuffix"] = revision_suffix @@ -442,7 +448,10 @@ def create_containerapp(cmd, 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 ingress is not None and target_port is not None: + 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) @@ -740,6 +749,7 @@ def create_managed_environment(cmd, 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) @@ -1467,6 +1477,7 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport= 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) @@ -1848,13 +1859,13 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= containerapp_def['properties']['dapr'] = {} if dapr_app_id: - containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id + containerapp_def['properties']['dapr']['appId'] = dapr_app_id if dapr_app_port: - containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port + containerapp_def['properties']['dapr']['appPort'] = dapr_app_port if dapr_app_protocol: - containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol + containerapp_def['properties']['dapr']['appProtocol'] = dapr_app_protocol containerapp_def['properties']['dapr']['enabled'] = True From 60f9e4b616cdc0de8fb911d2cb07ec1638498019 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 23 Mar 2022 12:29:17 -0400 Subject: [PATCH 082/177] Revert "Ingress enable --transport default. Secret list returns empty array. Secret update prints message saying user needs to restart their apps. Added show-values flag to secret list. Fixed yaml datetime field issues, replaced x00 values that also came up during testing." This reverts commit 51bc543338f82584b6c74ff5296453b76e6749aa. --- src/containerapp/azext_containerapp/custom.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 44a8f05cfc5..2fe74119b7f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -47,15 +47,13 @@ _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, _remove_env_vars, _update_revision_env_secretrefs) + _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 yaml_containerapp.get('systemData'): - yaml_containerapp['systemData'] = {} if not yaml_containerapp.get('properties'): yaml_containerapp['properties'] = {} @@ -75,7 +73,7 @@ def load_yaml_file(file_name): try: with open(file_name) as stream: # pylint: disable=unspecified-encoding - return yaml.safe_load(stream.read().replace('\x00', '')) + 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 @@ -1443,7 +1441,7 @@ def show_ingress(cmd, name, resource_group_name): 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="auto", allow_insecure=False, no_wait=False): # pylint: disable=redefined-builtin +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 @@ -1721,28 +1719,22 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): pass -def list_secrets(cmd, name, resource_group_name, show_values=False): +def list_secrets(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None try: - r = containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + 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 not show_values: - try: - return r["properties"]["configuration"]["secrets"] - except: - return [] try: return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] - except Exception: - return [] - # raise ValidationError("The containerapp {} has no assigned secrets.".format(name)) from e + 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): @@ -1835,7 +1827,6 @@ def set_secrets(cmd, name, resource_group_name, 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) - logger.warning("Containerapp must be restarted in order for secret changes to take effect.") return r["properties"]["configuration"]["secrets"] except Exception as e: handle_raw_exception(e) From feb12a7f899b4471479bba6419a507d527ce5bb0 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 23 Mar 2022 13:46:48 -0400 Subject: [PATCH 083/177] Revert "Fixed dapr in create." This reverts commit 37030adc542ab3e38264b29a01b284e29479f716. --- src/containerapp/azext_containerapp/custom.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2fe74119b7f..a5d40c3c2d9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -254,11 +254,6 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= name, resource_group_name )) - if "ingress" in containerapp_def["properties"]["configuration"] and "fqdn" in containerapp_def["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) @@ -423,11 +418,10 @@ def create_containerapp(cmd, dapr_def["appPort"] = dapr_app_port dapr_def["appProtocol"] = dapr_app_protocol - config_def["dapr"] = dapr_def - template_def = TemplateModel template_def["containers"] = [container_def] template_def["scale"] = scale_def + template_def["dapr"] = dapr_def if revision_suffix is not None: template_def["revisionSuffix"] = revision_suffix @@ -446,10 +440,7 @@ def create_containerapp(cmd, 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 ingress is not None and target_port is not None: - 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) @@ -747,7 +738,6 @@ def create_managed_environment(cmd, 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) @@ -1475,7 +1465,6 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, 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) @@ -1850,13 +1839,13 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= containerapp_def['properties']['dapr'] = {} if dapr_app_id: - containerapp_def['properties']['dapr']['appId'] = dapr_app_id + containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id if dapr_app_port: - containerapp_def['properties']['dapr']['appPort'] = dapr_app_port + containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port if dapr_app_protocol: - containerapp_def['properties']['dapr']['appProtocol'] = dapr_app_protocol + containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol containerapp_def['properties']['dapr']['enabled'] = True From e639c006b4ec9f4f6fb98b510f465e0b1e797116 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 22 Mar 2022 15:24:54 -0400 Subject: [PATCH 084/177] Ingress enable --transport default. Secret list returns empty array. Secret update prints message saying user needs to restart their apps. Added show-values flag to secret list. Fixed yaml datetime field issues, replaced x00 values that also came up during testing. --- src/containerapp/azext_containerapp/custom.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a5d40c3c2d9..5f354d81960 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -47,13 +47,15 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _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 yaml_containerapp.get('systemData'): + yaml_containerapp['systemData'] = {} if not yaml_containerapp.get('properties'): yaml_containerapp['properties'] = {} @@ -73,7 +75,7 @@ def load_yaml_file(file_name): try: with open(file_name) as stream: # pylint: disable=unspecified-encoding - return yaml.safe_load(stream) + return yaml.safe_load(stream.read().replace('\x00', '')) except (IOError, OSError) as ex: if getattr(ex, 'errno', 0) == errno.ENOENT: raise ValidationError('{} does not exist'.format(file_name)) from ex @@ -1431,7 +1433,7 @@ def show_ingress(cmd, name, resource_group_name): 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 +def enable_ingress(cmd, name, resource_group_name, type, target_port, transport="auto", allow_insecure=False, no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1708,22 +1710,28 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): pass -def list_secrets(cmd, name, resource_group_name): +def list_secrets(cmd, name, resource_group_name, show_values=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) + r = 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 not show_values: + try: + return r["properties"]["configuration"]["secrets"] + except: + return [] 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 + except Exception: + return [] + # raise ValidationError("The containerapp {} has no assigned secrets.".format(name)) from e def show_secret(cmd, name, resource_group_name, secret_name): @@ -1816,6 +1824,7 @@ def set_secrets(cmd, name, resource_group_name, 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) + logger.warning("Containerapp must be restarted in order for secret changes to take effect.") return r["properties"]["configuration"]["secrets"] except Exception as e: handle_raw_exception(e) From 0bfdddd7048a321df0eae3944d7103626fad51a2 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 2 Feb 2022 20:27:02 -0800 Subject: [PATCH 085/177] Skeleton code --- .github/CODEOWNERS | 4 +- src/containerapp/HISTORY.rst | 8 +++ src/containerapp/README.rst | 5 ++ .../azext_containerapp/__init__.py | 32 ++++++++++ .../azext_containerapp/_client_factory.py | 12 ++++ src/containerapp/azext_containerapp/_help.py | 38 ++++++++++++ .../azext_containerapp/_params.py | 23 +++++++ .../azext_containerapp/_validators.py | 20 +++++++ .../azext_containerapp/azext_metadata.json | 5 ++ .../azext_containerapp/commands.py | 29 +++++++++ src/containerapp/azext_containerapp/custom.py | 20 +++++++ .../azext_containerapp/tests/__init__.py | 5 ++ .../tests/latest/__init__.py | 5 ++ .../latest/test_containerapp_scenario.py | 17 ++++++ src/containerapp/setup.cfg | 2 + src/containerapp/setup.py | 60 +++++++++++++++++++ 16 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 src/containerapp/HISTORY.rst create mode 100644 src/containerapp/README.rst create mode 100644 src/containerapp/azext_containerapp/__init__.py create mode 100644 src/containerapp/azext_containerapp/_client_factory.py create mode 100644 src/containerapp/azext_containerapp/_help.py create mode 100644 src/containerapp/azext_containerapp/_params.py create mode 100644 src/containerapp/azext_containerapp/_validators.py create mode 100644 src/containerapp/azext_containerapp/azext_metadata.json create mode 100644 src/containerapp/azext_containerapp/commands.py create mode 100644 src/containerapp/azext_containerapp/custom.py create mode 100644 src/containerapp/azext_containerapp/tests/__init__.py create mode 100644 src/containerapp/azext_containerapp/tests/latest/__init__.py create mode 100644 src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py create mode 100644 src/containerapp/setup.cfg create mode 100644 src/containerapp/setup.py 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/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst new file mode 100644 index 00000000000..8c34bccfff8 --- /dev/null +++ b/src/containerapp/HISTORY.rst @@ -0,0 +1,8 @@ +.. :changelog: + +Release History +=============== + +0.1.0 +++++++ +* Initial release. \ No newline at end of file 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..e19af22d9e8 --- /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. +# -------------------------------------------------------------------------------------------- + +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 + from azext_containerapp._client_factory import cf_containerapp + containerapp_custom = CliCommandType( + operations_tmpl='azext_containerapp.custom#{}', + client_factory=cf_containerapp) + 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..842d3a16731 --- /dev/null +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +def cf_containerapp(cli_ctx, *_): + + from azure.cli.core.commands.client_factory import get_mgmt_service_client + # TODO: Replace CONTOSO with the appropriate label and uncomment + # from azure.mgmt.CONTOSO import CONTOSOManagementClient + # return get_mgmt_service_client(cli_ctx, CONTOSOManagementClient) + return None diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py new file mode 100644 index 00000000000..27af014f101 --- /dev/null +++ b/src/containerapp/azext_containerapp/_help.py @@ -0,0 +1,38 @@ +# 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: Commands to manage Containerapps. +""" + +helps['containerapp create'] = """ + type: command + short-summary: Create a Containerapp. +""" + +helps['containerapp list'] = """ + type: command + short-summary: List Containerapps. +""" + +# helps['containerapp delete'] = """ +# type: command +# short-summary: Delete a Containerapp. +# """ + +# helps['containerapp show'] = """ +# type: command +# short-summary: Show details of a Containerapp. +# """ + +# helps['containerapp update'] = """ +# type: command +# short-summary: Update a Containerapp. +# """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py new file mode 100644 index 00000000000..c732a35b7ce --- /dev/null +++ b/src/containerapp/azext_containerapp/_params.py @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------------------------- +# 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 knack.arguments import CLIArgumentType + + +def load_arguments(self, _): + + from azure.cli.core.commands.parameters import tags_type + from azure.cli.core.commands.validators import get_default_location_from_resource_group + + containerapp_name_type = CLIArgumentType(options_list='--containerapp-name-name', help='Name of the Containerapp.', id_part='name') + + with self.argument_context('containerapp') as c: + c.argument('tags', tags_type) + c.argument('location', validator=get_default_location_from_resource_group) + c.argument('containerapp_name', containerapp_name_type, options_list=['--name', '-n']) + + with self.argument_context('containerapp list') as c: + c.argument('containerapp_name', containerapp_name_type, id_part=None) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py new file mode 100644 index 00000000000..821630f5f34 --- /dev/null +++ b/src/containerapp/azext_containerapp/_validators.py @@ -0,0 +1,20 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def example_name_or_id_validator(cmd, namespace): + # Example of a storage account name or ID validator. + # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters + from azure.cli.core.commands.client_factory import get_subscription_id + from msrestazure.tools import is_valid_resource_id, resource_id + if namespace.storage_account: + if not is_valid_resource_id(namespace.RESOURCE): + namespace.storage_account = resource_id( + subscription=get_subscription_id(cmd.cli_ctx), + resource_group=namespace.resource_group_name, + namespace='Microsoft.Storage', + type='storageAccounts', + name=namespace.storage_account + ) diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json new file mode 100644 index 00000000000..c2d0f4fe8d0 --- /dev/null +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -0,0 +1,5 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "azext.maxCliCoreVersion": "2.33.0" +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py new file mode 100644 index 00000000000..07d4b120e47 --- /dev/null +++ b/src/containerapp/azext_containerapp/commands.py @@ -0,0 +1,29 @@ +# -------------------------------------------------------------------------------------------- +# 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.commands import CliCommandType +from azext_containerapp._client_factory import cf_containerapp + + +def load_command_table(self, _): + + # TODO: Add command type here + # containerapp_sdk = CliCommandType( + # operations_tmpl='.operations#None.{}', + # client_factory=cf_containerapp) + + + with self.command_group('containerapp') as g: + g.custom_command('create', 'create_containerapp') + # g.command('delete', 'delete') + g.custom_command('list', 'list_containerapp') + # g.show_command('show', 'get') + # g.generic_update_command('update', setter_name='update', custom_func_name='update_containerapp') + + + with self.command_group('containerapp', is_preview=True): + pass + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py new file mode 100644 index 00000000000..01a6a709509 --- /dev/null +++ b/src/containerapp/azext_containerapp/custom.py @@ -0,0 +1,20 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.util import CLIError + + +def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): + raise CLIError('TODO: Implement `containerapp create`') + + +def list_containerapp(cmd, resource_group_name=None): + raise CLIError('TODO: Implement `containerapp list`') + + +def update_containerapp(cmd, instance, tags=None): + with cmd.update_context(instance) as c: + c.set_param('tags', tags) + return instance \ No newline at end of file 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..f18855ca4eb --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -0,0 +1,17 @@ +# -------------------------------------------------------------------------------------------- +# 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 unittest + +from azure_devtools.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +class ContainerappScenarioTest(ScenarioTest): + pass \ No newline at end of file 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..b9f57ada671 --- /dev/null +++ b/src/containerapp/setup.py @@ -0,0 +1,60 @@ +#!/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', + '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']}, +) \ No newline at end of file From 16afa6933b50ba2e6d6520a9963d6e2ea5d5590d Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 10:18:09 -0800 Subject: [PATCH 086/177] az containerapp env show --- .../azext_containerapp/_client_factory.py | 49 +++++++++++++++++++ .../azext_containerapp/_clients.py | 29 +++++++++++ src/containerapp/azext_containerapp/_help.py | 29 +++++------ .../azext_containerapp/_params.py | 11 ++--- .../azext_containerapp/commands.py | 15 +++--- src/containerapp/azext_containerapp/custom.py | 17 ++++--- 6 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_clients.py diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 842d3a16731..53c03131967 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -3,6 +3,55 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from knack.util import CLIError + + +# pylint: disable=inconsistent-return-statements +def ex_handler_factory(creating_plan=False, no_throw=False): + def _polish_bad_errors(ex): + import json + from knack.util import CLIError + try: + content = json.loads(ex.response.content) + if 'message' in content: + detail = content['message'] + elif 'Message' in content: + detail = content['Message'] + + if creating_plan: + if 'Requested features are not supported in region' in detail: + detail = ("Plan with linux worker is not supported in current region. For " + + "supported regions, please refer to https://docs.microsoft.com/" + "azure/app-service-web/app-service-linux-intro") + elif 'Not enough available reserved instance servers to satisfy' in detail: + detail = ("Plan with Linux worker can only be created in a group " + + "which has never contained a Windows worker, and vice versa. " + + "Please use a new resource group. Original error:" + detail) + ex = CLIError(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 CLIError('({}) {}'.format(code, message)) + raise e + + def cf_containerapp(cli_ctx, *_): from azure.cli.core.commands.client_factory import get_mgmt_service_client diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py new file mode 100644 index 00000000000..62b2fe00951 --- /dev/null +++ b/src/containerapp/azext_containerapp/_clients.py @@ -0,0 +1,29 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from sys import api_version +from azure.cli.core.util import send_raw_request +from azure.cli.core.commands.client_factory import get_subscription_id + + +API_VERSION = "2021-03-01" + + +class KubeEnvironmentClient(): + @classmethod + def show(cls, cmd, resource_group_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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() diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 27af014f101..2f77234a8f6 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -17,22 +17,17 @@ short-summary: Create a Containerapp. """ -helps['containerapp list'] = """ - type: command - short-summary: List Containerapps. +# Environment Commands +helps['containerapp env'] = """ + type: group + short-summary: Commands to manage Containerapps environments. """ -# helps['containerapp delete'] = """ -# type: command -# short-summary: Delete a Containerapp. -# """ - -# helps['containerapp show'] = """ -# type: command -# short-summary: Show details of a Containerapp. -# """ - -# helps['containerapp update'] = """ -# type: command -# short-summary: Update a Containerapp. -# """ +helps['containerapp env show'] = """ + type: command + short-summary: Show details of a Containerapp environment. + examples: + - name: Show the details of a Containerapp Environment. + text: | + az containerapp env show -n MyContainerappEnvironment -g MyResourceGroup +""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c732a35b7ce..9642e2de985 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -12,12 +12,7 @@ def load_arguments(self, _): from azure.cli.core.commands.parameters import tags_type from azure.cli.core.commands.validators import get_default_location_from_resource_group - containerapp_name_type = CLIArgumentType(options_list='--containerapp-name-name', help='Name of the Containerapp.', id_part='name') + name_type = CLIArgumentType(options_list=['--name', '-n']) - with self.argument_context('containerapp') as c: - c.argument('tags', tags_type) - c.argument('location', validator=get_default_location_from_resource_group) - c.argument('containerapp_name', containerapp_name_type, options_list=['--name', '-n']) - - with self.argument_context('containerapp list') as c: - c.argument('containerapp_name', containerapp_name_type, id_part=None) + with self.argument_context('containerapp env show') as c: + c.argument('name', name_type, help='Name of the Kubernetes Environment.') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 07d4b120e47..69bcd468a57 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -5,7 +5,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType -from azext_containerapp._client_factory import cf_containerapp +from azext_containerapp._client_factory import cf_containerapp, ex_handler_factory def load_command_table(self, _): @@ -18,12 +18,11 @@ def load_command_table(self, _): with self.command_group('containerapp') as g: g.custom_command('create', 'create_containerapp') - # g.command('delete', 'delete') - g.custom_command('list', 'list_containerapp') - # g.show_command('show', 'get') - # g.generic_update_command('update', setter_name='update', custom_func_name='update_containerapp') - with self.command_group('containerapp', is_preview=True): - pass - + with self.command_group('containerapp env') as g: + g.custom_command('show', 'show_kube_environment') + # g.custom_command('list', 'list_kube_environments') + # g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 01a6a709509..a5ab7043e76 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,18 +3,19 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.core.azclierror import (ResourceNotFoundError) from knack.util import CLIError +from ._client_factory import handle_raw_exception +from ._clients import KubeEnvironmentClient + def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): raise CLIError('TODO: Implement `containerapp create`') -def list_containerapp(cmd, resource_group_name=None): - raise CLIError('TODO: Implement `containerapp list`') - - -def update_containerapp(cmd, instance, tags=None): - with cmd.update_context(instance) as c: - c.set_param('tags', tags) - return instance \ No newline at end of file +def show_kube_environment(cmd, name, resource_group_name): + try: + return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except CLIError as e: + handle_raw_exception(e) From 23235914b44a6ae4748b23def771b2f8b9fa311a Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 12:10:38 -0800 Subject: [PATCH 087/177] List kube/managed environments --- .../azext_containerapp/_clients.py | 135 ++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 14 +- .../azext_containerapp/_params.py | 2 +- .../azext_containerapp/commands.py | 5 +- src/containerapp/azext_containerapp/custom.py | 41 +++++- 5 files changed, 193 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 62b2fe00951..7332c740398 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -9,6 +9,7 @@ API_VERSION = "2021-03-01" +NEW_API_VERSION = "2022-01-01-preview" class KubeEnvironmentClient(): @@ -27,3 +28,137 @@ def show(cls, cmd, resource_group_name, name): r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() + + @classmethod + def list_by_subscription(cls, cmd, formatter=lambda x: x): + kube_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + request_url = "{}/subscriptions/{}/providers/Microsoft.Web/kubeEnvironments?api-version={}".format( + management_hostname.strip('/'), + sub_id, + api_version) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list + + @classmethod + def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): + kube_list = [] + + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments?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 kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list + + +class ManagedEnvironmentClient(): + @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): + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list + + @classmethod + def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_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 kube in j["value"]: + formatted = formatter(kube) + kube_list.append(formatted) + + return kube_list diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 2f77234a8f6..52469aef296 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -30,4 +30,16 @@ - name: Show the details of a Containerapp Environment. text: | az containerapp env show -n MyContainerappEnvironment -g MyResourceGroup -""" \ No newline at end of file +""" + +helps['containerapp env list'] = """ + type: command + short-summary: List Containerapp environments by subscription or resource group. + examples: + - name: List Containerapp Environments by subscription. + text: | + az containerapp env list + - name: List Containerapp Environments by resource group. + text: | + az containerapp env list -g MyResourceGroup +""" diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 9642e2de985..545da01b7de 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -15,4 +15,4 @@ def load_arguments(self, _): name_type = CLIArgumentType(options_list=['--name', '-n']) with self.argument_context('containerapp env show') as c: - c.argument('name', name_type, help='Name of the Kubernetes Environment.') \ No newline at end of file + c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 69bcd468a57..bf81094d722 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -22,7 +22,10 @@ def load_command_table(self, _): with self.command_group('containerapp env') as g: g.custom_command('show', 'show_kube_environment') - # g.custom_command('list', 'list_kube_environments') + # g.custom_command('show', 'show_managed_environment') + g.custom_command('list', 'list_kube_environments') + # g.custom_command('list', 'list_managed_environments') + # g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a5ab7043e76..2eaa63c5ce7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -7,7 +7,7 @@ from knack.util import CLIError from ._client_factory import handle_raw_exception -from ._clients import KubeEnvironmentClient +from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): @@ -19,3 +19,42 @@ def show_kube_environment(cmd, name, resource_group_name): return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) except CLIError as e: handle_raw_exception(e) + + +def list_kube_environments(cmd, resource_group_name=None): + try: + kube_envs = [] + if resource_group_name is None: + kube_envs = KubeEnvironmentClient.list_by_subscription(cmd=cmd) + else: + kube_envs = KubeEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) + + return [e for e in kube_envs if "properties" in e and + "environmentType" in e["properties"] and + e["properties"]["environmentType"] and + e["properties"]["environmentType"].lower() == "managed"] + except CLIError as e: + handle_raw_exception(e) + + +def show_managed_environment(cmd, name, resource_group_name): + 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): + 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 [e for e in managed_envs if "properties" in e and + "environmentType" in e["properties"] and + e["properties"]["environmentType"] and + e["properties"]["environmentType"].lower() == "managed"] + except CLIError as e: + handle_raw_exception(e) From 9397d54ac5626b64a3d32eedab39a0f708dde991 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 19:21:31 -0800 Subject: [PATCH 088/177] Create kube environment, wait doesn't work yet --- .../azext_containerapp/_client_factory.py | 18 ++++ .../azext_containerapp/_clients.py | 18 ++++ src/containerapp/azext_containerapp/_help.py | 12 +++ .../azext_containerapp/_models.py | 45 ++++++++++ .../azext_containerapp/_params.py | 30 ++++++- src/containerapp/azext_containerapp/_utils.py | 54 ++++++++++++ .../azext_containerapp/commands.py | 3 +- src/containerapp/azext_containerapp/custom.py | 85 ++++++++++++++++++- 8 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_models.py create mode 100644 src/containerapp/azext_containerapp/_utils.py diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 53c03131967..4c8eeeb7f86 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -3,6 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.core.commands.client_factory import get_mgmt_service_client +from azure.cli.core.profiles import ResourceType + from knack.util import CLIError @@ -40,18 +43,33 @@ 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 CLIError('({}) {}'.format(code, message)) + elif "Message" in jsonError: + message = jsonError["Message"] + raise CLIError(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 cf_containerapp(cli_ctx, *_): from azure.cli.core.commands.client_factory import get_mgmt_service_client diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 7332c740398..4a2577a9d1c 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import json + from sys import api_version from azure.cli.core.util import send_raw_request from azure.cli.core.commands.client_factory import get_subscription_id @@ -13,6 +15,22 @@ class KubeEnvironmentClient(): + @classmethod + def create(cls, cmd, resource_group_name, name, kube_environment_envelope): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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(kube_environment_envelope)) + return r.json() + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 52469aef296..f0e33a7c83a 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -23,6 +23,18 @@ short-summary: Commands to manage Containerapps environments. """ +helps['containerapp env create'] = """ + type: command + short-summary: Create a Containerapp environment. + examples: + - name: Create a Containerapp Environment. + text: | + az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ + --logs-workspace-id myLogsWorkspaceID \\ + --logs-workspace-key myLogsWorkspaceKey \\ + --location Canada Central +""" + helps['containerapp env show'] = """ type: command short-summary: Show details of a Containerapp environment. diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py new file mode 100644 index 00000000000..71242502bdc --- /dev/null +++ b/src/containerapp/azext_containerapp/_models.py @@ -0,0 +1,45 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +ContainerAppsConfiguration = { + "daprAIInstrumentationKey": None, + "appSubnetResourceId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIP": None, + "internalOnly": False +} + +KubeEnvironment = { + "id": None, # readonly + "name": None, # readonly + "kind": None, + "location": None, + "tags": None, + "properties": { + "type": None, + "environmentType": None, + "containerAppsConfiguration": None, + "provisioningState": None, # readonly + "deploymentErrors": None, # readonly + "defaultDomain": None, # readonly + "staticIp": None, + "arcConfiguration": None, + "appLogsConfiguration": None, + "aksResourceId": None + }, + "extendedLocation": None +} + +AppLogsConfiguration = { + "destination": None, + "logAnalyticsConfiguration": None +} + +LogAnalyticsConfiguration = { + "customerId": None, + "sharedKey": None +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 545da01b7de..0c35090f82e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -6,13 +6,37 @@ from knack.arguments import CLIArgumentType +from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, + get_resource_name_completion_list, + get_three_state_flag, get_enum_type, tags_type) +from azure.cli.core.commands.validators import get_default_location_from_resource_group -def load_arguments(self, _): - from azure.cli.core.commands.parameters import tags_type - from azure.cli.core.commands.validators import get_default_location_from_resource_group +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') + 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 env') as c: + c.argument('name', name_type) + 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('logs_destination', options_list=['--logs-dest']) + c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') + c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') + c.argument('instrumentation_key', options_list=['--instrumentation-key']) + c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') + c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') + c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py new file mode 100644 index 00000000000..6b0a92b4914 --- /dev/null +++ b/src/containerapp/azext_containerapp/_utils.py @@ -0,0 +1,54 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.commands.client_factory import get_subscription_id + +from ._client_factory import providers_client_factory, cf_resource_groups + + +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): + providers_client = None + try: + providers_client = providers_client_factory(cmd.cli_ctx, get_subscription_id(cmd.cli_ctx)) + 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( + resource_provider, resource_provider)) + except ValidationError as ex: + raise ex + except Exception: + pass + + +def _ensure_location_allowed(cmd, location, resource_provider): + 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', "") == 'containerApps': + res_locations = getattr(res, 'locations', []) + + res_locations = [res_loc.lower().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=='containerApps'].locations'`".format( + location, resource_provider)) + except ValidationError as ex: + raise ex + except Exception: + pass diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index bf81094d722..be632e0a997 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -25,7 +25,8 @@ def load_command_table(self, _): # g.custom_command('show', 'show_managed_environment') g.custom_command('list', 'list_kube_environments') # g.custom_command('list', 'list_managed_environments') + g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2eaa63c5ce7..2e2bf3f467f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,17 +3,96 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from azure.cli.core.azclierror import (ResourceNotFoundError) +from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient +from ._models import KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed - -def create_containerapp(cmd, resource_group_name, containerapp_name, location=None, tags=None): +def create_containerapp(cmd, resource_group_name, name, location=None, tags=None): raise CLIError('TODO: Implement `containerapp create`') +def create_kube_environment(cmd, + name, + resource_group_name, + logs_customer_id, + logs_key, + logs_destination="log-analytics", + location=None, + instrumentation_key=None, + controlplane_subnet_resource_id=None, + app_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.Web") + _ensure_location_allowed(cmd, location, "Microsoft.Web") + + containerapps_config_def = ContainerAppsConfiguration + + if instrumentation_key is not None: + containerapps_config_def["daprAIInstrumentationKey"] = instrumentation_key + + if controlplane_subnet_resource_id is not None: + if not app_subnet_resource_id: + raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') + containerapps_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id + + if app_subnet_resource_id is not None: + if not controlplane_subnet_resource_id: + raise ValidationError('Controlplane subnet resource ID needs to be supplied with app subnet resource ID.') + containerapps_config_def["appSubnetResourceId"] = app_subnet_resource_id + + if docker_bridge_cidr is not None: + containerapps_config_def["dockerBridgeCidr"] = docker_bridge_cidr + + if platform_reserved_cidr is not None: + containerapps_config_def["platformReservedCidr"] = platform_reserved_cidr + + if platform_reserved_dns_ip is not None: + containerapps_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip + + if internal_only: + if not controlplane_subnet_resource_id or not app_subnet_resource_id: + raise ValidationError('Controlplane subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') + containerapps_config_def["internalOnly"] = True + + log_analytics_config_def = LogAnalyticsConfiguration + log_analytics_config_def["customerId"] = logs_customer_id + log_analytics_config_def["sharedKey"] = logs_key + + app_logs_config_def = AppLogsConfiguration + app_logs_config_def["destination"] = logs_destination + app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def + + kube_def = KubeEnvironment + kube_def["location"] = location + kube_def["properties"]["internalLoadBalancerEnabled"] = False + kube_def["properties"]["environmentType"] = "managed" + kube_def["properties"]["type"] = "managed" + kube_def["properties"]["appLogsConfiguration"] = app_logs_config_def + kube_def["properties"]["containerAppsConfiguration"] = containerapps_config_def + kube_def["tags"] = tags + + try: + return sdk_no_wait(no_wait, KubeEnvironmentClient.create, + cmd=cmd, resource_group_name=resource_group_name, + name=name, kube_environment_envelope=kube_def) + except Exception as e: + handle_raw_exception(e) + + def show_kube_environment(cmd, name, resource_group_name): try: return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) From 4fa3771ab75daafb912847bc6d4e7de07d269082 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 20:55:52 -0800 Subject: [PATCH 089/177] Update containerapp stubs (check if it is supported now) --- src/containerapp/azext_containerapp/_help.py | 5 +++++ src/containerapp/azext_containerapp/_params.py | 4 ++++ src/containerapp/azext_containerapp/commands.py | 3 ++- src/containerapp/azext_containerapp/custom.py | 8 ++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index f0e33a7c83a..62e7cd7740c 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -35,6 +35,11 @@ --location Canada Central """ +helps['containerapp env update'] = """ + type: command + short-summary: Update a Containerapp environment. Currently Unsupported. +""" + helps['containerapp env show'] = """ type: command short-summary: Show details of a Containerapp environment. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 0c35090f82e..cc9dece1784 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -38,5 +38,9 @@ def load_arguments(self, _): c.argument('internal_only', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env update') as c: + c.argument('name', name_type, help='Name of the kubernetes environment.') + c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index be632e0a997..44b72bd037d 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -27,6 +27,7 @@ def load_command_table(self, _): # g.custom_command('list', 'list_managed_environments') g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2e2bf3f467f..9f8cc1809fe 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -93,6 +93,14 @@ def create_kube_environment(cmd, handle_raw_exception(e) +def update_kube_environment(cmd, + name, + resource_group_name, + tags=None, + no_wait=False): + raise CLIError('Containerapp env update is not yet implemented') + + def show_kube_environment(cmd, name, resource_group_name): try: return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) From 0d7ce565350a9369a53fb29be3b8b8d3cd6379d8 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Feb 2022 21:05:44 -0800 Subject: [PATCH 090/177] Containerapp env delete, polling not working yet --- src/containerapp/azext_containerapp/_clients.py | 15 +++++++++++++++ src/containerapp/azext_containerapp/_help.py | 8 ++++++++ src/containerapp/azext_containerapp/_params.py | 3 +++ src/containerapp/azext_containerapp/commands.py | 4 ++-- src/containerapp/azext_containerapp/custom.py | 7 +++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 4a2577a9d1c..5c0c06f58a1 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -31,6 +31,21 @@ def create(cls, cmd, resource_group_name, name, kube_environment_envelope): r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(kube_environment_envelope)) return r.json() + @classmethod + def delete(cls, cmd, resource_group_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = API_VERSION + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + + send_raw_request(cmd.cli_ctx, "DELETE", request_url) # API doesn't return JSON for some reason + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 62e7cd7740c..a0b0f1421e8 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -40,6 +40,14 @@ short-summary: Update a Containerapp environment. Currently Unsupported. """ +helps['containerapp env delete'] = """ + type: command + short-summary: Deletes a Containerapp Environment. + examples: + - name: Delete Containerapp Environment. + text: az containerapp env delete -g MyResourceGroup -n MyContainerappEnvironment +""" + helps['containerapp env show'] = """ type: command short-summary: Show details of a Containerapp environment. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index cc9dece1784..2cc985f43ce 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -42,5 +42,8 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the kubernetes 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 Kubernetes Environment.') + with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Kubernetes Environment.') diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 44b72bd037d..7a94c3cb3e8 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -29,5 +29,5 @@ def load_command_table(self, _): # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - - # g.command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + # g.command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 9f8cc1809fe..b07a9e114d3 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -101,6 +101,13 @@ def update_kube_environment(cmd, raise CLIError('Containerapp env update is not yet implemented') +def delete_kube_environment(cmd, name, resource_group_name): + try: + return KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) + except CLIError as e: + handle_raw_exception(e) + + def show_kube_environment(cmd, name, resource_group_name): try: return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) From 055907fc575b6185faad72b0cbd431ed5fc23169 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 4 Feb 2022 15:03:22 -0800 Subject: [PATCH 091/177] Added polling for create and delete --- .../azext_containerapp/_clients.py | 86 ++++++++++++++++++- src/containerapp/azext_containerapp/custom.py | 21 +++-- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5c0c06f58a1..779aa1b2143 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -3,7 +3,10 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from ast import NotEq import json +import time +import sys from sys import api_version from azure.cli.core.util import send_raw_request @@ -12,11 +15,62 @@ 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): + 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 not "properties" in r2 or not "provisioningState" 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: + animation.flush() + + if poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete + return + + raise e class KubeEnvironmentClient(): @classmethod - def create(cls, cmd, resource_group_name, name, kube_environment_envelope): + def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) @@ -29,10 +83,23 @@ def create(cls, cmd, resource_group_name, name, kube_environment_envelope): api_version) r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(kube_environment_envelope)) + + if no_wait: + return r.json() + elif r.status_code == 201: + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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): + def delete(cls, cmd, resource_group_name, name, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) @@ -44,7 +111,20 @@ def delete(cls, cmd, resource_group_name, name): name, api_version) - send_raw_request(cmd.cli_ctx, "DELETE", request_url) # API doesn't return JSON for some reason + 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.Web/kubeEnvironments/{}?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + api_version) + poll(cmd, request_url, "scheduledfordelete") + return @classmethod def show(cls, cmd, resource_group_name, name): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index b07a9e114d3..4420b7f6516 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -7,12 +7,16 @@ from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait from knack.util import CLIError +from knack.log import get_logger from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient from ._models import KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed +logger = get_logger(__name__) + + def create_containerapp(cmd, resource_group_name, name, location=None, tags=None): raise CLIError('TODO: Implement `containerapp create`') @@ -86,9 +90,13 @@ def create_kube_environment(cmd, kube_def["tags"] = tags try: - return sdk_no_wait(no_wait, KubeEnvironmentClient.create, - cmd=cmd, resource_group_name=resource_group_name, - name=name, kube_environment_envelope=kube_def) + r = KubeEnvironmentClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=kube_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) @@ -101,9 +109,12 @@ def update_kube_environment(cmd, raise CLIError('Containerapp env update is not yet implemented') -def delete_kube_environment(cmd, name, resource_group_name): +def delete_kube_environment(cmd, name, resource_group_name, no_wait=False): try: - return KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) + r = KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + if not r and not no_wait: + logger.warning('Containerapp successfully deleted') + return r except CLIError as e: handle_raw_exception(e) From 31b2415b1d09f3235dd351dd12ccdda51180f42e Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 4 Feb 2022 16:25:32 -0800 Subject: [PATCH 092/177] Use Microsoft.App RP for show, list, delete command --- .../azext_containerapp/_clients.py | 28 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/commands.py | 9 ++---- src/containerapp/azext_containerapp/custom.py | 16 +++++++++-- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 779aa1b2143..dd7961f401c 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -202,6 +202,34 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) class ManagedEnvironmentClient(): + @classmethod + def delete(cls, cmd, resource_group_name, name, no_wait=False): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + api_version = 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) + poll(cmd, request_url, "scheduledfordelete") + return + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a0b0f1421e8..18ce06e05be 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -20,7 +20,7 @@ # Environment Commands helps['containerapp env'] = """ type: group - short-summary: Commands to manage Containerapps environments. + short-summary: Commands to manage Containerapp environments. """ helps['containerapp env create'] = """ diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 7a94c3cb3e8..3539787f326 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -21,13 +21,10 @@ def load_command_table(self, _): with self.command_group('containerapp env') as g: - g.custom_command('show', 'show_kube_environment') - # g.custom_command('show', 'show_managed_environment') - g.custom_command('list', 'list_kube_environments') - # g.custom_command('list', 'list_managed_environments') + g.custom_command('show', 'show_managed_environment') + g.custom_command('list', 'list_managed_environments') g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_kube_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) - # g.command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4420b7f6516..43b2eec5542 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -94,7 +94,7 @@ def create_kube_environment(cmd, cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=kube_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)) + logger.warning('Containerapp environment creation in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) return r except Exception as e: @@ -113,7 +113,7 @@ def delete_kube_environment(cmd, name, resource_group_name, no_wait=False): try: r = KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) if not r and not no_wait: - logger.warning('Containerapp successfully deleted') + logger.warning('Containerapp environment successfully deleted') return r except CLIError as e: handle_raw_exception(e) @@ -155,7 +155,7 @@ def list_managed_environments(cmd, resource_group_name=None): 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) + managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return [e for e in managed_envs if "properties" in e and "environmentType" in e["properties"] and @@ -163,3 +163,13 @@ def list_managed_environments(cmd, resource_group_name=None): e["properties"]["environmentType"].lower() == "managed"] except CLIError as e: handle_raw_exception(e) + + +def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): + try: + r = ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + if not r and not no_wait: + logger.warning('Containerapp environment successfully deleted') + return r + except CLIError as e: + handle_raw_exception(e) From 232512f2cf2596940fbc59d47196334f1ceb8b17 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Sat, 5 Feb 2022 18:29:07 -0800 Subject: [PATCH 093/177] Create containerapp env using Microsoft.App RP --- .../azext_containerapp/_clients.py | 60 +++++++++++- .../azext_containerapp/_models.py | 20 ++++ .../azext_containerapp/_params.py | 14 +-- src/containerapp/azext_containerapp/_utils.py | 2 +- .../azext_containerapp/commands.py | 6 +- src/containerapp/azext_containerapp/custom.py | 94 +++++++++++++++---- 6 files changed, 166 insertions(+), 30 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index dd7961f401c..f245e6863e4 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -202,10 +202,68 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) class ManagedEnvironmentClient(): + @classmethod + def create(cls, cmd, resource_group_name, name, kube_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(kube_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, kube_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(kube_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 = API_VERSION + 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( diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 71242502bdc..d3c503d559a 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -34,6 +34,26 @@ "extendedLocation": None } +ManagedEnvironment = { + "id": None, # readonly + "name": None, # readonly + "kind": None, + "location": None, + "tags": None, + "properties": { + "daprAIInstrumentationKey": None, + "vnetConfiguration": { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIp": None + }, + "internalLoadBalancer": None, + "appLogsConfiguration": None + } +} + AppLogsConfiguration = { "destination": None, "logAnalyticsConfiguration": None diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 2cc985f43ce..40fd153c5e0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -29,13 +29,13 @@ def load_arguments(self, _): c.argument('logs_destination', options_list=['--logs-dest']) c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') - c.argument('instrumentation_key', options_list=['--instrumentation-key']) - c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') - c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') - c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') + # c.argument('instrumentation_key', options_list=['--instrumentation-key']) + # c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + # c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') + # c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') c.argument('tags', arg_type=tags_type) with self.argument_context('containerapp env update') as c: diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 6b0a92b4914..f62cd64cb45 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -46,7 +46,7 @@ def _ensure_location_allowed(cmd, location, resource_provider): 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=='containerApps'].locations'`".format( + raise ValidationError("Location '{}' is not currently supported. To get list of supported locations, run `az provider show -n {} --query \"resourceTypes[?resourceType=='containerApps'].locations\"`".format( location, resource_provider)) except ValidationError as ex: raise ex diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 3539787f326..7696326525e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -23,8 +23,6 @@ def load_command_table(self, _): with self.command_group('containerapp env') as g: g.custom_command('show', 'show_managed_environment') g.custom_command('list', 'list_managed_environments') - g.custom_command('create', 'create_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('update', 'update_kube_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) - # g.custom_command('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('update', 'update_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()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 43b2eec5542..8a203c55155 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -11,7 +11,7 @@ from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient -from ._models import KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._models import ManagedEnvironment, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed logger = get_logger(__name__) @@ -39,38 +39,38 @@ def create_kube_environment(cmd, no_wait=False): location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) - + _validate_subscription_registered(cmd, "Microsoft.Web") _ensure_location_allowed(cmd, location, "Microsoft.Web") - containerapps_config_def = ContainerAppsConfiguration + containerapp_env_config_def = ContainerAppsConfiguration if instrumentation_key is not None: - containerapps_config_def["daprAIInstrumentationKey"] = instrumentation_key + containerapp_env_config_def["daprAIInstrumentationKey"] = instrumentation_key if controlplane_subnet_resource_id is not None: if not app_subnet_resource_id: raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') - containerapps_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id + containerapp_env_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id if app_subnet_resource_id is not None: if not controlplane_subnet_resource_id: raise ValidationError('Controlplane subnet resource ID needs to be supplied with app subnet resource ID.') - containerapps_config_def["appSubnetResourceId"] = app_subnet_resource_id + containerapp_env_config_def["appSubnetResourceId"] = app_subnet_resource_id if docker_bridge_cidr is not None: - containerapps_config_def["dockerBridgeCidr"] = docker_bridge_cidr + containerapp_env_config_def["dockerBridgeCidr"] = docker_bridge_cidr if platform_reserved_cidr is not None: - containerapps_config_def["platformReservedCidr"] = platform_reserved_cidr + containerapp_env_config_def["platformReservedCidr"] = platform_reserved_cidr if platform_reserved_dns_ip is not None: - containerapps_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip + containerapp_env_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip if internal_only: if not controlplane_subnet_resource_id or not app_subnet_resource_id: raise ValidationError('Controlplane subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') - containerapps_config_def["internalOnly"] = True + containerapp_env_config_def["internalOnly"] = True log_analytics_config_def = LogAnalyticsConfiguration log_analytics_config_def["customerId"] = logs_customer_id @@ -86,7 +86,7 @@ def create_kube_environment(cmd, kube_def["properties"]["environmentType"] = "managed" kube_def["properties"]["type"] = "managed" kube_def["properties"]["appLogsConfiguration"] = app_logs_config_def - kube_def["properties"]["containerAppsConfiguration"] = containerapps_config_def + kube_def["properties"]["containerAppsConfiguration"] = containerapp_env_config_def kube_def["tags"] = tags try: @@ -101,12 +101,69 @@ def create_kube_environment(cmd, handle_raw_exception(e) -def update_kube_environment(cmd, +def create_managed_environment(cmd, + name, + resource_group_name, + logs_customer_id, + logs_key, + logs_destination="log-analytics", + location=None, + 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") + + log_analytics_config_def = LogAnalyticsConfiguration + log_analytics_config_def["customerId"] = logs_customer_id + log_analytics_config_def["sharedKey"] = logs_key + + app_logs_config_def = AppLogsConfiguration + app_logs_config_def["destination"] = logs_destination + app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def + + managed_env_def = ManagedEnvironment + 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 + + try: + r = ManagedEnvironmentClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, kube_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 show -n {} -g {}`'.format(name, resource_group_name)) + + 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 CLIError('Containerapp env update is not yet implemented') + raise CLIError('Containerapp env update is not yet supported.') + + _validate_subscription_registered(cmd, "Microsoft.App") + + managed_env_def = ManagedEnvironment + managed_env_def["tags"] = tags + + try: + r = ManagedEnvironmentClient.update( + cmd=cmd, resource_group_name=resource_group_name, name=name, kube_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 update 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 delete_kube_environment(cmd, name, resource_group_name, no_wait=False): @@ -143,6 +200,8 @@ def list_kube_environments(cmd, resource_group_name=None): 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: @@ -150,6 +209,8 @@ def show_managed_environment(cmd, name, resource_group_name): def list_managed_environments(cmd, resource_group_name=None): + _validate_subscription_registered(cmd, "Microsoft.App") + try: managed_envs = [] if resource_group_name is None: @@ -157,15 +218,14 @@ def list_managed_environments(cmd, resource_group_name=None): else: managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) - return [e for e in managed_envs if "properties" in e and - "environmentType" in e["properties"] and - e["properties"]["environmentType"] and - e["properties"]["environmentType"].lower() == "managed"] + 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: r = ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) if not r and not no_wait: From baf19b4c4e7d8d617551533dede73bef6220d1b2 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Feb 2022 07:06:53 -0800 Subject: [PATCH 094/177] Add optional containerapp env create arguments --- .../azext_containerapp/_models.py | 8 ++++ .../azext_containerapp/_params.py | 18 ++++---- src/containerapp/azext_containerapp/custom.py | 42 ++++++++++++++++++- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index d3c503d559a..b9abfdd0ac5 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -63,3 +63,11 @@ "customerId": None, "sharedKey": None } + +VnetConfiguration = { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, + "dockerBridgeCidr": None, + "platformReservedCidr": None, + "platformReservedDnsIp": None +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 40fd153c5e0..2c659029454 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -29,15 +29,19 @@ def load_arguments(self, _): c.argument('logs_destination', options_list=['--logs-dest']) c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') - # c.argument('instrumentation_key', options_list=['--instrumentation-key']) - # c.argument('controlplane_subnet_resource_id', options_list=['--controlplane-subnet-resource-id'], help='Resource ID of a subnet for control plane infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') - # c.argument('app_subnet_resource_id', 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 controlPlaneSubnetResourceId.') - # c.argument('docker_bridge_cidr', 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', 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', 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', 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, must provide ControlPlaneSubnetResourceId and AppSubnetResourceId if enabling this property') c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env', arg_group='Dapr') as c: + c.argument('instrumentation_key', options_list=['--instrumentation-key'], help='Azure Monitor 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', options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + c.argument('app_subnet_resource_id', 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', 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', 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', 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', 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 kubernetes environment.') c.argument('tags', arg_type=tags_type) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 8a203c55155..d2e839fd352 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from platform import platform from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait @@ -11,7 +12,7 @@ from ._client_factory import handle_raw_exception from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient -from ._models import ManagedEnvironment, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._models import ManagedEnvironment, VnetConfiguration, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed logger = get_logger(__name__) @@ -108,6 +109,13 @@ def create_managed_environment(cmd, logs_key, logs_destination="log-analytics", location=None, + instrumentation_key=None, + infrastructure_subnet_resource_id=None, + app_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): @@ -130,6 +138,38 @@ def create_managed_environment(cmd, 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 app_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: + vnet_config_def = VnetConfiguration + + if infrastructure_subnet_resource_id is not None: + if not app_subnet_resource_id: + raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') + vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id + + if app_subnet_resource_id is not None: + if not infrastructure_subnet_resource_id: + raise ValidationError('Infrastructure subnet resource ID needs to be supplied with app subnet resource ID.') + vnet_config_def["runtimeSubnetId"] = app_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["platformReservedCidr"] = platform_reserved_dns_ip + + managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def + + if internal_only: + if not infrastructure_subnet_resource_id or not app_subnet_resource_id: + raise ValidationError('Infrastructure subnet resource ID and App subnet resource ID need 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, kube_environment_envelope=managed_env_def, no_wait=no_wait) From 76f62ba0ff8cd75efa78aa89333fffa532c726ee Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Feb 2022 07:14:07 -0800 Subject: [PATCH 095/177] Remove old kube environment code, naming fixes --- .../azext_containerapp/_clients.py | 137 +----------------- .../azext_containerapp/_models.py | 48 +----- .../azext_containerapp/_params.py | 6 +- src/containerapp/azext_containerapp/custom.py | 123 +--------------- 4 files changed, 16 insertions(+), 298 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index f245e6863e4..5785ca0518d 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -68,142 +68,9 @@ def poll(cmd, request_url, poll_if_status): raise e -class KubeEnvironmentClient(): - @classmethod - def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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(kube_environment_envelope)) - - if no_wait: - return r.json() - elif r.status_code == 201: - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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 = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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.Web/kubeEnvironments/{}?api-version={}" - request_url = url_fmt.format( - management_hostname.strip('/'), - sub_id, - resource_group_name, - name, - api_version) - poll(cmd, request_url, "scheduledfordelete") - return - - @classmethod - def show(cls, cmd, resource_group_name, name): - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments/{}?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): - kube_list = [] - - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - request_url = "{}/subscriptions/{}/providers/Microsoft.Web/kubeEnvironments?api-version={}".format( - management_hostname.strip('/'), - sub_id, - api_version) - - r = send_raw_request(cmd.cli_ctx, "GET", request_url) - j = r.json() - for kube in j["value"]: - formatted = formatter(kube) - kube_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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) - - return kube_list - - @classmethod - def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): - kube_list = [] - - management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = API_VERSION - sub_id = get_subscription_id(cmd.cli_ctx) - url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/kubeEnvironments?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 kube in j["value"]: - formatted = formatter(kube) - kube_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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) - - return kube_list - - class ManagedEnvironmentClient(): @classmethod - def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): + 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) @@ -215,7 +82,7 @@ def create(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wa name, api_version) - r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(kube_environment_envelope)) + r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(managed_environment_envelope)) if no_wait: return r.json() diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index b9abfdd0ac5..c95a9dfda0e 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -4,34 +4,12 @@ # -------------------------------------------------------------------------------------------- -ContainerAppsConfiguration = { - "daprAIInstrumentationKey": None, - "appSubnetResourceId": None, +VnetConfiguration = { + "infrastructureSubnetId": None, + "runtimeSubnetId": None, "dockerBridgeCidr": None, "platformReservedCidr": None, - "platformReservedDnsIP": None, - "internalOnly": False -} - -KubeEnvironment = { - "id": None, # readonly - "name": None, # readonly - "kind": None, - "location": None, - "tags": None, - "properties": { - "type": None, - "environmentType": None, - "containerAppsConfiguration": None, - "provisioningState": None, # readonly - "deploymentErrors": None, # readonly - "defaultDomain": None, # readonly - "staticIp": None, - "arcConfiguration": None, - "appLogsConfiguration": None, - "aksResourceId": None - }, - "extendedLocation": None + "platformReservedDnsIP": None } ManagedEnvironment = { @@ -42,14 +20,8 @@ "tags": None, "properties": { "daprAIInstrumentationKey": None, - "vnetConfiguration": { - "infrastructureSubnetId": None, - "runtimeSubnetId": None, - "dockerBridgeCidr": None, - "platformReservedCidr": None, - "platformReservedDnsIp": None - }, - "internalLoadBalancer": None, + "vnetConfiguration": VnetConfiguration, + "internalLoadBalancerEnabled": None, "appLogsConfiguration": None } } @@ -63,11 +35,3 @@ "customerId": None, "sharedKey": None } - -VnetConfiguration = { - "infrastructureSubnetId": None, - "runtimeSubnetId": None, - "dockerBridgeCidr": None, - "platformReservedCidr": None, - "platformReservedDnsIp": None -} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 2c659029454..1f38065f2ae 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -43,11 +43,11 @@ def load_arguments(self, _): c.argument('internal_only', 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 kubernetes environment.') + c.argument('name', name_type, help='Name of the managed 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 Kubernetes Environment.') + c.argument('name', name_type, help='Name of the managed Environment.') with self.argument_context('containerapp env show') as c: - c.argument('name', name_type, help='Name of the Kubernetes Environment.') + c.argument('name', name_type, help='Name of the managed Environment.') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d2e839fd352..a7887c1bdc2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -11,8 +11,8 @@ from knack.log import get_logger from ._client_factory import handle_raw_exception -from ._clients import KubeEnvironmentClient, ManagedEnvironmentClient -from ._models import ManagedEnvironment, VnetConfiguration, KubeEnvironment, ContainerAppsConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration +from ._clients import ManagedEnvironmentClient +from ._models import ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed logger = get_logger(__name__) @@ -22,86 +22,6 @@ def create_containerapp(cmd, resource_group_name, name, location=None, tags=None raise CLIError('TODO: Implement `containerapp create`') -def create_kube_environment(cmd, - name, - resource_group_name, - logs_customer_id, - logs_key, - logs_destination="log-analytics", - location=None, - instrumentation_key=None, - controlplane_subnet_resource_id=None, - app_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.Web") - _ensure_location_allowed(cmd, location, "Microsoft.Web") - - containerapp_env_config_def = ContainerAppsConfiguration - - if instrumentation_key is not None: - containerapp_env_config_def["daprAIInstrumentationKey"] = instrumentation_key - - if controlplane_subnet_resource_id is not None: - if not app_subnet_resource_id: - raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') - containerapp_env_config_def["controlPlaneSubnetResourceId"] = controlplane_subnet_resource_id - - if app_subnet_resource_id is not None: - if not controlplane_subnet_resource_id: - raise ValidationError('Controlplane subnet resource ID needs to be supplied with app subnet resource ID.') - containerapp_env_config_def["appSubnetResourceId"] = app_subnet_resource_id - - if docker_bridge_cidr is not None: - containerapp_env_config_def["dockerBridgeCidr"] = docker_bridge_cidr - - if platform_reserved_cidr is not None: - containerapp_env_config_def["platformReservedCidr"] = platform_reserved_cidr - - if platform_reserved_dns_ip is not None: - containerapp_env_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip - - if internal_only: - if not controlplane_subnet_resource_id or not app_subnet_resource_id: - raise ValidationError('Controlplane subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') - containerapp_env_config_def["internalOnly"] = True - - log_analytics_config_def = LogAnalyticsConfiguration - log_analytics_config_def["customerId"] = logs_customer_id - log_analytics_config_def["sharedKey"] = logs_key - - app_logs_config_def = AppLogsConfiguration - app_logs_config_def["destination"] = logs_destination - app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def - - kube_def = KubeEnvironment - kube_def["location"] = location - kube_def["properties"]["internalLoadBalancerEnabled"] = False - kube_def["properties"]["environmentType"] = "managed" - kube_def["properties"]["type"] = "managed" - kube_def["properties"]["appLogsConfiguration"] = app_logs_config_def - kube_def["properties"]["containerAppsConfiguration"] = containerapp_env_config_def - kube_def["tags"] = tags - - try: - r = KubeEnvironmentClient.create( - cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=kube_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 show -n {} -g {}`'.format(name, resource_group_name)) - - return r - except Exception as e: - handle_raw_exception(e) - - def create_managed_environment(cmd, name, resource_group_name, @@ -161,7 +81,7 @@ def create_managed_environment(cmd, vnet_config_def["platformReservedCidr"] = platform_reserved_cidr if platform_reserved_dns_ip is not None: - vnet_config_def["platformReservedCidr"] = platform_reserved_dns_ip + vnet_config_def["platformReservedDnsIP"] = platform_reserved_dns_ip managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def @@ -172,7 +92,7 @@ def create_managed_environment(cmd, try: r = ManagedEnvironmentClient.create( - cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=managed_env_def, no_wait=no_wait) + 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 show -n {} -g {}`'.format(name, resource_group_name)) @@ -196,7 +116,7 @@ def update_managed_environment(cmd, try: r = ManagedEnvironmentClient.update( - cmd=cmd, resource_group_name=resource_group_name, name=name, kube_environment_envelope=managed_env_def, no_wait=no_wait) + 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 update in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) @@ -206,39 +126,6 @@ def update_managed_environment(cmd, handle_raw_exception(e) -def delete_kube_environment(cmd, name, resource_group_name, no_wait=False): - try: - r = KubeEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - if not r and not no_wait: - logger.warning('Containerapp environment successfully deleted') - return r - except CLIError as e: - handle_raw_exception(e) - - -def show_kube_environment(cmd, name, resource_group_name): - try: - return KubeEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: - handle_raw_exception(e) - - -def list_kube_environments(cmd, resource_group_name=None): - try: - kube_envs = [] - if resource_group_name is None: - kube_envs = KubeEnvironmentClient.list_by_subscription(cmd=cmd) - else: - kube_envs = KubeEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) - - return [e for e in kube_envs if "properties" in e and - "environmentType" in e["properties"] and - e["properties"]["environmentType"] and - e["properties"]["environmentType"].lower() == "managed"] - except CLIError as e: - handle_raw_exception(e) - - def show_managed_environment(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") From 12524a7c58d894797263ffad642aa77d7875d4c4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 10 Feb 2022 21:43:09 -0800 Subject: [PATCH 096/177] Containerapp create almost done --- .../azext_containerapp/_models.py | 153 +++++++++++++++++- .../azext_containerapp/_params.py | 43 +++++ src/containerapp/azext_containerapp/_utils.py | 99 ++++++++++++ .../azext_containerapp/_validators.py | 62 +++++++ src/containerapp/azext_containerapp/custom.py | 150 ++++++++++++++++- 5 files changed, 498 insertions(+), 9 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index c95a9dfda0e..379e69b0029 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -13,14 +13,11 @@ } ManagedEnvironment = { - "id": None, # readonly - "name": None, # readonly - "kind": None, "location": None, "tags": None, "properties": { "daprAIInstrumentationKey": None, - "vnetConfiguration": VnetConfiguration, + "vnetConfiguration": None, # VnetConfiguration "internalLoadBalancerEnabled": None, "appLogsConfiguration": None } @@ -35,3 +32,151 @@ "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] + "allowInsecure": None +} + +RegistryCredentials = { + "server": None, + "username": None, + "passwordSecretRef": None +} + +Template = { + "revisionSuffix": None, + "containers": None, # [Container] + "scale": Scale, + "dapr": Dapr, + "volumes": None # [Volume] +} + +Configuration = { + "secrets": None, # [Secret] + "activeRevisionsMode": None, # 'multiple' or 'single' + "ingress": None, # Ingress + "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 +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 1f38065f2ae..618d1b4ba13 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -11,6 +11,8 @@ 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) def load_arguments(self, _): @@ -22,6 +24,47 @@ def load_arguments(self, _): 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 create') as c: + c.argument('tags', arg_type=tags_type) + c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment', '-e'], help="Name or resource ID of the containerapp's environment.") + c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') + + # Container + with self.argument_context('containerapp create', arg_group='Container') as c: + c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") + c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") + c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") + c.argument('env_vars', type=str, options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Comma-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") + c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") + c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") + + # Scale + with self.argument_context('containerapp create', arg_group='Scale') as c: + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + + # Configuration + with self.argument_context('containerapp create', arg_group='Configuration') as c: + c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") + c.argument('secrets', type=str, options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Comma-separated values in 'key=value' format.") + + # Ingress + with self.argument_context('containerapp create', arg_group='Ingress') as c: + c.argument('ingress', options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") + c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") + + # Dapr + with self.argument_context('containerapp create', arg_group='Dapr') as c: + c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) + c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") + c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") + c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") + with self.argument_context('containerapp env') as c: c.argument('name', name_type) c.argument('resource_group_name', arg_type=resource_group_name_type) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index f62cd64cb45..45b552676e7 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,9 +5,13 @@ from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id +from knack.log import get_logger +from urllib.parse import urlparse from ._client_factory import providers_client_factory, cf_resource_groups +logger = get_logger(__name__) + def _get_location_from_resource_group(cli_ctx, resource_group_name): client = cf_resource_groups(cli_ctx) @@ -52,3 +56,98 @@ def _ensure_location_allowed(cmd, location, resource_provider): raise ex except Exception: pass + + +def parse_env_var_flags(env_string, is_update_containerapp=False): + env_pair_strings = env_string.split(',') + env_pairs = {} + + for pair in env_pair_strings: + key_val = pair.split('=') + if len(key_val) is not 2: + if is_update_containerapp: + raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") + 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) is 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_string): + secret_pair_strings = secret_string.split(',') + secret_pairs = {} + + for pair in secret_pair_strings: + key_val = pair.split('=', 1) + if len(key_val) is not 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 store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass): + 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(): + raise ValidationError('Found secret with name \"{}\" but value does not equal the supplied registry password.'.format(registry_secret_name)) + else: + return registry_secret_name + + logger.warning('Adding registry password as a secret with name \"{}\"'.format(registry_secret_name)) + 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] diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 821630f5f34..0843ab2a374 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -3,6 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from unicodedata import name +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) + def example_name_or_id_validator(cmd, namespace): # Example of a storage account name or ID validator. @@ -18,3 +21,62 @@ def example_name_or_id_validator(cmd, namespace): type='storageAccounts', name=namespace.storage_account ) + +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: + if namespace.cpu is None: + raise RequiredArgumentMissingError('Usage error: --cpu required if specifying --memory') + + 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 is not None and namespace.memory is None: + raise RequiredArgumentMissingError('Usage error: --memory required if specifying --cpu') + +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 namespace.registry_server: + if not namespace.registry_user or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + +def validate_registry_user(namespace): + if namespace.registry_user: + if not namespace.registry_server or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + +def validate_registry_pass(namespace): + if namespace.registry_pass: + if not namespace.registry_user or not namespace.registry_server: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + +def validate_target_port(namespace): + if namespace.target_port: + if not namespace.ingress: + raise ValidationError("Usage error: must specify --ingress with --target-port") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a7887c1bdc2..989f4b91b71 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,22 +4,162 @@ # -------------------------------------------------------------------------------------------- from platform import platform -from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger +from msrestazure.tools import parse_resource_id from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient -from ._models import ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration -from ._utils import _validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed +from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, + Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) +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_list_of_strings, parse_env_var_flags) logger = get_logger(__name__) -def create_containerapp(cmd, resource_group_name, name, location=None, tags=None): - raise CLIError('TODO: Implement `containerapp create`') +def create_containerapp(cmd, + name, + resource_group_name, + yaml=None, + image_name=None, + managed_env=None, + min_replicas=None, + max_replicas=None, + target_port=None, + transport="auto", + ingress=None, + revisions_mode=None, + 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, + # dapr_components=None, + location=None, + startup_command=None, + args=None, + 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") + + if yaml: + # TODO: Implement yaml + return + + if image_name is None: + raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') + + 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 = location or managed_env_info.location + + 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 = Ingress + ingress_def["external"] = external_ingress + ingress_def["target_port"] = 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: + credentials_def = RegistryCredentials + credentials_def["server"] = registry_server + credentials_def["username"] = registry_user + + if secrets_def is None: + secrets_def = [] + credentials_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + + config_def = Configuration + config_def["secrets"] = secrets_def + config_def["activeRevisionsMode"] = revisions_mode + config_def["ingress"] = ingress_def + config_def["registries"] = registries_def + + scale_def = None + if min_replicas is not None or max_replicas is not None: + scale_def = Scale + 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 = ContainerResources + resources_def["cpu"] = cpu + resources_def["memory"] = memory + + container_def = Container + container_def["name"] = name + container_def["image"] = image_name + if env_vars is not None: + container_def["env"] = parse_env_var_flags(env_vars) + if startup_command is not None: + container_def["command"] = parse_list_of_strings(startup_command) + if args is not None: + container_def["args"] = parse_list_of_strings(args) + if resources_def is not None: + container_def["resources"] = resources_def + + dapr_def = None + if dapr_enabled: + dapr_def = Dapr + dapr_def["daprEnabled"] = True + dapr_def["appId"] = dapr_app_id + dapr_def["appPort"] = dapr_app_port + dapr_def["appProtocol"] = dapr_app_protocol + + template_def = Template + template_def["container"] = [container_def] + template_def["scale"] = scale_def + template_def["dapr"] = dapr_def + + containerapp_def = ContainerApp + container_def["location"] = location + containerapp_def["properties"]["managedEnvironmentId"] = managed_env + containerapp_def["properties"]["configuration"] = config_def + containerapp_def["properties"]["template"] = template_def + container_def["tags"] = tags + + # TODO: Call create with nowait poller def create_managed_environment(cmd, From 99a3d38a4b5209760f6e4c0fc5f0f21d4d2e2f76 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 08:08:58 -0800 Subject: [PATCH 097/177] Done containerapp create, except for --yaml. Need to test --- .../azext_containerapp/_clients.py | 31 +++++++++++++++++++ src/containerapp/azext_containerapp/custom.py | 23 +++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5785ca0518d..57411e1a438 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -68,6 +68,37 @@ def poll(cmd, request_url, poll_if_status): raise e +class ContainerAppClient(): + @classmethod + def create(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, "waiting") + + return r.json() + + class ManagedEnvironmentClient(): @classmethod def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no_wait=False): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 989f4b91b71..de2a48aa5a4 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -12,7 +12,7 @@ from msrestazure.tools import parse_resource_id from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) from ._utils import (_validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed, @@ -57,7 +57,7 @@ def create_containerapp(cmd, if yaml: # TODO: Implement yaml - return + raise CLIError("--yaml is not yet implemented") if image_name is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') @@ -153,13 +153,22 @@ def create_containerapp(cmd, template_def["dapr"] = dapr_def containerapp_def = ContainerApp - container_def["location"] = location + containerapp_def["location"] = location containerapp_def["properties"]["managedEnvironmentId"] = managed_env containerapp_def["properties"]["configuration"] = config_def containerapp_def["properties"]["template"] = template_def - container_def["tags"] = tags + containerapp_def["tags"] = tags - # TODO: Call create with nowait poller + try: + r = ContainerAppClient.create( + cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_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)) + + return r + except Exception as e: + handle_raw_exception(e) def create_managed_environment(cmd, @@ -235,7 +244,7 @@ def create_managed_environment(cmd, 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 show -n {} -g {}`'.format(name, resource_group_name)) + logger.warning('Containerapp environment creation in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) return r except Exception as e: @@ -259,7 +268,7 @@ def update_managed_environment(cmd, 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 update in progress. Please monitor the creation using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + logger.warning('Containerapp environment update in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) return r except Exception as e: From 1c74c2eee17e698309f94c0cd30ba0664dc604b5 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 10:52:37 -0800 Subject: [PATCH 098/177] Containerapp show, list --- .../azext_containerapp/_clients.py | 107 +++++++++++++++++- src/containerapp/azext_containerapp/_help.py | 21 ++++ .../azext_containerapp/_models.py | 2 +- .../azext_containerapp/commands.py | 4 +- src/containerapp/azext_containerapp/custom.py | 30 ++++- 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 57411e1a438..82cc2c6be23 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -94,9 +94,112 @@ def create(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= resource_group_name, name, api_version) - return poll(cmd, request_url, "waiting") + 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 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 r.json() + return app_list class ManagedEnvironmentClient(): diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 18ce06e05be..655e528985e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -17,6 +17,27 @@ short-summary: Create a Containerapp. """ +helps['containerapp show'] = """ + type: command + short-summary: Show details of a Containerapp. + examples: + - name: Show the details of a Containerapp. + text: | + az containerapp show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp list'] = """ + type: command + short-summary: List Containerapps. + examples: + - name: List Containerapps by subscription. + text: | + az containerapp list + - name: List Containerapps by resource group. + text: | + az containerapp list -g MyResourceGroup +""" + # Environment Commands helps['containerapp env'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 379e69b0029..f0d068b1bbc 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -137,7 +137,7 @@ "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight "customDomains": None, # [CustomDomain] - "allowInsecure": None + # "allowInsecure": None } RegistryCredentials = { diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 7696326525e..d3c3853d2f6 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -17,7 +17,9 @@ def load_command_table(self, _): with self.command_group('containerapp') as g: - g.custom_command('create', 'create_containerapp') + g.custom_command('show', 'show_containerapp') + g.custom_command('list', 'list_containerapp') + g.custom_command('create', 'create_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index de2a48aa5a4..ff347f1226a 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -92,7 +92,7 @@ def create_containerapp(cmd, if target_port is not None and ingress is not None: ingress_def = Ingress ingress_def["external"] = external_ingress - ingress_def["target_port"] = target_port + ingress_def["targetPort"] = target_port ingress_def["transport"] = transport secrets_def = None @@ -148,7 +148,7 @@ def create_containerapp(cmd, dapr_def["appProtocol"] = dapr_app_protocol template_def = Template - template_def["container"] = [container_def] + template_def["containers"] = [container_def] template_def["scale"] = scale_def template_def["dapr"] = dapr_def @@ -161,7 +161,7 @@ def create_containerapp(cmd, try: r = ContainerAppClient.create( - cmd=cmd, resource_group_name=resource_group_name, name=name, managed_environment_envelope=containerapp_def, no_wait=no_wait) + 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)) @@ -171,6 +171,30 @@ def create_containerapp(cmd, 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 create_managed_environment(cmd, name, resource_group_name, From 3c0c5011c15b6e96779435f708d737e8661c9226 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 11:44:42 -0800 Subject: [PATCH 099/177] Fix helptext --- src/containerapp/azext_containerapp/_help.py | 54 +++++++++++++++++++ .../azext_containerapp/_params.py | 20 +++---- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 655e528985e..200b45cc1e6 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -15,6 +15,60 @@ helps['containerapp create'] = """ type: command short-summary: Create a Containerapp. + examples: + - name: Create a Containerapp + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with secrets and environment variables + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --secrets mysecret=escapefromtarkov,anothersecret=isadifficultgame \\ + --environment-variables myenvvar=foo,anotherenvvar=bar \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp that only accepts internal traffic + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --ingress internal \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp using an image from a private registry + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --secrets mypassword=verysecurepassword \\ + --registry-login-server MyRegistryServerAddress \\ + --registry-username MyUser \\ + --registry-password mypassword \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with a specified startup command and arguments + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --command "/bin/sh" \\ + --args "-c", "while true; do echo hello; sleep 10;done" \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with a minimum resource and replica requirements + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --cpu 0.5 --memory 1.0Gi \\ + --min-replicas 4 --max-replicas 8 \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp with dapr components + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage -e MyContainerappEnv \\ + --enable-dapr --dapr-app-port myAppPort \\ + --dapr-app-id myAppID \\ + --dapr-components PathToDaprComponentsFile \\ + --query properties.configuration.ingress.fqdn + - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + -- yaml "C:/path/to/yaml/file.yml" """ helps['containerapp show'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 618d1b4ba13..78ae210d3c4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -30,7 +30,7 @@ def load_arguments(self, _): c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container - with self.argument_context('containerapp create', arg_group='Container') as c: + with self.argument_context('containerapp create', arg_group='Container (Creates new revision)') as c: c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") @@ -39,10 +39,18 @@ def load_arguments(self, _): c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") # Scale - with self.argument_context('containerapp create', arg_group='Scale') as c: + with self.argument_context('containerapp create', arg_group='Scale (Creates new revision)') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + # Dapr + with self.argument_context('containerapp create', arg_group='Dapr (Creates new revision)') as c: + c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) + c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") + c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") + c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") + # Configuration with self.argument_context('containerapp create', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") @@ -57,14 +65,6 @@ def load_arguments(self, _): c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") - # Dapr - with self.argument_context('containerapp create', arg_group='Dapr') as c: - c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) - c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") - c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") - c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") - c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") - with self.argument_context('containerapp env') as c: c.argument('name', name_type) c.argument('resource_group_name', arg_type=resource_group_name_type) From 958facfefadef6f5700dde5451983001068fc56c Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 11 Feb 2022 13:03:48 -0800 Subject: [PATCH 100/177] Containerapp delete --- .../azext_containerapp/_clients.py | 28 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 8 ++++++ .../azext_containerapp/commands.py | 22 +++++++++++++-- src/containerapp/azext_containerapp/custom.py | 12 ++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 82cc2c6be23..f08399aaf06 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -127,6 +127,34 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= 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) + poll(cmd, request_url, "cancelled") + return + @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 200b45cc1e6..0c81811d0e3 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -71,6 +71,14 @@ -- yaml "C:/path/to/yaml/file.yml" """ +helps['containerapp delete'] = """ + type: command + short-summary: Delete a Containerapp. + examples: + - name: Delete a Containerapp. + text: az containerapp delete -g MyResourceGroup -n MyContainerapp +""" + helps['containerapp show'] = """ type: command short-summary: Show details of a Containerapp. diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index d3c3853d2f6..995557294ca 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -5,9 +5,26 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType +from msrestazure.tools import is_valid_resource_id, parse_resource_id from azext_containerapp._client_factory import cf_containerapp, 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 Exception: + result['fqdn'] = None + + return result + + +def transform_containerapp_list_output(apps): + return [transform_containerapp_output(a) for a in apps] + + def load_command_table(self, _): # TODO: Add command type here @@ -17,9 +34,10 @@ def load_command_table(self, _): with self.command_group('containerapp') as g: - g.custom_command('show', 'show_containerapp') - g.custom_command('list', 'list_containerapp') + g.custom_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()) + g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ff347f1226a..2c4751aa124 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -195,6 +195,18 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) +def delete_containerapp(cmd, name, resource_group_name, no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + try: + r = ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + if not r and not no_wait: + logger.warning('Containerapp successfully deleted') + return r + except CLIError as e: + handle_raw_exception(e) + + def create_managed_environment(cmd, name, resource_group_name, From 2a230f0ae153cfb696e22ef283ed7675a4902d9d Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Feb 2022 09:44:34 -0800 Subject: [PATCH 101/177] Containerapp update. Needs secrets api to be implemented, and testing --- .../azext_containerapp/_client_factory.py | 3 + .../azext_containerapp/_clients.py | 2 +- src/containerapp/azext_containerapp/_help.py | 46 ++++- .../azext_containerapp/_params.py | 19 +- src/containerapp/azext_containerapp/_utils.py | 10 +- .../azext_containerapp/_validators.py | 40 ++-- .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 188 +++++++++++++++++- 8 files changed, 263 insertions(+), 46 deletions(-) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 4c8eeeb7f86..3ee674ace77 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -58,6 +58,9 @@ def handle_raw_exception(e): elif "Message" in jsonError: message = jsonError["Message"] raise CLIError(message) + elif "message" in jsonError: + message = jsonError["message"] + raise CLIError(message) raise e diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index f08399aaf06..9b2f0f89750 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -70,7 +70,7 @@ def poll(cmd, request_url, poll_if_status): class ContainerAppClient(): @classmethod - def create(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): + 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) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 0c81811d0e3..27b2c0c98f3 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -57,20 +57,50 @@ --cpu 0.5 --memory 1.0Gi \\ --min-replicas 4 --max-replicas 8 \\ --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with dapr components - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --enable-dapr --dapr-app-port myAppPort \\ - --dapr-app-id myAppID \\ - --dapr-components PathToDaprComponentsFile \\ - --query properties.configuration.ingress.fqdn - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ -- yaml "C:/path/to/yaml/file.yml" """ +helps['containerapp update'] = """ + type: command + short-summary: Update a Containerapp. + examples: + - name: Update a Containerapp's container image + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --image MyNewContainerImage + - name: Update a Containerapp with secrets and environment variables + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --secrets mysecret=secretfoo,anothersecret=secretbar + --environment-variables myenvvar=foo,anotherenvvar=secretref:mysecretname + - name: Update a Containerapp's ingress setting to internal + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --ingress internal + - name: Update a Containerapp using an image from a private registry + text: | + az containerapp update -n MyContainerapp -g MyResourceGroup \\ + --image MyNewContainerImage \\ + --secrets mypassword=verysecurepassword \\ + --registry-login-server MyRegistryServerAddress \\ + --registry-username MyUser \\ + --registry-password mypassword + - name: Update a Containerapp using a specified startup command and arguments + text: | + az containerapp create -n MyContainerapp -g MyResourceGroup \\ + --image MyContainerImage \\ + --command "/bin/sh" + --args "-c", "while true; do echo hello; sleep 10;done" + - name: Update a Containerapp with a minimum resource and replica requirements + 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 Containerapp. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 78ae210d3c4..c41e729e2d2 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -12,7 +12,7 @@ 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_registry_user, validate_registry_pass, validate_target_port, validate_ingress) def load_arguments(self, _): @@ -24,27 +24,28 @@ def load_arguments(self, _): 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 create') as c: + 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', '-e'], help="Name or resource ID of the containerapp's environment.") c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container - with self.argument_context('containerapp create', arg_group='Container (Creates new revision)') as c: + with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', type=str, options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Comma-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") + c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale - with self.argument_context('containerapp create', arg_group='Scale (Creates new revision)') as c: + with self.argument_context('containerapp', arg_group='Scale (Creates new revision)') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") # Dapr - with self.argument_context('containerapp create', arg_group='Dapr (Creates new revision)') as c: + with self.argument_context('containerapp', arg_group='Dapr (Creates new revision)') as c: c.argument('dapr_enabled', options_list=['--enable-dapr'], default=False, arg_type=get_three_state_flag()) c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") @@ -52,7 +53,7 @@ def load_arguments(self, _): c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") # Configuration - with self.argument_context('containerapp create', arg_group='Configuration') as c: + with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, 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 container image registry server. If stored as a secret, value must start with \'secretref:\' followed by the secret name.") @@ -60,13 +61,13 @@ def load_arguments(self, _): c.argument('secrets', type=str, options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Comma-separated values in 'key=value' format.") # Ingress - with self.argument_context('containerapp create', arg_group='Ingress') as c: - c.argument('ingress', options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") + with self.argument_context('containerapp', arg_group='Ingress') as c: + c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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 env') as c: - c.argument('name', name_type) + c.argument('name', name_type, help='Name of the containerapp 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('logs_destination', options_list=['--logs-dest']) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 45b552676e7..c2565c651b6 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -111,7 +111,7 @@ def parse_secret_flags(secret_string): return secret_var_def -def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass): +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 @@ -135,9 +135,11 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ for secret in secrets_list: if secret['name'].lower() == registry_secret_name.lower(): if secret['value'].lower() != registry_pass.lower(): - raise ValidationError('Found secret with name \"{}\" but value does not equal the supplied registry password.'.format(registry_secret_name)) - else: - return registry_secret_name + 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)) secrets_list.append({ diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 0843ab2a374..1f5913e3fed 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -33,9 +33,6 @@ def validate_memory(namespace): memory = namespace.memory if memory is not None: - if namespace.cpu is None: - raise RequiredArgumentMissingError('Usage error: --cpu required if specifying --memory') - valid = False if memory.endswith("Gi"): @@ -45,8 +42,7 @@ def validate_memory(namespace): raise ValidationError("Usage error: --memory must be a number ending with \"Gi\"") def validate_cpu(namespace): - if namespace.cpu is not None and namespace.memory is None: - raise RequiredArgumentMissingError('Usage error: --memory required if specifying --cpu') + return def validate_managed_env_name_or_id(cmd, namespace): from azure.cli.core.commands.client_factory import get_subscription_id @@ -62,21 +58,31 @@ def validate_managed_env_name_or_id(cmd, namespace): ) def validate_registry_server(namespace): - if namespace.registry_server: - if not namespace.registry_user or not namespace.registry_pass: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if "create" in namespace.command.lower(): + if namespace.registry_server: + if not namespace.registry_user or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") def validate_registry_user(namespace): - if namespace.registry_user: - if not namespace.registry_server or not namespace.registry_pass: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if "create" in namespace.command.lower(): + if namespace.registry_user: + if not namespace.registry_server or not namespace.registry_pass: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") def validate_registry_pass(namespace): - if namespace.registry_pass: - if not namespace.registry_user or not namespace.registry_server: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if "create" in namespace.command.lower(): + if namespace.registry_pass: + if not namespace.registry_user or not namespace.registry_server: + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") def validate_target_port(namespace): - if namespace.target_port: - if not namespace.ingress: - raise ValidationError("Usage error: must specify --ingress with --target-port") + 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/commands.py b/src/containerapp/azext_containerapp/commands.py index 995557294ca..330ac7234cb 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -37,6 +37,7 @@ def load_command_table(self, _): g.custom_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()) + g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2c4751aa124..68db52f3238 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -101,19 +101,19 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: - credentials_def = RegistryCredentials - credentials_def["server"] = registry_server - credentials_def["username"] = registry_user + registries_def = RegistryCredentials + registries_def["server"] = registry_server + registries_def["username"] = registry_user if secrets_def is None: secrets_def = [] - credentials_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) config_def = Configuration - config_def["secrets"] = secrets_def + config_def["secrets"] = None # TODO: Uncomment secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def - config_def["registries"] = registries_def + config_def["registries"] = [registries_def] scale_def = None if min_replicas is not None or max_replicas is not None: @@ -160,7 +160,7 @@ def create_containerapp(cmd, containerapp_def["tags"] = tags try: - r = ContainerAppClient.create( + 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: @@ -171,6 +171,180 @@ def create_containerapp(cmd, handle_raw_exception(e) +def update_containerapp(cmd, + name, + resource_group_name, + yaml=None, + image_name=None, + min_replicas=None, + max_replicas=None, + ingress=None, + target_port=None, + transport=None, + # traffic_weights=None, + revisions_mode=None, + secrets=None, + env_vars=None, + cpu=None, + memory=None, + registry_server=None, + registry_user=None, + registry_pass=None, + dapr_enabled=None, + dapr_app_port=None, + dapr_app_id=None, + dapr_app_protocol=None, + # dapr_components=None, + revision_suffix=None, + startup_command=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if yaml: + # TODO: Implement yaml + raise CLIError("--yaml is not yet implemented") + + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + update_map = {} + update_map['secrets'] = secrets is not None + update_map['ingress'] = ingress or target_port or transport + update_map['registries'] = registry_server or registry_user or registry_pass + update_map['scale'] = min_replicas or max_replicas + update_map['container'] = image_name or env_vars or cpu or memory or startup_command or args + update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol + update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None + + if update_map['container'] and len(containerapp_def['properties']['template']['containers']) > 1: + raise CLIError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") + + if tags: + containerapp_def['tags'] = tags + + if revision_suffix is not None: + containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix + + # Containers + if image_name is not None: + containerapp_def["properties"]["template"]["containers"][0]["image"] = image_name + if env_vars is not None: + containerapp_def["properties"]["template"]["containers"][0]["env"] = parse_env_var_flags(env_vars) + if startup_command is not None: + containerapp_def["properties"]["template"]["containers"][0]["command"] = parse_list_of_strings(startup_command) + if args is not None: + containerapp_def["properties"]["template"]["containers"][0]["args"] = parse_list_of_strings(startup_command) + if cpu is not None or memory is not None: + resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] + if resources: + if cpu is not None: + resources["cpu"] = cpu + if memory is not None: + resources["memory"] = memory + else: + resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] = { + "cpu": cpu, + "memory": memory + } + + # 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 + + # Dapr + if update_map["dapr"]: + if "dapr" not in containerapp_def["properties"]["template"]: + containerapp_def["properties"]["template"]["dapr"] = {} + if dapr_enabled is not None: + containerapp_def["properties"]["template"]["dapr"]["daprEnabled"] = dapr_enabled + if dapr_app_id is not None: + containerapp_def["properties"]["template"]["dapr"]["appId"] = dapr_app_id + if dapr_app_port is not None: + containerapp_def["properties"]["template"]["dapr"]["appPort"] = dapr_app_port + if dapr_app_protocol is not None: + containerapp_def["properties"]["template"]["dapr"]["appProtocol"] = dapr_app_protocol + + # Configuration + if revisions_mode is not None: + containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode + + if update_map["ingress"]: + external_ingress = None + if ingress is not None: + if ingress.lower() == "internal": + external_ingress = False + elif ingress.lower() == "external": + external_ingress = True + containerapp_def["properties"]["configuration"]["external"] = external_ingress + + if target_port is not None: + containerapp_def["properties"]["configuration"]["targetPort"] = target_port + + config = containerapp_def["properties"]["configuration"] + if (config["targetPort"] is not None and config["external"] is None) or (config["targetPort"] is None and config["external"] is not None): + raise ValidationError("Usage error: must specify --target-port with --ingress") + + if transport is not None: + containerapp_def["properties"]["configuration"]["transport"] = transport + + # TODO: Need list_secrets API to do secrets before registries + + if update_map["registries"]: + 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 len(registries_def) == 0: # Adding new registry + if not(registry_server is not None and registry_user is not None and registry_pass is not None): + raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") + + registry = RegistryCredentials + registry["server"] = registry_server + registry["username"] = registry_user + registries_def.append(registry) + elif len(registries_def) == 1: # Modifying single registry + if registry_server is not None: + registries_def[0]["server"] = registry_server + if registry_user is not None: + registries_def[0]["username"] = registry_user + else: # Multiple registries + raise ValidationError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") + + if "secrets" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["secrets"] = [] + secrets_def = containerapp_def["properties"]["configuration"]["secrets"] + + registries_def[0]["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass, update_existing_secret=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) + + 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") From c65b264b1379ad9185c137ca544afc434d2aacc6 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Feb 2022 14:52:07 -0800 Subject: [PATCH 102/177] Add scale command --- src/containerapp/azext_containerapp/_help.py | 8 +++ .../azext_containerapp/_params.py | 4 ++ .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 70 ++++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 27b2c0c98f3..bb669b2a11f 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -109,6 +109,14 @@ text: az containerapp delete -g MyResourceGroup -n MyContainerapp """ +helps['containerapp scale'] = """ + type: command + short-summary: Set the min and max replicas for a Containerapp. + examples: + - name: Scale a Containerapp. + text: az containerapp scale -g MyResourceGroup -n MyContainerapp --min-replicas 1 --max-replicas 2 +""" + helps['containerapp show'] = """ type: command short-summary: Show details of a Containerapp. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c41e729e2d2..8c52a2eecc2 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,6 +66,10 @@ def load_arguments(self, _): c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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 scale') as c: + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + with self.argument_context('containerapp env') as c: c.argument('name', name_type, help='Name of the containerapp environment') c.argument('resource_group_name', arg_type=resource_group_name_type) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 330ac7234cb..d2cb9c22668 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -37,6 +37,7 @@ def load_command_table(self, _): g.custom_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()) + g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 68db52f3238..147dee12fd9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from platform import platform +from turtle import update from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait @@ -110,7 +111,7 @@ def create_containerapp(cmd, registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) config_def = Configuration - config_def["secrets"] = None # TODO: Uncomment secrets_def + config_def["secrets"] = secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def config_def["registries"] = [registries_def] @@ -345,6 +346,73 @@ def update_containerapp(cmd, handle_raw_exception(e) +def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_replicas=None, no_wait=False): + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + shouldWork = False # TODO: Should only setting minReplicas and maxReplicas in the body work? Or do we have to do a GET on the containerapp, add in secrets, then modify minReplicas and maxReplicas + if shouldWork: + updated_containerapp_def = { + "location": containerapp_def["location"], + "properties": { + "template": { + "scale": None + } + } + } + + if "scale" not in containerapp_def["properties"]["template"]: + updated_containerapp_def["properties"]["template"]["scale"] = {} + else: + updated_containerapp_def["properties"]["template"]["scale"] = containerapp_def["properties"]["template"]["scale"] + + if min_replicas is not None: + updated_containerapp_def["properties"]["template"]["scale"]["minReplicas"] = min_replicas + + if max_replicas is not None: + updated_containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas + + try: + r = ContainerAppClient.create_or_update( + cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=updated_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 scale 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) + else: + 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 + + del containerapp_def["properties"]["configuration"]["registries"] + del containerapp_def["properties"]["configuration"]["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) + + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp scale 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") From 2901b61d4fa5ad169d7096201dc8d7be002a3fea Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 15 Feb 2022 11:46:47 -0800 Subject: [PATCH 103/177] Various validations, small fixes --- .../azext_containerapp/_clients.py | 36 +++++++++---------- .../azext_containerapp/_params.py | 20 +++++------ src/containerapp/azext_containerapp/_utils.py | 8 ++--- .../azext_containerapp/_validators.py | 7 +++- .../azext_containerapp/commands.py | 7 ---- src/containerapp/azext_containerapp/custom.py | 2 +- 6 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 9b2f0f89750..b4552ebfaeb 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -261,7 +261,7 @@ def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no return r.json() @classmethod - def update(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wait=False): + 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) @@ -273,7 +273,7 @@ def update(cls, cmd, resource_group_name, name, kube_environment_envelope, no_wa name, api_version) - r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(kube_environment_envelope)) + r = send_raw_request(cmd.cli_ctx, "PATCH", request_url, body=json.dumps(managed_environment_envelope)) if no_wait: return r.json() @@ -335,7 +335,7 @@ def show(cls, cmd, resource_group_name, name): @classmethod def list_by_subscription(cls, cmd, formatter=lambda x: x): - kube_list = [] + env_list = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -347,23 +347,23 @@ def list_by_subscription(cls, cmd, formatter=lambda x: x): r = send_raw_request(cmd.cli_ctx, "GET", request_url) j = r.json() - for kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + 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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + for env in j["value"]: + formatted = formatter(env) + env_list.append(formatted) - return kube_list + return env_list @classmethod def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x): - kube_list = [] + env_list = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -377,16 +377,16 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) r = send_raw_request(cmd.cli_ctx, "GET", request_url) j = r.json() - for kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + 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 kube in j["value"]: - formatted = formatter(kube) - kube_list.append(formatted) + for env in j["value"]: + formatted = formatter(env) + env_list.append(formatted) - return kube_list + return env_list diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 8c52a2eecc2..c36d70c5fea 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -12,7 +12,7 @@ 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) + validate_registry_user, validate_registry_pass, validate_target_port, validate_ingress) def load_arguments(self, _): @@ -74,21 +74,21 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the containerapp 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('logs_destination', options_list=['--logs-dest']) - c.argument('logs_customer_id', options_list=['--logs-workspace-id'], help='Log analytics workspace ID') - c.argument('logs_key', options_list=['--logs-workspace-key'], help='Log analytics workspace key') + c.argument('logs_destination', type=str, options_list=['--logs-dest']) + c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Log analytics workspace ID') + c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log analytics workspace key') c.argument('tags', arg_type=tags_type) with self.argument_context('containerapp env', arg_group='Dapr') as c: c.argument('instrumentation_key', options_list=['--instrumentation-key'], help='Azure Monitor 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', options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') - c.argument('app_subnet_resource_id', 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', 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', 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', 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', 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') + c.argument('infrastructure_subnet_resource_id', type=str, options_list=['--infrastructure-subnet-resource-id'], help='Resource ID of a subnet for infrastructure components. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + 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 managed environment.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index c2565c651b6..c77d45f3557 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -63,8 +63,8 @@ def parse_env_var_flags(env_string, is_update_containerapp=False): env_pairs = {} for pair in env_pair_strings: - key_val = pair.split('=') - if len(key_val) is not 2: + key_val = pair.split('=', 1) + if len(key_val) != 2: if is_update_containerapp: raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\".") @@ -75,7 +75,7 @@ def parse_env_var_flags(env_string, is_update_containerapp=False): env_var_def = [] for key, value in env_pairs.items(): - if len(value) is 2: + if len(value) == 2: env_var_def.append({ "name": key, "secretRef": value[1] @@ -95,7 +95,7 @@ def parse_secret_flags(secret_string): for pair in secret_pair_strings: key_val = pair.split('=', 1) - if len(key_val) is not 2: + 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])) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 1f5913e3fed..b0dcb62a9e7 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -42,7 +42,12 @@ def validate_memory(namespace): raise ValidationError("Usage error: --memory must be a number ending with \"Gi\"") def validate_cpu(namespace): - return + if namespace.cpu: + cpu = namespace.cpu + try: + float(cpu) + except ValueError: + raise ValidationError("Usage error: --cpu must be a number eg. \"0.5\"") def validate_managed_env_name_or_id(cmd, namespace): from azure.cli.core.commands.client_factory import get_subscription_id diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index d2cb9c22668..177aee414b6 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -26,13 +26,6 @@ def transform_containerapp_list_output(apps): def load_command_table(self, _): - - # TODO: Add command type here - # containerapp_sdk = CliCommandType( - # operations_tmpl='.operations#None.{}', - # client_factory=cf_containerapp) - - with self.command_group('containerapp') as g: g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) g.custom_command('list', 'list_containerapp', table_transformer=transform_containerapp_list_output) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 147dee12fd9..c272a42c91e 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -493,7 +493,7 @@ def create_managed_environment(cmd, if infrastructure_subnet_resource_id is not None: if not app_subnet_resource_id: - raise ValidationError('App subnet resource ID needs to be supplied with controlplane subnet resource ID.') + raise ValidationError('App subnet resource ID needs to be supplied with infrastructure subnet resource ID.') vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id if app_subnet_resource_id is not None: From 02cf535d268af37639ea8cce30860c78630e8a75 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 18 Feb 2022 07:57:42 -0800 Subject: [PATCH 104/177] listSecrets API for updates, autogen log analytics for env --- .../azext_containerapp/_client_factory.py | 9 ++ .../azext_containerapp/_clients.py | 38 ++++- src/containerapp/azext_containerapp/_help.py | 6 +- .../azext_containerapp/_params.py | 8 +- src/containerapp/azext_containerapp/_utils.py | 133 +++++++++++++++++- .../azext_containerapp/_validators.py | 3 +- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 104 +++++--------- 8 files changed, 224 insertions(+), 79 deletions(-) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 3ee674ace77..cc9da7661ec 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -72,6 +72,15 @@ 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 def cf_containerapp(cli_ctx, *_): diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index b4552ebfaeb..9575a1ced03 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -11,7 +11,9 @@ from sys import api_version 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" @@ -152,7 +154,14 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): resource_group_name, name, api_version) - poll(cmd, request_url, "cancelled") + + 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') return @classmethod @@ -229,6 +238,24 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) return app_list + @classmethod + def list_secrets(cls, cmd, resource_group_name, name): + secrets = [] + + 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() + class ManagedEnvironmentClient(): @classmethod @@ -314,7 +341,14 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): resource_group_name, name, api_version) - poll(cmd, request_url, "scheduledfordelete") + + 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 diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index bb669b2a11f..d6a4b353e15 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -148,7 +148,11 @@ type: command short-summary: Create a Containerapp environment. examples: - - name: Create a Containerapp Environment. + - name: Create a Containerapp Environment with an autogenerated Log Analytics + text: | + az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ + -- location Canada Central + - name: Create a Containerapp Environment with Log Analytics text: | az containerapp env create -n MyContainerappEnvironment -g MyResourceGroup \\ --logs-workspace-id myLogsWorkspaceID \\ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c36d70c5fea..184a3a0e100 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -74,11 +74,13 @@ def load_arguments(self, _): c.argument('name', name_type, help='Name of the containerapp 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('logs_destination', type=str, options_list=['--logs-dest']) - c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Log analytics workspace ID') - c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log analytics workspace key') c.argument('tags', arg_type=tags_type) + with self.argument_context('containerapp env', arg_group='Log Analytics') as c: + c.argument('logs_destination', type=str, options_list=['--logs-dest']) + 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=['--instrumentation-key'], help='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index c77d45f3557..573b5ead3a5 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -3,12 +3,15 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from distutils.filelist import findall from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) 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 urllib.parse import urlparse -from ._client_factory import providers_client_factory, cf_resource_groups +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__) @@ -34,7 +37,7 @@ def _validate_subscription_registered(cmd, resource_provider): pass -def _ensure_location_allowed(cmd, location, resource_provider): +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)) @@ -43,15 +46,15 @@ def _ensure_location_allowed(cmd, location, resource_provider): resource_types = getattr(providers_client.get(resource_provider), 'resource_types', []) res_locations = [] for res in resource_types: - if res and getattr(res, 'resource_type', "") == 'containerApps': + if res and getattr(res, 'resource_type', "") == resource_type: res_locations = getattr(res, 'locations', []) - res_locations = [res_loc.lower().replace(" ", "") for res_loc in res_locations if res_loc.strip()] + 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=='containerApps'].locations\"`".format( - location, resource_provider)) + 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: @@ -153,3 +156,121 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ def parse_list_of_strings(comma_separated_string): comma_separated = comma_separated_string.split(',') return [s.strip() for s in comma_separated] + + +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): + location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") + if location: + return location + + except Exception: + return default_location + return default_location + +# Generate random 4 character string +def _new_tiny_guid(): + import random, 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: + 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)) + + 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") + 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: + handle_raw_exception(e) + + containerapp_def["properties"]["configuration"]["secrets"] = secrets["value"] diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index b0dcb62a9e7..4b3286fa687 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -58,7 +58,8 @@ def validate_managed_env_name_or_id(cmd, namespace): namespace.managed_env = resource_id( subscription=get_subscription_id(cmd.cli_ctx), resource_group=namespace.resource_group_name, - namespace='Microsoft.App', type='managedEnvironments', + namespace='Microsoft.App', + type='managedEnvironments', name=namespace.managed_env ) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 177aee414b6..998e41cf3ae 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -39,5 +39,5 @@ def load_command_table(self, _): g.custom_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('update', 'update_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) + # g.custom_command('update', 'update_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()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c272a42c91e..a2827b62eea 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -17,7 +17,8 @@ from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) 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_list_of_strings, parse_env_var_flags) + parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, parse_env_var_flags, + _generate_log_analytics_if_not_provided, _get_existing_secrets) logger = get_logger(__name__) @@ -46,6 +47,7 @@ def create_containerapp(cmd, dapr_app_id=None, dapr_app_protocol=None, # dapr_components=None, + revision_suffix=None, location=None, startup_command=None, args=None, @@ -54,7 +56,7 @@ def create_containerapp(cmd, 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") + _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: # TODO: Implement yaml @@ -80,7 +82,13 @@ def create_containerapp(cmd, if not managed_env_info: raise ValidationError("The environment '{}' does not exist. Specify a valid environment".format(managed_env)) - location = location or managed_env_info.location + if not location: + location = managed_env_info["location"] + elif location.lower() != managed_env_info["location"].lower(): + raise ValidationError("The location \"{}\" of the containerapp must be the same as the Managed Environment location \"{}\"".format( + location, + managed_env_info["location"] + )) external_ingress = None if ingress is not None: @@ -114,7 +122,7 @@ def create_containerapp(cmd, config_def["secrets"] = secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def - config_def["registries"] = [registries_def] + config_def["registries"] = [registries_def] if registries_def is not None else None scale_def = None if min_replicas is not None or max_replicas is not None: @@ -153,6 +161,9 @@ def create_containerapp(cmd, template_def["scale"] = scale_def template_def["dapr"] = dapr_def + if revision_suffix is not None: + template_def["revisionSuffix"] = revision_suffix + containerapp_def = ContainerApp containerapp_def["location"] = location containerapp_def["properties"]["managedEnvironmentId"] = managed_env @@ -301,7 +312,7 @@ def update_containerapp(cmd, if transport is not None: containerapp_def["properties"]["configuration"]["transport"] = transport - # TODO: Need list_secrets API to do secrets before registries + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) if update_map["registries"]: registries_def = None @@ -356,61 +367,27 @@ def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_re if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) - shouldWork = False # TODO: Should only setting minReplicas and maxReplicas in the body work? Or do we have to do a GET on the containerapp, add in secrets, then modify minReplicas and maxReplicas - if shouldWork: - updated_containerapp_def = { - "location": containerapp_def["location"], - "properties": { - "template": { - "scale": None - } - } - } - - if "scale" not in containerapp_def["properties"]["template"]: - updated_containerapp_def["properties"]["template"]["scale"] = {} - else: - updated_containerapp_def["properties"]["template"]["scale"] = containerapp_def["properties"]["template"]["scale"] - - if min_replicas is not None: - updated_containerapp_def["properties"]["template"]["scale"]["minReplicas"] = min_replicas - - if max_replicas is not None: - updated_containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - - try: - r = ContainerAppClient.create_or_update( - cmd=cmd, resource_group_name=resource_group_name, name=name, container_app_envelope=updated_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 scale 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) - else: - if "scale" not in containerapp_def["properties"]["template"]: - containerapp_def["properties"]["template"]["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 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 + if max_replicas is not None: + containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - del containerapp_def["properties"]["configuration"]["registries"] - del containerapp_def["properties"]["configuration"]["secrets"] + _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) + 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 scale in progress. Please monitor the update using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp scale 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) + return r + except Exception as e: + handle_raw_exception(e) def show_containerapp(cmd, name, resource_group_name): @@ -441,10 +418,7 @@ def delete_containerapp(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") try: - r = ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - if not r and not no_wait: - logger.warning('Containerapp successfully deleted') - return r + 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) @@ -452,8 +426,8 @@ def delete_containerapp(cmd, name, resource_group_name, no_wait=False): def create_managed_environment(cmd, name, resource_group_name, - logs_customer_id, - logs_key, + logs_customer_id=None, + logs_key=None, logs_destination="log-analytics", location=None, instrumentation_key=None, @@ -469,7 +443,10 @@ def create_managed_environment(cmd, 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") + _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 = LogAnalyticsConfiguration log_analytics_config_def["customerId"] = logs_customer_id @@ -581,9 +558,6 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") try: - r = ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - if not r and not no_wait: - logger.warning('Containerapp environment successfully deleted') - return r + 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) From fbd6407d1745b8efbcdc32f87e6e664b6f905753 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 09:10:10 -0800 Subject: [PATCH 105/177] Use space delimiter for secrets and env variables --- src/containerapp/azext_containerapp/_params.py | 4 ++-- src/containerapp/azext_containerapp/_utils.py | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 184a3a0e100..e851bc3639d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -34,7 +34,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', type=str, options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Comma-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") + c.argument('env_vars', nargs='*', options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -58,7 +58,7 @@ def load_arguments(self, _): c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") - c.argument('secrets', type=str, options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Comma-separated values in 'key=value' format.") + c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") # Ingress with self.argument_context('containerapp', arg_group='Ingress') as c: diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 573b5ead3a5..33da031e78d 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -61,11 +61,10 @@ def _ensure_location_allowed(cmd, location, resource_provider, resource_type): pass -def parse_env_var_flags(env_string, is_update_containerapp=False): - env_pair_strings = env_string.split(',') +def parse_env_var_flags(env_list, is_update_containerapp=False): env_pairs = {} - for pair in env_pair_strings: + for pair in env_list: key_val = pair.split('=', 1) if len(key_val) != 2: if is_update_containerapp: @@ -92,11 +91,10 @@ def parse_env_var_flags(env_string, is_update_containerapp=False): return env_var_def -def parse_secret_flags(secret_string): - secret_pair_strings = secret_string.split(',') +def parse_secret_flags(secret_list): secret_pairs = {} - for pair in secret_pair_strings: + for pair in secret_list: key_val = pair.split('=', 1) if len(key_val) != 2: raise ValidationError("--secrets: must be in format \"=,=,...\"") From 6513981d6d61d381d73a1663cdd5b2a3e5c88eb8 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 25 Feb 2022 09:14:26 -0800 Subject: [PATCH 106/177] Verify sub is registered to Microsoft.ContainerRegistration if creating vnet enabled env, remove logs-type parameter --- src/containerapp/azext_containerapp/_params.py | 1 - src/containerapp/azext_containerapp/custom.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index e851bc3639d..7c66cd3c526 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -77,7 +77,6 @@ def load_arguments(self, _): c.argument('tags', arg_type=tags_type) with self.argument_context('containerapp env', arg_group='Log Analytics') as c: - c.argument('logs_destination', type=str, options_list=['--logs-dest']) 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.') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a2827b62eea..1a0425f2d2f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -428,7 +428,6 @@ def create_managed_environment(cmd, resource_group_name, logs_customer_id=None, logs_key=None, - logs_destination="log-analytics", location=None, instrumentation_key=None, infrastructure_subnet_resource_id=None, @@ -445,6 +444,10 @@ def create_managed_environment(cmd, _validate_subscription_registered(cmd, "Microsoft.App") _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") + # Microsoft.ContainerService RP registration is required for vnet enabled environments + if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: + _validate_subscription_registered(cmd, "Microsoft.ContainerService") + 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) @@ -453,7 +456,7 @@ def create_managed_environment(cmd, log_analytics_config_def["sharedKey"] = logs_key app_logs_config_def = AppLogsConfiguration - app_logs_config_def["destination"] = logs_destination + app_logs_config_def["destination"] = "log-analytics" app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def managed_env_def = ManagedEnvironment From bacf864f4354969eec5c633c2817dd27de0c2343 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Feb 2022 21:46:42 -0800 Subject: [PATCH 107/177] Containerapp create --yaml --- src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/_params.py | 8 +- .../azext_containerapp/_sdk_models.py | 3390 +++++++++++++++++ src/containerapp/azext_containerapp/_utils.py | 66 + .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 223 +- 6 files changed, 3662 insertions(+), 28 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_sdk_models.py diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index d6a4b353e15..05c2f63b96e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -60,7 +60,7 @@ - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - -- yaml "C:/path/to/yaml/file.yml" + --yaml "C:/path/to/yaml/file.yml" """ helps['containerapp update'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 7c66cd3c526..16a44fe17d5 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -7,7 +7,7 @@ from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, - get_resource_name_completion_list, + get_resource_name_completion_list, file_type, get_three_state_flag, get_enum_type, tags_type) from azure.cli.core.commands.validators import get_default_location_from_resource_group @@ -27,14 +27,14 @@ def load_arguments(self, _): 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', '-e'], help="Name or resource ID of the containerapp's environment.") - c.argument('yaml', help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') + c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables', '-v'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") + c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -62,7 +62,7 @@ def load_arguments(self, _): # Ingress with self.argument_context('containerapp', arg_group='Ingress') as c: - c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external+internal ingress traffic to the Containerapp.") + c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py new file mode 100644 index 00000000000..9472034039d --- /dev/null +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -0,0 +1,3390 @@ +# 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. +# -------------------------------------------------------------------------- + +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 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'}, + '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.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 dapr: Dapr configuration for the Container App. + :type dapr: ~commondefinitions.models.Dapr + :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'}, + 'dapr': {'key': 'dapr', 'type': 'Dapr'}, + '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.dapr = kwargs.get('dapr', 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 index 33da031e78d..0478500f032 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- from distutils.filelist import findall +from operator import is_ from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger @@ -272,3 +273,68 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): handle_raw_exception(e) containerapp_def["properties"]["configuration"]["secrets"] = secrets["value"] + + +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 _object_to_dict(obj): + import json + return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) + + +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" + ] + + 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] diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 998e41cf3ae..ef15c7236e2 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -33,6 +33,7 @@ def load_command_table(self, _): g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('test', 'create_or_update_containerapp_yaml') with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1a0425f2d2f..00b7caf0148 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -10,19 +10,187 @@ from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger -from msrestazure.tools import parse_resource_id +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 -from ._models import (ManagedEnvironment, VnetConfiguration, AppLogsConfiguration, LogAnalyticsConfiguration, - Ingress, Configuration, Template, RegistryCredentials, ContainerApp, Dapr, ContainerResources, Scale, Container) +from ._sdk_models import * +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) 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_list_of_strings, parse_env_var_flags, - _generate_log_analytics_if_not_provided, _get_existing_secrets) + _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) 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: + return yaml.safe_load(stream) + except (IOError, OSError) as ex: + if getattr(ex, 'errno', 0) == errno.ENOENT: + raise CLIError('{} does not exist'.format(file_name)) + raise + except (yaml.parser.ParserError, UnicodeDecodeError) as ex: + raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) + + +def create_deserializer(): + from msrest import Deserializer + import sys, inspect + + sdkClasses = inspect.getmembers(sys.modules["azext_containerapp._sdk_models"]) + deserializer = {} + + for sdkClass in sdkClasses: + deserializer[sdkClass[0]] = sdkClass[1] + + return Deserializer(deserializer) + + +def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name, is_update, no_wait=False): + yaml_containerapp = process_loaded_yaml(load_yaml_file(file_name)) + if type(yaml_containerapp) != dict: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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 as ex: + pass + + if is_update and current_containerapp_def is None: + raise ValidationError("The containerapp '{}' does not exist".format(name)) + + # 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + # 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'): + if is_update: + containerapp_def["properties"]['managedEnvironmentId'] = current_containerapp_def["properties"]['managedEnvironmentId'] + else: + raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + managed_env_id = containerapp_def["properties"]['managedEnvironmentId'] + if not managed_env_id: + raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + env_name = None + env_rg = None + env_info = None + + if (is_valid_resource_id(managed_env_id)): + parsed_managed_env = parse_resource_id(managed_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'] + + # Secrets + if is_update: + add_secrets = [] + if containerapp_def["properties"]["configuration"].get('secrets'): + add_secrets = containerapp_def["properties"]["configuration"]["secrets"] + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + if add_secrets: + _add_or_update_secrets(containerapp_def, add_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) + + if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: + logger.warning('Containerapp {} in progress. Please monitor the {} using `az containerapp show -n {} -g {}`'.format( + "update" if is_update else "creation", + name, + resource_group_name)) + + return r + except Exception as e: + handle_raw_exception(e) + + def create_containerapp(cmd, name, resource_group_name, @@ -59,8 +227,12 @@ def create_containerapp(cmd, _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: - # TODO: Implement yaml - raise CLIError("--yaml is not yet implemented") + if image_name 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\ + location 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=False, no_wait=no_wait) if image_name is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') @@ -99,7 +271,7 @@ def create_containerapp(cmd, ingress_def = None if target_port is not None and ingress is not None: - ingress_def = Ingress + ingress_def = IngressModel ingress_def["external"] = external_ingress ingress_def["targetPort"] = target_port ingress_def["transport"] = transport @@ -110,7 +282,7 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: - registries_def = RegistryCredentials + registries_def = RegistryCredentialsModel registries_def["server"] = registry_server registries_def["username"] = registry_user @@ -118,7 +290,7 @@ def create_containerapp(cmd, secrets_def = [] registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) - config_def = Configuration + config_def = ConfigurationModel config_def["secrets"] = secrets_def config_def["activeRevisionsMode"] = revisions_mode config_def["ingress"] = ingress_def @@ -126,17 +298,17 @@ def create_containerapp(cmd, scale_def = None if min_replicas is not None or max_replicas is not None: - scale_def = Scale + 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 = ContainerResources + resources_def = ContainerResourcesModel resources_def["cpu"] = cpu resources_def["memory"] = memory - container_def = Container + container_def = ContainerModel container_def["name"] = name container_def["image"] = image_name if env_vars is not None: @@ -150,13 +322,13 @@ def create_containerapp(cmd, dapr_def = None if dapr_enabled: - dapr_def = Dapr + dapr_def = DaprModel dapr_def["daprEnabled"] = True dapr_def["appId"] = dapr_app_id dapr_def["appPort"] = dapr_app_port dapr_def["appProtocol"] = dapr_app_protocol - template_def = Template + template_def = TemplateModel template_def["containers"] = [container_def] template_def["scale"] = scale_def template_def["dapr"] = dapr_def @@ -164,7 +336,7 @@ def create_containerapp(cmd, if revision_suffix is not None: template_def["revisionSuffix"] = revision_suffix - containerapp_def = ContainerApp + containerapp_def = ContainerAppModel containerapp_def["location"] = location containerapp_def["properties"]["managedEnvironmentId"] = managed_env containerapp_def["properties"]["configuration"] = config_def @@ -215,8 +387,12 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - # TODO: Implement yaml - raise CLIError("--yaml is not yet implemented") + if image_name 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=True, no_wait=no_wait) containerapp_def = None try: @@ -313,6 +489,7 @@ def update_containerapp(cmd, containerapp_def["properties"]["configuration"]["transport"] = transport _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) if update_map["registries"]: registries_def = None @@ -327,7 +504,7 @@ def update_containerapp(cmd, if not(registry_server is not None and registry_user is not None and registry_pass is not None): raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") - registry = RegistryCredentials + registry = RegistryCredentialsModel registry["server"] = registry_server registry["username"] = registry_user registries_def.append(registry) @@ -451,15 +628,15 @@ def create_managed_environment(cmd, 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 = LogAnalyticsConfiguration + log_analytics_config_def = LogAnalyticsConfigurationModel log_analytics_config_def["customerId"] = logs_customer_id log_analytics_config_def["sharedKey"] = logs_key - app_logs_config_def = AppLogsConfiguration + app_logs_config_def = AppLogsConfigurationModel app_logs_config_def["destination"] = "log-analytics" app_logs_config_def["logAnalyticsConfiguration"] = log_analytics_config_def - managed_env_def = ManagedEnvironment + managed_env_def = ManagedEnvironmentModel managed_env_def["location"] = location managed_env_def["properties"]["internalLoadBalancerEnabled"] = False managed_env_def["properties"]["appLogsConfiguration"] = app_logs_config_def @@ -469,7 +646,7 @@ def create_managed_environment(cmd, managed_env_def["properties"]["daprAIInstrumentationKey"] = instrumentation_key if infrastructure_subnet_resource_id or app_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: - vnet_config_def = VnetConfiguration + vnet_config_def = VnetConfigurationModel if infrastructure_subnet_resource_id is not None: if not app_subnet_resource_id: @@ -518,7 +695,7 @@ def update_managed_environment(cmd, _validate_subscription_registered(cmd, "Microsoft.App") - managed_env_def = ManagedEnvironment + managed_env_def = ManagedEnvironmentModel managed_env_def["tags"] = tags try: From 9777c5fc8f0eef657efc92ba40af1a94e437d114 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Feb 2022 22:25:33 -0800 Subject: [PATCH 108/177] Fix updating registry to do create or update --- src/containerapp/azext_containerapp/custom.py | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 00b7caf0148..56162f0e759 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -500,28 +500,41 @@ def update_containerapp(cmd, registries_def = containerapp_def["properties"]["configuration"]["registries"] - if len(registries_def) == 0: # Adding new registry + if not registry_server: + raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + + # Check if updating existing registry + updating_existing_registry = False + for r in registries_def: + if r['server'].lower() == registry_server.lower(): + updating_existing_registry = True + + if registry_user: + r["username"] = registry_user + if registry_pass: + r["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + r["username"], + r["server"], + registry_pass, + update_existing_secret=True) + + # If not updating existing registry, add as new registry + if not updating_existing_registry: if not(registry_server is not None and registry_user is not None and registry_pass is not None): raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") registry = RegistryCredentialsModel registry["server"] = registry_server registry["username"] = registry_user - registries_def.append(registry) - elif len(registries_def) == 1: # Modifying single registry - if registry_server is not None: - registries_def[0]["server"] = registry_server - if registry_user is not None: - registries_def[0]["username"] = registry_user - else: # Multiple registries - raise ValidationError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") - - if "secrets" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["secrets"] = [] - secrets_def = containerapp_def["properties"]["configuration"]["secrets"] - - registries_def[0]["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass, update_existing_secret=True) + registry["passwordSecretRef"] = store_as_secret_and_return_secret_ref( + containerapp_def["properties"]["configuration"]["secrets"], + registry_user, + registry_server, + registry_pass, + 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) From 71e9c9b99316a4d35902de5f0ad6845db176cc57 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 23 Feb 2022 08:17:32 -0800 Subject: [PATCH 109/177] Fix containerapp update command. Add image-name parameter to support multi container updates. Fix updating registries, containers and secrets --- .../azext_containerapp/_params.py | 9 +- src/containerapp/azext_containerapp/_utils.py | 25 +++++ src/containerapp/azext_containerapp/custom.py | 97 +++++++++++++------ 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 16a44fe17d5..a664c5bfcc4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -31,12 +31,13 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: - c.argument('image_name', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag. If there are multiple containers, please use --yaml instead.") + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. If there are multiple containers, please use --yaml instead.") - c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'. If there are multiple containers, please use --yaml instead.") - c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'. If there are multiple containers, please use --yaml instead.") + c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'.") + c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'.") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0478500f032..0a092694e59 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -291,6 +291,31 @@ def _add_or_update_secrets(containerapp_def, add_secrets): containerapp_def["properties"]["configuration"]["secrets"].append(new_secret) +def _add_or_update_env_vars(existing_env_vars, new_env_vars): + 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 "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: + existing_env_vars.append(new_env_var) + + def _object_to_dict(obj): import json return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 56162f0e759..4bc0b277861 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -33,7 +33,8 @@ 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_list_of_strings, 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) + _object_to_dict, _add_or_update_secrets, _remove_additional_attributes, _remove_readonly_attributes, + _add_or_update_env_vars) logger = get_logger(__name__) @@ -195,6 +196,7 @@ def create_containerapp(cmd, name, resource_group_name, yaml=None, + image=None, image_name=None, managed_env=None, min_replicas=None, @@ -227,14 +229,14 @@ def create_containerapp(cmd, _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") if yaml: - if image_name or managed_env or min_replicas or max_replicas or target_port or ingress or\ + 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\ location 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=False, no_wait=no_wait) - if image_name is None: + if image is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') if managed_env is None: @@ -309,8 +311,8 @@ def create_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = name - container_def["image"] = image_name + container_def["name"] = image_name if image_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: @@ -359,6 +361,7 @@ def update_containerapp(cmd, name, resource_group_name, yaml=None, + image=None, image_name=None, min_replicas=None, max_replicas=None, @@ -387,7 +390,7 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - if image_name or min_replicas or max_replicas or target_port or ingress or\ + if image 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: @@ -408,7 +411,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image_name or env_vars or cpu or memory or startup_command or args + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command or args update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -422,26 +425,62 @@ def update_containerapp(cmd, containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix # Containers - if image_name is not None: - containerapp_def["properties"]["template"]["containers"][0]["image"] = image_name - if env_vars is not None: - containerapp_def["properties"]["template"]["containers"][0]["env"] = parse_env_var_flags(env_vars) - if startup_command is not None: - containerapp_def["properties"]["template"]["containers"][0]["command"] = parse_list_of_strings(startup_command) - if args is not None: - containerapp_def["properties"]["template"]["containers"][0]["args"] = parse_list_of_strings(startup_command) - if cpu is not None or memory is not None: - resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] - if resources: - if cpu is not None: - resources["cpu"] = cpu - if memory is not None: - resources["memory"] = memory - else: - resources = containerapp_def["properties"]["template"]["containers"][0]["resources"] = { - "cpu": cpu, - "memory": memory - } + if update_map["container"]: + if not image_name: + 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() == image_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + if env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + if startup_command is not None: + c["command"] = parse_list_of_strings(startup_command) + if args is not None: + c["args"] = parse_list_of_strings(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"] = image_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"] = parse_list_of_strings(startup_command) + if args is not None: + container_def["args"] = parse_list_of_strings(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"]: @@ -489,7 +528,9 @@ def update_containerapp(cmd, containerapp_def["properties"]["configuration"]["transport"] = transport _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) + + if secrets is not None: + _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) if update_map["registries"]: registries_def = None From 5e3888a7ad855f96b7a6c8a249487a5c96547792 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 23 Feb 2022 11:42:24 -0800 Subject: [PATCH 110/177] started update with --yaml. Need to do create or update for when an attribute is a list of items --- src/containerapp/azext_containerapp/_utils.py | 32 +++- src/containerapp/azext_containerapp/custom.py | 152 ++++++++++++++---- 2 files changed, 151 insertions(+), 33 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0a092694e59..afd1834589e 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -316,6 +316,17 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars): existing_env_vars.append(new_env_var) +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 return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) @@ -345,6 +356,7 @@ def _remove_additional_attributes(o): for key in o: _remove_additional_attributes(o[key]) + def _remove_readonly_attributes(containerapp_def): unneeded_properties = [ "id", @@ -355,7 +367,8 @@ def _remove_readonly_attributes(containerapp_def): "latestRevisionName", "latestRevisionFqdn", "customDomainVerificationId", - "outboundIpAddresses" + "outboundIpAddresses", + "fqdn" ] for unneeded_property in unneeded_properties: @@ -363,3 +376,20 @@ def _remove_readonly_attributes(containerapp_def): del containerapp_def[unneeded_property] elif unneeded_property in containerapp_def['properties']: del containerapp_def['properties'][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 + import collections + + for key, val in new_dict.items(): + if isinstance(val, collections.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 \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4bc0b277861..2ea394304af 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -34,7 +34,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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_env_vars, _add_or_update_tags, update_nested_dictionary) logger = get_logger(__name__) @@ -82,7 +82,7 @@ def create_deserializer(): return Deserializer(deserializer) -def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name, is_update, no_wait=False): +def update_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: raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') @@ -106,7 +106,7 @@ def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name except Exception as ex: pass - if is_update and current_containerapp_def is None: + if not current_containerapp_def: raise ValidationError("The containerapp '{}' does not exist".format(name)) # Deserialize the yaml into a ContainerApp object. Need this since we're not using SDK @@ -129,27 +129,129 @@ def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name # 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) + + ''' + # Not sure if update should replace items that are a list, or do createOrUpdate. This commented out section is the implementation for createOrUpdate. + # (If a property is a list, do createOrUpdate, rather than just replace with new list) + + if 'properties' in containerapp_def and 'template' in containerapp_def['properties']: + # Containers + if 'containers' in containerapp_def['properties']['template'] and containerapp_def['properties']['template']['containers']: + for new_container in containerapp_def['properties']['template']['containers']: + if "name" not in new_container or not new_container["name"]: + raise ValidationError("The container name is not specified.") + + # Check if updating existing container + updating_existing_container = False + for existing_container in current_containerapp_def["properties"]["template"]["containers"]: + if existing_container['name'].lower() == new_container['name'].lower(): + updating_existing_container = True + + if 'image' in new_container and new_container['image']: + existing_container['image'] = new_container['image'] + if 'env' in new_container and new_container['env']: + if 'env' not in existing_container or not existing_container['env']: + existing_container['env'] = [] + _add_or_update_env_vars(existing_container['env'], new_container['env']) + if 'command' in new_container and new_container['command']: + existing_container['command'] = new_container['command'] + if 'args' in new_container and new_container['args']: + existing_container['args'] = new_container['args'] + if 'resources' in new_container and new_container['resources']: + if 'cpu' in new_container['resources'] and new_container['resources']['cpu'] is not None: + existing_container['resources']['cpu'] = new_container['resources']['cpu'] + if 'memory' in new_container['resources'] and new_container['resources']['memory'] is not None: + existing_container['resources']['memory'] = new_container['resources']['memory'] + + # If not updating existing container, add as new container + if not updating_existing_container: + current_containerapp_def["properties"]["template"]["containers"].append(new_container) + + # Traffic Weights + + # Secrets + + # Registries + + # Scale rules + + # Source Controls + + ''' + 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: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + # 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'): - if is_update: - containerapp_def["properties"]['managedEnvironmentId'] = current_containerapp_def["properties"]['managedEnvironmentId'] - else: - raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') - - managed_env_id = containerapp_def["properties"]['managedEnvironmentId'] - if not managed_env_id: raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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(managed_env_id)): - parsed_managed_env = parse_resource_id(managed_env_id) + 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: @@ -167,25 +269,14 @@ def create_or_update_containerapp_yaml(cmd, name, resource_group_name, file_name if not containerapp_def.get('location'): containerapp_def['location'] = env_info['location'] - # Secrets - if is_update: - add_secrets = [] - if containerapp_def["properties"]["configuration"].get('secrets'): - add_secrets = containerapp_def["properties"]["configuration"]["secrets"] - - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - if add_secrets: - _add_or_update_secrets(containerapp_def, add_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) if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "waiting" and not no_wait: - logger.warning('Containerapp {} in progress. Please monitor the {} using `az containerapp show -n {} -g {}`'.format( - "update" if is_update else "creation", - name, - resource_group_name)) + 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: @@ -234,7 +325,7 @@ def create_containerapp(cmd, registry_user or registry_pass or dapr_enabled or dapr_app_port or dapr_app_id or\ location 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=False, no_wait=no_wait) + return create_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait) if image is None: raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') @@ -395,7 +486,7 @@ def update_containerapp(cmd, 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_or_update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, is_update=True, no_wait=no_wait) + 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: @@ -415,11 +506,8 @@ def update_containerapp(cmd, update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None - if update_map['container'] and len(containerapp_def['properties']['template']['containers']) > 1: - raise CLIError("Usage error: trying to update image, environment variables, resources claims on a multicontainer containerapp. Please use --yaml or ARM templates for multicontainer containerapp update") - if tags: - containerapp_def['tags'] = tags + _add_or_update_tags(containerapp_def, tags) if revision_suffix is not None: containerapp_def["properties"]["template"]["revisionSuffix"] = revision_suffix From b419f8d956b3f67033048e122995368441ac5c97 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 09:18:12 -0800 Subject: [PATCH 111/177] use space delimiter for startup_command and args, instead of comma delimiter --- src/containerapp/azext_containerapp/_params.py | 4 ++-- src/containerapp/azext_containerapp/commands.py | 1 - src/containerapp/azext_containerapp/custom.py | 12 ++++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index a664c5bfcc4..740a139afb0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -36,8 +36,8 @@ def load_arguments(self, _): c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") - c.argument('startup_command', type=str, options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Comma-separated values e.g. '/bin/queue'.") - c.argument('args', type=str, options_list=['--args'], help="A list of container startup command argument(s). Comma-separated values e.g. '-c, mycommand'.") + c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\".") + c.argument('args', nargs='*', options_list=['--args'], help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\".") c.argument('revision_suffix', type=str, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index ef15c7236e2..998e41cf3ae 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -33,7 +33,6 @@ def load_command_table(self, _): g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('test', 'create_or_update_containerapp_yaml') with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2ea394304af..a96b7ac2d6b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -407,9 +407,9 @@ def create_containerapp(cmd, if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) if startup_command is not None: - container_def["command"] = parse_list_of_strings(startup_command) + container_def["command"] = startup_command if args is not None: - container_def["args"] = parse_list_of_strings(args) + container_def["args"] = args if resources_def is not None: container_def["resources"] = resources_def @@ -530,9 +530,9 @@ def update_containerapp(cmd, c["env"] = [] _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) if startup_command is not None: - c["command"] = parse_list_of_strings(startup_command) + c["command"] = startup_command if args is not None: - c["args"] = parse_list_of_strings(args) + 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: @@ -562,9 +562,9 @@ def update_containerapp(cmd, if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) if startup_command is not None: - container_def["command"] = parse_list_of_strings(startup_command) + container_def["command"] = startup_command if args is not None: - container_def["args"] = parse_list_of_strings(args) + container_def["args"] = args if resources_def is not None: container_def["resources"] = resources_def From 84c56b5d745e77f5faaf7a78caa8a5e21dd90e4c Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 1 Mar 2022 07:34:07 -0800 Subject: [PATCH 112/177] Traffic weights --- .../azext_containerapp/_params.py | 1 + src/containerapp/azext_containerapp/_utils.py | 38 ++++++++++++++++++- src/containerapp/azext_containerapp/custom.py | 22 +++++++---- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 740a139afb0..4662a35bb1f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,6 +66,7 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format.") with self.argument_context('containerapp scale') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index afd1834589e..524024589dd 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -392,4 +392,40 @@ def update_nested_dictionary(orig_dict, new_dict): else: if new_dict[key] is not None: orig_dict[key] = new_dict[key] - return orig_dict \ No newline at end of file + return orig_dict + + +def _is_valid_weight(weight): + try: + n = int(weight) + if n >= 0 and n <= 100: + return True + return False + except ValueError: + return False + + +def _add_or_update_traffic_Weights(containerapp_def, list_weights): + if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: + 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') + + for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: + if existing_weight["revisionName"].lower() == new_weight[0].lower(): + is_existing = True + existing_weight["weight"] = int(key_val[1]) + + if not is_existing: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ + "revisionName": key_val[0], + "weight": int(key_val[1]) + }) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a96b7ac2d6b..31510ad80a7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -34,7 +34,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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) + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _add_or_update_traffic_Weights) logger = get_logger(__name__) @@ -459,7 +459,7 @@ def update_containerapp(cmd, ingress=None, target_port=None, transport=None, - # traffic_weights=None, + traffic_weights=None, revisions_mode=None, secrets=None, env_vars=None, @@ -499,7 +499,7 @@ def update_containerapp(cmd, update_map = {} update_map['secrets'] = secrets is not None - update_map['ingress'] = ingress or target_port or transport + update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command or args @@ -597,23 +597,31 @@ def update_containerapp(cmd, containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode if update_map["ingress"]: + if "ingress" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["ingress"] = {} + external_ingress = None if ingress is not None: if ingress.lower() == "internal": external_ingress = False elif ingress.lower() == "external": external_ingress = True - containerapp_def["properties"]["configuration"]["external"] = external_ingress + + if external_ingress is not None: + containerapp_def["properties"]["configuration"]["ingress"]["external"] = external_ingress if target_port is not None: - containerapp_def["properties"]["configuration"]["targetPort"] = target_port + containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - config = containerapp_def["properties"]["configuration"] + config = containerapp_def["properties"]["configuration"]["ingress"] if (config["targetPort"] is not None and config["external"] is None) or (config["targetPort"] is None and config["external"] is not None): raise ValidationError("Usage error: must specify --target-port with --ingress") if transport is not None: - containerapp_def["properties"]["configuration"]["transport"] = transport + containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport + + if traffic_weights is not None: + containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = _add_or_update_traffic_Weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) From 07cae9d5ed02cefaff24da43ce9e8b9face94c6b Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 08:34:24 -0800 Subject: [PATCH 113/177] List and show revisions --- .../azext_containerapp/_clients.py | 49 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 18 +++++++ .../azext_containerapp/_params.py | 3 ++ src/containerapp/azext_containerapp/_utils.py | 10 ++++ .../azext_containerapp/commands.py | 21 ++++++++ src/containerapp/azext_containerapp/custom.py | 20 +++++++- 6 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 9575a1ced03..1a3a17bcc14 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -256,6 +256,55 @@ def list_secrets(cls, cmd, resource_group_name, name): 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() + class ManagedEnvironmentClient(): @classmethod diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 05c2f63b96e..e452af3eb04 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -138,6 +138,24 @@ az containerapp list -g MyResourceGroup """ +helps['containerapp revision show'] = """ + type: command + short-summary: Show details of a Containerapp's revision. + examples: + - name: Show details of a Containerapp's revision. + text: | + az containerapp revision show --revision-name MyContainerappRevision -g MyResourceGroup +""" + +helps['containerapp revision list'] = """ + type: command + short-summary: List details of a Containerapp's revisions. + examples: + - name: List a Containerapp's revisions. + text: | + az containerapp revision list -n MyContainerapp -g MyResourceGroup +""" + # Environment Commands helps['containerapp env'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 4662a35bb1f..913b4ee502d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -102,3 +102,6 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the managed Environment.') + + with self.argument_context('containerapp revision') as c: + c.argument('revision_name', type=str, help='Name of the revision') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 524024589dd..63006d1aae4 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -429,3 +429,13 @@ def _add_or_update_traffic_Weights(containerapp_def, list_weights): "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 diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 998e41cf3ae..da8269470af 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -25,6 +25,20 @@ def transform_containerapp_list_output(apps): return [transform_containerapp_output(a) for a in apps] +def transform_revision_output(rev): + props = ['name', 'replicas', 'active', 'createdTime'] + result = {k: rev[k] for k in rev if k in props} + + if 'latestRevisionFqdn' in rev['template']: + result['fqdn'] = rev['template']['latestRevisionFqdn'] + + 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') as g: g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) @@ -41,3 +55,10 @@ def load_command_table(self, _): g.custom_command('create', 'create_managed_environment', supports_no_wait=True, exception_handler=ex_handler_factory()) # g.custom_command('update', 'update_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 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_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 31510ad80a7..5014957291b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -34,7 +34,8 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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, _add_or_update_traffic_Weights) + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _add_or_update_traffic_Weights, + _get_app_from_revision) logger = get_logger(__name__) @@ -891,3 +892,20 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): 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 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) From 7df8730c2863890c7eb8b0f229d1e314f99de285 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Feb 2022 10:39:06 -0800 Subject: [PATCH 114/177] az containerapp revision restart, activate, deactivate --- .../azext_containerapp/_clients.py | 50 +++++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 35 ++++++++++++- .../azext_containerapp/commands.py | 6 +-- src/containerapp/azext_containerapp/custom.py | 30 +++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 1a3a17bcc14..4d525bee181 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -305,6 +305,56 @@ def show_revision(cls, cmd, resource_group_name, container_app_name, name): 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 diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index e452af3eb04..b32ac7f7b90 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -138,6 +138,12 @@ az containerapp list -g MyResourceGroup """ +# Revision Commands +helps['containerapp revision'] = """ + type: group + short-summary: Commands to manage a Containerapp's revisions. +""" + helps['containerapp revision show'] = """ type: command short-summary: Show details of a Containerapp's revision. @@ -153,7 +159,34 @@ examples: - name: List a Containerapp's revisions. text: | - az containerapp revision list -n MyContainerapp -g MyResourceGroup + az containerapp revision list --revision-name MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision restart'] = """ + type: command + short-summary: Restart a Containerapps's revision. + examples: + - name: Restart a Containerapp's revision. + text: | + az containerapp revision restart --revision-name MyContainerappRevision -g MyResourceGroup +""" + +helps['containerapp revision activate'] = """ + type: command + short-summary: Activates Containerapp's revision. + examples: + - name: Activate a Containerapp's revision. + text: | + az containerapp revision activate --revision-name MyContainerappRevision -g MyResourceGroup +""" + +helps['containerapp revision deactivate'] = """ + type: command + short-summary: Deactivates Containerapp's revision. + examples: + - name: Deactivate a Containerapp's revision. + text: | + az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup """ # Environment Commands diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index da8269470af..20d7c332c0d 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -57,8 +57,8 @@ def load_command_table(self, _): g.custom_command('delete', 'delete_managed_environment', supports_no_wait=True, confirmation=True, 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('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_command('restart', 'restart_revision') g.custom_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5014957291b..ae27c6474b7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -909,3 +909,33 @@ def show_revision(cmd, resource_group_name, revision_name, name=None): 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) + From 43897ccc4b7956d373fb09d2915f635612acb2ed Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Mar 2022 08:58:01 -0800 Subject: [PATCH 115/177] Add ability for users to clear args/command in az containerapp update --- .../azext_containerapp/_params.py | 4 +-- .../azext_containerapp/azext_metadata.json | 3 +- src/containerapp/azext_containerapp/custom.py | 29 ++++++++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 913b4ee502d..52453298085 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -36,8 +36,8 @@ def load_arguments(self, _): c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") - c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\".") - c.argument('args', nargs='*', options_list=['--args'], help="A list of container startup command argument(s). Space-separated values e.g. \"-c\" \"mycommand\".") + c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") + c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json index c2d0f4fe8d0..55c81bf3328 100644 --- a/src/containerapp/azext_containerapp/azext_metadata.json +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -1,5 +1,4 @@ { "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67", - "azext.maxCliCoreVersion": "2.33.0" + "azext.minCliCoreVersion": "2.0.67" } \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ae27c6474b7..00add15382d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,8 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from platform import platform -from turtle import update from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import sdk_no_wait @@ -503,7 +501,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command or args + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -516,7 +514,10 @@ def update_containerapp(cmd, # Containers if update_map["container"]: if not image_name: - raise ValidationError("Usage error: --image-name is required when adding or updating a container") + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + image_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 @@ -531,9 +532,15 @@ def update_containerapp(cmd, c["env"] = [] _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) if startup_command is not None: - c["command"] = startup_command + if isinstance(startup_command, list) and not startup_command: + c["command"] = None + else: + c["command"] = startup_command if args is not None: - c["args"] = args + 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: @@ -563,9 +570,15 @@ def update_containerapp(cmd, 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 isinstance(startup_command, list) and not startup_command: + container_def["command"] = None + else: + container_def["command"] = startup_command if args is not None: - container_def["args"] = args + 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 From 7a380e314b9cdb27d998dc667681ee6c43155202 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 3 Mar 2022 12:46:57 -0800 Subject: [PATCH 116/177] Various fixes, traffic weights fixes --- .../azext_containerapp/__init__.py | 3 +-- .../azext_containerapp/_client_factory.py | 19 +------------------ src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 9 ++------- .../azext_containerapp/_validators.py | 15 --------------- .../azext_containerapp/azext_metadata.json | 2 +- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 8 ++------ src/containerapp/setup.py | 2 +- 10 files changed, 11 insertions(+), 53 deletions(-) diff --git a/src/containerapp/azext_containerapp/__init__.py b/src/containerapp/azext_containerapp/__init__.py index e19af22d9e8..f772766731c 100644 --- a/src/containerapp/azext_containerapp/__init__.py +++ b/src/containerapp/azext_containerapp/__init__.py @@ -12,10 +12,9 @@ class ContainerappCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - from azext_containerapp._client_factory import cf_containerapp containerapp_custom = CliCommandType( operations_tmpl='azext_containerapp.custom#{}', - client_factory=cf_containerapp) + client_factory=None) super(ContainerappCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=containerapp_custom) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index cc9da7661ec..f998486c63e 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -10,7 +10,7 @@ # pylint: disable=inconsistent-return-statements -def ex_handler_factory(creating_plan=False, no_throw=False): +def ex_handler_factory(no_throw=False): def _polish_bad_errors(ex): import json from knack.util import CLIError @@ -21,15 +21,6 @@ def _polish_bad_errors(ex): elif 'Message' in content: detail = content['Message'] - if creating_plan: - if 'Requested features are not supported in region' in detail: - detail = ("Plan with linux worker is not supported in current region. For " + - "supported regions, please refer to https://docs.microsoft.com/" - "azure/app-service-web/app-service-linux-intro") - elif 'Not enough available reserved instance servers to satisfy' in detail: - detail = ("Plan with Linux worker can only be created in a group " + - "which has never contained a Windows worker, and vice versa. " + - "Please use a new resource group. Original error:" + detail) ex = CLIError(detail) except Exception: # pylint: disable=broad-except pass @@ -81,11 +72,3 @@ 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 - -def cf_containerapp(cli_ctx, *_): - - from azure.cli.core.commands.client_factory import get_mgmt_service_client - # TODO: Replace CONTOSO with the appropriate label and uncomment - # from azure.mgmt.CONTOSO import CONTOSOManagementClient - # return get_mgmt_service_client(cli_ctx, CONTOSOManagementClient) - return None diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index b32ac7f7b90..ac9638014c7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -218,7 +218,7 @@ helps['containerapp env delete'] = """ type: command - short-summary: Deletes a Containerapp Environment. + short-summary: Delete a Containerapp Environment. examples: - name: Delete Containerapp Environment. text: az containerapp env delete -g MyResourceGroup -n MyContainerappEnvironment diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 52453298085..c38c32711c4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,7 +66,7 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") - c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format.") + c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. 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, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 63006d1aae4..d0c5c996650 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -405,8 +405,8 @@ def _is_valid_weight(weight): return False -def _add_or_update_traffic_Weights(containerapp_def, list_weights): - if "traffic" not in containerapp_def["properties"]["configuration"]["ingress"]: +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: @@ -419,11 +419,6 @@ def _add_or_update_traffic_Weights(containerapp_def, list_weights): if not _is_valid_weight(key_val[1]): raise ValidationError('Traffic weights must be integers between 0 and 100') - for existing_weight in containerapp_def["properties"]["configuration"]["ingress"]["traffic"]: - if existing_weight["revisionName"].lower() == new_weight[0].lower(): - is_existing = True - existing_weight["weight"] = int(key_val[1]) - if not is_existing: containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({ "revisionName": key_val[0], diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 4b3286fa687..23ed260e360 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -7,21 +7,6 @@ from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) -def example_name_or_id_validator(cmd, namespace): - # Example of a storage account name or ID validator. - # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters - from azure.cli.core.commands.client_factory import get_subscription_id - from msrestazure.tools import is_valid_resource_id, resource_id - if namespace.storage_account: - if not is_valid_resource_id(namespace.RESOURCE): - namespace.storage_account = resource_id( - subscription=get_subscription_id(cmd.cli_ctx), - resource_group=namespace.resource_group_name, - namespace='Microsoft.Storage', - type='storageAccounts', - name=namespace.storage_account - ) - def _is_number(s): try: float(s) diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json index 55c81bf3328..001f223de90 100644 --- a/src/containerapp/azext_containerapp/azext_metadata.json +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.isPreview": true, "azext.minCliCoreVersion": "2.0.67" -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 20d7c332c0d..8fd840ccabd 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -6,7 +6,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType from msrestazure.tools import is_valid_resource_id, parse_resource_id -from azext_containerapp._client_factory import cf_containerapp, ex_handler_factory +from azext_containerapp._client_factory import ex_handler_factory def transform_containerapp_output(app): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 00add15382d..005bb52d9af 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -32,7 +32,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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, _add_or_update_traffic_Weights, + _add_or_update_env_vars, _add_or_update_tags, update_nested_dictionary, _update_traffic_Weights, _get_app_from_revision) logger = get_logger(__name__) @@ -627,15 +627,11 @@ def update_containerapp(cmd, if target_port is not None: containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - config = containerapp_def["properties"]["configuration"]["ingress"] - if (config["targetPort"] is not None and config["external"] is None) or (config["targetPort"] is None and config["external"] is not None): - raise ValidationError("Usage error: must specify --target-port with --ingress") - if transport is not None: containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport if traffic_weights is not None: - containerapp_def["properties"]["configuration"]["ingress"]["traffic"] = _add_or_update_traffic_Weights(containerapp_def, traffic_weights) + _update_traffic_Weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index b9f57ada671..be4cd26f637 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -57,4 +57,4 @@ packages=find_packages(), install_requires=DEPENDENCIES, package_data={'azext_containerapp': ['azext_metadata.json']}, -) \ No newline at end of file +) From 983af7c7d179260ffb61d42a587eedc108ac4547 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Fri, 4 Mar 2022 10:49:08 -0800 Subject: [PATCH 117/177] Verify subnet subscription is registered to Microsoft.ContainerServices --- src/containerapp/azext_containerapp/_utils.py | 11 +++++++---- src/containerapp/azext_containerapp/custom.py | 7 ++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index d0c5c996650..0ed9d21bf43 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -23,15 +23,18 @@ def _get_location_from_resource_group(cli_ctx, resource_group_name): return group.location -def _validate_subscription_registered(cmd, resource_provider): +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, get_subscription_id(cmd.cli_ctx)) + 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( - resource_provider, resource_provider)) + 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: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 005bb52d9af..3ef91290a5f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -783,7 +783,12 @@ def create_managed_environment(cmd, # Microsoft.ContainerService RP registration is required for vnet enabled environments if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: - _validate_subscription_registered(cmd, "Microsoft.ContainerService") + if (is_valid_resource_id(app_subnet_resource_id)): + parsed_app_subnet_resource_id = parse_resource_id(app_subnet_resource_id) + subnet_subscription = parsed_app_subnet_resource_id["subscription"] + _validate_subscription_registered(cmd, "Microsoft.ContainerService", subnet_subscription) + else: + raise ValidationError('Subnet resource ID is invalid.') 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) From 328683b150e6555209057789a2db5b0ab1ea4d29 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:38:30 -0500 Subject: [PATCH 118/177] GitHub Actions Update (#17) * Added models. Finished transferring Calvin's previous work. * Updated wrong models. * Updated models in custom.py, added githubactionclient. * Updated envelope to be correct. * Small bug fixes. * Updated error handling. Fixed bugs. Initial working state. * Added better error handling. * Added error messages for tokens with inappropriate access rights. * Added back get_acr_cred. * Fixed problems from merge conflict. * Updated names of imports from ._models.py to fix pylance erros. * Removed random imports. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_clients.py | 84 +++++++- .../azext_containerapp/_github_oauth.py | 86 ++++++++ src/containerapp/azext_containerapp/_help.py | 47 +++++ .../azext_containerapp/_models.py | 32 +++ .../azext_containerapp/_params.py | 17 ++ src/containerapp/azext_containerapp/_utils.py | 9 +- .../azext_containerapp/commands.py | 5 + src/containerapp/azext_containerapp/custom.py | 199 +++++++++++++++++- 8 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_github_oauth.py diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 4d525bee181..8184e6d86e2 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -2,8 +2,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- - -from ast import NotEq import json import time import sys @@ -523,3 +521,85 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) 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() + + #TODO + @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 diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py new file mode 100644 index 00000000000..3df73a6b1aa --- /dev/null +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -0,0 +1,86 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.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)) + + raise UnclassifiedUserFault('Activation did not happen in time. Please try again') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index ac9638014c7..33f196f133e 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -244,3 +244,50 @@ text: | az containerapp env list -g MyResourceGroup """ +helps['containerapp github-action add'] = """ + type: command + short-summary: Adds GitHub Actions to the Containerapp + 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 Dockerhub 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: Removes GitHub Actions from the Containerapp + examples: + - name: Removes GitHub Actions, personal access token. + text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp + --token MyAccessToken + - name: Removes 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 Containerapp + examples: + - name: Show the GitHub Actions configuration on a Containerapp + text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp +""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index f0d068b1bbc..6e8947ee58c 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -180,3 +180,35 @@ }, "tags": None } + +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 +} \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c38c32711c4..ac3b640b40e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -103,5 +103,22 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the managed 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 "master" 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 url of the registry, 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', type=str, help='Name of the revision') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0ed9d21bf43..83b707640f5 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,7 +5,8 @@ from distutils.filelist import findall from operator import is_ -from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError) +from azure.cli.core.azclierror import (ResourceNotFoundError, 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 @@ -159,6 +160,12 @@ 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" diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 8fd840ccabd..fed17d21da0 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -56,6 +56,11 @@ def load_command_table(self, _): # g.custom_command('update', 'update_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 github-action') as g: + g.custom_command('add', 'create_or_update_github_action', exception_handler=ex_handler_factory()) + g.custom_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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 3ef91290a5f..bac77b3ab61 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -5,15 +5,19 @@ from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger +from urllib.parse import urlparse + 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 +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient from ._sdk_models import * +from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, VnetConfiguration as VnetConfigurationModel, @@ -27,13 +31,13 @@ Dapr as DaprModel, ContainerResources as ContainerResourcesModel, Scale as ScaleModel, - Container as ContainerModel) + Container as ContainerModel, GitHubActionConfiguration, RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, SourceControl as SourceControlModel) 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_list_of_strings, 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) + _get_app_from_revision, raise_missing_token_suggestion) logger = get_logger(__name__) @@ -908,6 +912,195 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): handle_raw_exception(e) +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, UnknownObjectException + + 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 CLIError("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 CLIError(error_msg) + logger.warning('Verified GitHub repo and branch') + except BadCredentialsException: + raise CLIError("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.") + 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 CLIError(error_msg) + except CLIError as clierror: + raise clierror + except Exception as ex: + # 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 = client.get_source_control_info(resource_group_name, name).properties + 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\"') + 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"] = "master" + + 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 not registry_username or not registry_password: + # 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') + + 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, UnknownObjectException + + 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 CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) + except BadCredentialsException: + raise CLIError("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.") + 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 CLIError(error_msg) + except CLIError as clierror: + raise clierror + except Exception as ex: + # 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) From 0f582e030ed2cb206698cb2a8a1b67beeb74d19b Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 7 Mar 2022 16:03:33 -0800 Subject: [PATCH 119/177] Remove --location since location must be same as managed env --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/custom.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ac3b640b40e..c6d27b2d97f 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -26,7 +26,7 @@ def load_arguments(self, _): 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', '-e'], help="Name or resource ID of the containerapp's environment.") + c.argument('managed_env', validator=validate_managed_env_name_or_id, options_list=['--environment'], help="Name or resource ID of the containerapp's environment.") c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') # Container diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bac77b3ab61..1ba0da1c6ef 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -312,15 +312,11 @@ def create_containerapp(cmd, dapr_app_protocol=None, # dapr_components=None, revision_suffix=None, - location=None, startup_command=None, args=None, 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", "containerApps") if yaml: if image or managed_env or min_replicas or max_replicas or target_port or ingress or\ @@ -350,13 +346,8 @@ def create_containerapp(cmd, if not managed_env_info: raise ValidationError("The environment '{}' does not exist. Specify a valid environment".format(managed_env)) - if not location: - location = managed_env_info["location"] - elif location.lower() != managed_env_info["location"].lower(): - raise ValidationError("The location \"{}\" of the containerapp must be the same as the Managed Environment location \"{}\"".format( - location, - managed_env_info["location"] - )) + location = managed_env_info["location"] + _ensure_location_allowed(cmd, location, "Microsoft.App", "containerApps") external_ingress = None if ingress is not None: From d4272d8c3c07b091b431a7a313d7ff55b1050520 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:28:52 -0800 Subject: [PATCH 120/177] Add options for flag names: --env-vars and --registry-srever --- src/containerapp/azext_containerapp/_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c6d27b2d97f..d5c9428d0c6 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -56,7 +56,7 @@ def load_arguments(self, _): # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-login-server'], help="The url of the registry, e.g. myregistry.azurecr.io") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server', '--registry-login-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") From 42519dc238e4872d84d8043716da4658d9cb8f47 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:29:50 -0800 Subject: [PATCH 121/177] Empty string to clear env_vars --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 4 ++-- src/containerapp/azext_containerapp/custom.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index d5c9428d0c6..6bec838fb93 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 83b707640f5..16cac247433 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -73,8 +73,8 @@ def parse_env_var_flags(env_list, is_update_containerapp=False): key_val = pair.split('=', 1) if len(key_val) != 2: if is_update_containerapp: - raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\". If you are updating a Containerapp, did you pass in the flag \"--environment\"? Updating a containerapp environment is not supported, please re-run the command without this flag.") - raise ValidationError("Environment variables must be in the format \"=,=secretref:,...\".") + 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:') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1ba0da1c6ef..45c557eb026 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -496,7 +496,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['container'] = image or image_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -523,9 +523,12 @@ def update_containerapp(cmd, if image is not None: c["image"] = image if env_vars is not None: - if "env" not in c or not c["env"]: + if isinstance(env_vars, list) and not env_vars: c["env"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + else: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) if startup_command is not None: if isinstance(startup_command, list) and not startup_command: c["command"] = None From 8caebc7dac2ebe7fc9ba359fbc30a6a0078717ad Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 09:51:49 -0800 Subject: [PATCH 122/177] Default revisions_mode to single --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 45c557eb026..bdc83fdf2a6 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -298,7 +298,7 @@ def create_containerapp(cmd, target_port=None, transport="auto", ingress=None, - revisions_mode=None, + revisions_mode="single", secrets=None, env_vars=None, cpu=None, From 11e7fe0d2b690ce6afe067aa497f92337a4b7688 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 14:17:14 -0800 Subject: [PATCH 123/177] Infer acr credentials if it is acr and credentials are not provided --- src/containerapp/azext_containerapp/_utils.py | 17 ++++++++++++++++- .../azext_containerapp/_validators.py | 11 ++++++----- src/containerapp/azext_containerapp/custom.py | 13 +++++++++++-- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 16cac247433..02b436597c8 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,8 +5,8 @@ from distutils.filelist import findall from operator import is_ +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (ResourceNotFoundError, 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 @@ -444,3 +444,18 @@ def _get_app_from_revision(revision): 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 url is required if using Azure Container Registry, otherwise Registry username and password are required.') + 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)) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 23ed260e360..c95d675cb00 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -52,19 +52,20 @@ 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: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if ".azurecr.io" not in namespace.registry_server: + raise ValidationError("Usage error: --registry-login-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: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if not namespace.registry_server or (not namespace.registry_pass and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-login-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_user or not namespace.registry_server: - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required together") + if not namespace.registry_server or (not namespace.registry_user and ".azurecr.io" not in namespace.registry_server): + raise ValidationError("Usage error: --registry-login-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(): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bdc83fdf2a6..5da806288fb 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (RequiredArgumentMissingError, ResourceNotFoundError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger @@ -37,7 +37,7 @@ _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) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials) logger = get_logger(__name__) @@ -370,6 +370,11 @@ def create_containerapp(cmd, registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel + + # Infer credentials if not supplied and its azurecr + if not registry_user or not registry_pass: + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + registries_def["server"] = registry_server registries_def["username"] = registry_user @@ -648,6 +653,10 @@ def update_containerapp(cmd, if not registry_server: raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + # Infer credentials if not supplied and its azurecr + if not registry_user or not registry_pass: + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + # Check if updating existing registry updating_existing_registry = False for r in registries_def: From 85fd0f5e146d26efaadf9a519966bf6ad582875f Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 14:49:36 -0800 Subject: [PATCH 124/177] fix help msg --- src/containerapp/azext_containerapp/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 33f196f133e..f4d7713ce93 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -90,7 +90,7 @@ --registry-password mypassword - name: Update a Containerapp using a specified startup command and arguments text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ + az containerapp update -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage \\ --command "/bin/sh" --args "-c", "while true; do echo hello; sleep 10;done" From 6bf5a560e0a48c50c4ad6077f6f0b6793b765ae1 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 8 Mar 2022 17:25:47 -0800 Subject: [PATCH 125/177] if image is hosted on acr, and no registry server is supplied, infer the registry server --- src/containerapp/azext_containerapp/custom.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5da806288fb..d346cc75f65 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -367,6 +367,11 @@ def create_containerapp(cmd, if secrets is not None: secrets_def = parse_secret_flags(secrets) + # If ACR image and registry_server is not supplied, infer it + if image and '.azurecr.io' in image: + if not registry_server: + registry_server = image.split('/')[0] + registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel @@ -496,6 +501,11 @@ def update_containerapp(cmd, if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) + # If ACR image and registry_server is not supplied, infer it + if image and '.azurecr.io' in image: + if not registry_server: + registry_server = image.split('/')[0] + update_map = {} update_map['secrets'] = secrets is not None update_map['ingress'] = ingress or target_port or transport or traffic_weights From 25e125087723058a634ae66675acafce508c5f77 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Fri, 11 Mar 2022 13:12:52 -0500 Subject: [PATCH 126/177] Added subgroups (Ingress, Registry, Secret) and updated revisions (#18) * Added ingress subgroup. * Added help for ingress. * Fixed ingress traffic help. * Added registry commands. * Updated registry remove util to clear secrets if none remaining. Added warning when updating existing registry. Added registry help. * Changed registry delete to remove. * Added error message if user tries to remove non assigned registry. * Changed registry add back to registry set. * Added secret subgroup commands. * Removed yaml support from secret set. * Changed secret add to secret set. Updated consistency between secret set and secret delete. Added secret help. Require at least one secret passed with --secrets for secret commands. * Changed param name for secret delete from --secrets to --secret-names. Updated help. * Changed registry remove to registry delete. * Fixed bug in registry delete. * Added revision mode set and revision copy. * Modified update_containerapp_yaml to support updating from non-current revision. Authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/_help.py | 171 +++++ .../azext_containerapp/_models.py | 2 +- .../azext_containerapp/_params.py | 20 +- src/containerapp/azext_containerapp/_utils.py | 17 + .../azext_containerapp/commands.py | 25 + src/containerapp/azext_containerapp/custom.py | 599 +++++++++++++++++- 6 files changed, 830 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index f4d7713ce93..6122a3d895a 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -189,6 +189,24 @@ az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup """ +helps['containerapp revision mode set'] = """ + type: command + short-summary: Set the revision mode of a Containerapp. + examples: + - name: Set the revision mode of a Containerapp. + text: | + az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi +""" + # Environment Commands helps['containerapp env'] = """ type: group @@ -244,6 +262,159 @@ text: | az containerapp env list -g MyResourceGroup """ + +# Ingress Commands +helps['containerapp ingress'] = """ + type: group + short-summary: Commands to manage Containerapp ingress. +""" + +helps['containerapp ingress traffic'] = """ + type: subgroup + short-summary: Commands to manage Containerapp ingress traffic. +""" + +helps['containerapp ingress show'] = """ + type: command + short-summary: Show details of a Containerapp ingress. + examples: + - name: Show the details of a Containerapp ingress. + text: | + az containerapp ingress show -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress enable'] = """ + type: command + short-summary: Enable Containerapp ingress. + examples: + - name: Enable Containerapp ingress. + 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 Containerapp ingress. + examples: + - name: Disable Containerapp ingress. + text: | + az containerapp ingress disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp ingress traffic'] = """ + type: group + short-summary: Commands to manage Containerapp ingress traffic. +""" + +helps['containerapp ingress traffic set'] = """ + type: command + short-summary: Set Containerapp ingress traffic. + examples: + - name: Set Containerapp ingress traffic. + text: | + az containerapp ingress traffic set -n MyContainerapp -g MyResourceGroup --traffic-weight latest=100 +""" + +helps['containerapp ingress traffic show'] = """ + type: command + short-summary: Show Containerapp ingress traffic. + examples: + - name: Show Containerapp ingress traffic. + text: | + az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup +""" + +# Registry Commands +helps['containerapp registry'] = """ + type: group + short-summary: Commands to manage Containerapp registries. +""" + +helps['containerapp registry show'] = """ + type: command + short-summary: Show details of a Containerapp registry. + examples: + - name: Show the details of a Containerapp registry. + text: | + az containerapp registry show -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +helps['containerapp registry list'] = """ + type: command + short-summary: List registries assigned to a Containerapp. + examples: + - name: Show the details of a Containerapp registry. + text: | + az containerapp registry list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp registry set'] = """ + type: command + short-summary: Add or update a Containerapp registry. + examples: + - name: Add a registry to a Containerapp. + text: | + az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + - name: Update a Containerapp registry. + text: | + az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword + +""" + +helps['containerapp registry delete'] = """ + type: command + short-summary: Delete a registry from a Containerapp. + examples: + - name: Delete a registry from a Containerapp. + text: | + az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io +""" + +# Secret Commands +helps['containerapp secret'] = """ + type: group + short-summary: Commands to manage Containerapp secrets. +""" + +helps['containerapp secret show'] = """ + type: command + short-summary: Show details of a Containerapp secret. + examples: + - name: Show the details of a Containerapp 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 Containerapp. + examples: + - name: List the secrets of a Containerapp. + text: | + az containerapp secret list -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp secret delete'] = """ + type: command + short-summary: Delete secrets from a Containerapp. + examples: + - name: Delete secrets from a Containerapp. + text: | + az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 +""" + +helps['containerapp secret set'] = """ + type: command + short-summary: Create/update Containerapp secrets. + examples: + - name: Add a secret to a Containerapp. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName=MySecretValue + - name: Update a Containerapp secret. + text: | + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue +""" + helps['containerapp github-action add'] = """ type: command short-summary: Adds GitHub Actions to the Containerapp diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 6e8947ee58c..6440c677635 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -137,7 +137,7 @@ "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight "customDomains": None, # [CustomDomain] - # "allowInsecure": None + "allowInsecure": None # Boolean } RegistryCredentials = { diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 6bec838fb93..545f6b8d05a 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -121,4 +121,22 @@ def load_arguments(self, _): 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', type=str, help='Name of the revision') + 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="Ingress type that allows either internal or external traffic to the Containerapp.") + 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 Containerapp. 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 containerapp. Space-separated values in 'key=value' format.") + + with self.argument_context('containerapp secret delete') as c: + c.argument('secret_names', nargs='+', help="A list of secret(s) for the containerapp. Space-separated secret values names.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 02b436597c8..a4e11f220fd 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -300,6 +300,23 @@ def _add_or_update_secrets(containerapp_def, add_secrets): 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 i in range(0, len(containerapp_def["properties"]["configuration"]["secrets"])): + existing_secret = containerapp_def["properties"]["configuration"]["secrets"][i] + if existing_secret["name"].lower() == secret_name.lower(): + containerapp_def["properties"]["configuration"]["secrets"].pop(i) + break def _add_or_update_env_vars(existing_env_vars, new_env_vars): for new_env_var in new_env_vars: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index fed17d21da0..2ea2e48b04c 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -67,3 +67,28 @@ def load_command_table(self, _): 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_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()) + + with self.command_group('containerapp revision mode') as g: + g.custom_command('set', '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_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_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_command('show', 'show_registry') + g.custom_command('list', 'list_registry') + g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) + + with self.command_group('containerapp secret') as g: + g.custom_command('list', 'list_secrets') + g.custom_command('show', 'show_secret') + g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) + g.custom_command('set', 'set_secrets', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d346cc75f65..6908f9d6371 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -13,6 +13,8 @@ from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError +from azure.cli.command_modules.appservice.custom import _get_acr_cred +from urllib.parse import urlparse from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient @@ -37,7 +39,7 @@ _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) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret) logger = get_logger(__name__) @@ -85,7 +87,7 @@ def create_deserializer(): return Deserializer(deserializer) -def update_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait=False): +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: raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') @@ -112,6 +114,14 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= 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) + 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() @@ -1159,3 +1169,588 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): except CLIError as e: handle_raw_exception(e) +def copy_revision(cmd, + name, + resource_group_name, + from_revision=None, + #label=None, + yaml=None, + image=None, + image_name=None, + min_replicas=None, + max_replicas=None, + env_vars=None, + cpu=None, + memory=None, + revision_suffix=None, + startup_command=None, + traffic_weights=None, + args=None, + tags=None, + no_wait=False): + _validate_subscription_registered(cmd, "Microsoft.App") + + if not from_revision: + from_revision = containerapp_def["properties"]["latestRevisionName"] + + if yaml: + if image or min_replicas or max_replicas or\ + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + 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) + + containerapp_def["properties"]["template"] = r["properties"]["template"] + + update_map = {} + update_map['ingress'] = traffic_weights + update_map['scale'] = min_replicas or max_replicas + update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['configuration'] = update_map['ingress'] + + 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 image_name: + if len(containerapp_def["properties"]["template"]["containers"]) == 1: + image_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() == image_name.lower(): + updating_existing_container = True + + if image is not None: + c["image"] = image + if env_vars is not None: + if "env" not in c or not c["env"]: + c["env"] = [] + _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + 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"] = image_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: + 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 + + # Configuration + if update_map["ingress"]: + if "ingress" not in containerapp_def["properties"]["configuration"]: + containerapp_def["properties"]["configuration"]["ingress"] = {} + + 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) + + 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 CLIError("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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"] + except: + raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) + +def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, allow_insecure=False, 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 CLIError("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) + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + containerapp_def["properties"]["configuration"]["ingress"] = None + + _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("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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["ingress"] + except: + raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") + + 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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] + except: + raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") + +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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + for r in registries_def: + if r['server'].lower() == server.lower(): + return r + raise CLIError("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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + +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 CLIError("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') + + # 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) + # Should this be false? ^ + + 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 delete_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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + registries_def = None + registry = None + + try: + containerapp_def["properties"]["configuration"]["registries"] + except: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + wasRemoved = False + for i in range(0, len(registries_def)): + r = registries_def[i] + 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 CLIError("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 as e: + return + +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 CLIError("The containerapp '{}' does not exist".format(name)) + + try: + return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] + except: + raise CLIError("The containerapp {} has no assigned secrets.".format(name)) + +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 CLIError("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 CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + +def delete_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 CLIError("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 CLIError("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, + #secrets=None, + #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 CLIError("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 CLIError("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) + + From 22e428cbbe879bdab85a56fe6e3a7192f5d4e310 Mon Sep 17 00:00:00 2001 From: Calvin Date: Fri, 11 Mar 2022 10:13:37 -0800 Subject: [PATCH 127/177] More p0 fixes (#20) * Remove --registry-login-server, only allow --registry-server * Rename --environment-variables to --env-vars * If no image is supplied, use default quickstart image --- src/containerapp/azext_containerapp/_help.py | 8 ++++---- src/containerapp/azext_containerapp/_params.py | 4 ++-- src/containerapp/azext_containerapp/_validators.py | 6 +++--- src/containerapp/azext_containerapp/custom.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 6122a3d895a..0720d816793 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -26,7 +26,7 @@ az containerapp create -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage -e MyContainerappEnv \\ --secrets mysecret=escapefromtarkov,anothersecret=isadifficultgame \\ - --environment-variables myenvvar=foo,anotherenvvar=bar \\ + --env-vars myenvvar=foo,anotherenvvar=bar \\ --query properties.configuration.ingress.fqdn - name: Create a Containerapp that only accepts internal traffic text: | @@ -39,7 +39,7 @@ az containerapp create -n MyContainerapp -g MyResourceGroup \\ --image MyContainerImage -e MyContainerappEnv \\ --secrets mypassword=verysecurepassword \\ - --registry-login-server MyRegistryServerAddress \\ + --registry-server MyRegistryServerAddress \\ --registry-username MyUser \\ --registry-password mypassword \\ --query properties.configuration.ingress.fqdn @@ -75,7 +75,7 @@ text: | az containerapp update -n MyContainerapp -g MyResourceGroup \\ --secrets mysecret=secretfoo,anothersecret=secretbar - --environment-variables myenvvar=foo,anotherenvvar=secretref:mysecretname + --env-vars myenvvar=foo,anotherenvvar=secretref:mysecretname - name: Update a Containerapp's ingress setting to internal text: | az containerapp update -n MyContainerapp -g MyResourceGroup \\ @@ -85,7 +85,7 @@ az containerapp update -n MyContainerapp -g MyResourceGroup \\ --image MyNewContainerImage \\ --secrets mypassword=verysecurepassword \\ - --registry-login-server MyRegistryServerAddress \\ + --registry-server MyRegistryServerAddress \\ --registry-username MyUser \\ --registry-password mypassword - name: Update a Containerapp using a specified startup command and arguments diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 545f6b8d05a..e6fb2908f67 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars', '--environment-variables'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") + c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -56,7 +56,7 @@ def load_arguments(self, _): # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server', '--registry-login-server'], help="The url of the registry, e.g. myregistry.azurecr.io") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index c95d675cb00..916d9eb5b57 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -53,19 +53,19 @@ def validate_registry_server(namespace): 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-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + 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-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + 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-login-server, --registry-password and --registry-username are required together if not using Azure Container Registry") + 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(): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 6908f9d6371..f814103c875 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -336,8 +336,8 @@ def create_containerapp(cmd, 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 image is None: - raise RequiredArgumentMissingError('Usage error: --image is required if not using --yaml') + 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') @@ -671,7 +671,7 @@ def update_containerapp(cmd, registries_def = containerapp_def["properties"]["configuration"]["registries"] if not registry_server: - raise ValidationError("Usage error: --registry-login-server is required when adding or updating a registry") + raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") # Infer credentials if not supplied and its azurecr if not registry_user or not registry_pass: @@ -696,7 +696,7 @@ def update_containerapp(cmd, # If not updating existing registry, add as new registry if not updating_existing_registry: if not(registry_server is not None and registry_user is not None and registry_pass is not None): - raise ValidationError("Usage error: --registry-login-server, --registry-password and --registry-username are required when adding a registry") + raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required when adding a registry") registry = RegistryCredentialsModel registry["server"] = registry_server From e12b19cb40da162ca36d7eef09cc6993ec6c492f Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Mon, 14 Mar 2022 09:01:49 -0700 Subject: [PATCH 128/177] Update help text (#21) * Update help text * Update punctuation * master -> main --- src/containerapp/azext_containerapp/_help.py | 267 ++++++++---------- .../azext_containerapp/_params.py | 58 ++-- src/containerapp/azext_containerapp/custom.py | 2 +- 3 files changed, 143 insertions(+), 184 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 0720d816793..4f6fd755cc7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -9,92 +9,47 @@ helps['containerapp'] = """ type: group - short-summary: Commands to manage Containerapps. + short-summary: Commands to manage Azure Container Apps. """ helps['containerapp create'] = """ type: command - short-summary: Create a Containerapp. + short-summary: Create a container app. examples: - - name: Create a Containerapp + - name: Create a container app and retrieve its fully qualified domain name. text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ + --image myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --ingress external --target-port 80 \\ --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with secrets and environment variables + - name: Create a container app with resource requirements and replica count limits. text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --secrets mysecret=escapefromtarkov,anothersecret=isadifficultgame \\ - --env-vars myenvvar=foo,anotherenvvar=bar \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp that only accepts internal traffic - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --ingress internal \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp using an image from a private registry - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --secrets mypassword=verysecurepassword \\ - --registry-server MyRegistryServerAddress \\ - --registry-username MyUser \\ - --registry-password mypassword \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with a specified startup command and arguments - text: | - az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage -e MyContainerappEnv \\ - --command "/bin/sh" \\ - --args "-c", "while true; do echo hello; sleep 10;done" \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp with a minimum resource and replica requirements + --image myregistry.azurecr.io/my-app:v1.0 --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 MyContainerImage -e MyContainerappEnv \\ - --cpu 0.5 --memory 1.0Gi \\ - --min-replicas 4 --max-replicas 8 \\ - --query properties.configuration.ingress.fqdn - - name: Create a Containerapp using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples + --image myregistry.azurecr.io/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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ - --yaml "C:/path/to/yaml/file.yml" + --environment MyContainerappEnv \\ + --yaml "path/to/yaml/file.yml" """ helps['containerapp update'] = """ type: command - short-summary: Update a Containerapp. + short-summary: Update a container app. In multiple revisions mode, create a new revision based on the latest revision. examples: - - name: Update a Containerapp's container image - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --image MyNewContainerImage - - name: Update a Containerapp with secrets and environment variables - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --secrets mysecret=secretfoo,anothersecret=secretbar - --env-vars myenvvar=foo,anotherenvvar=secretref:mysecretname - - name: Update a Containerapp's ingress setting to internal + - name: Update a container app's container image. text: | az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --ingress internal - - name: Update a Containerapp using an image from a private registry - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --image MyNewContainerImage \\ - --secrets mypassword=verysecurepassword \\ - --registry-server MyRegistryServerAddress \\ - --registry-username MyUser \\ - --registry-password mypassword - - name: Update a Containerapp using a specified startup command and arguments - text: | - az containerapp update -n MyContainerapp -g MyResourceGroup \\ - --image MyContainerImage \\ - --command "/bin/sh" - --args "-c", "while true; do echo hello; sleep 10;done" - - name: Update a Containerapp with a minimum resource and replica requirements + --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 \\ @@ -103,37 +58,37 @@ helps['containerapp delete'] = """ type: command - short-summary: Delete a Containerapp. + short-summary: Delete a container app. examples: - - name: Delete a Containerapp. + - name: Delete a container app. text: az containerapp delete -g MyResourceGroup -n MyContainerapp """ helps['containerapp scale'] = """ type: command - short-summary: Set the min and max replicas for a Containerapp. + short-summary: Set the min and max replicas for a container app (latest revision in multiple revisions mode). examples: - - name: Scale a Containerapp. + - name: Scale a container's latest revision. text: az containerapp scale -g MyResourceGroup -n MyContainerapp --min-replicas 1 --max-replicas 2 """ helps['containerapp show'] = """ type: command - short-summary: Show details of a Containerapp. + short-summary: Show details of a container app. examples: - - name: Show the details of a Containerapp. + - name: Show the details of a container app. text: | az containerapp show -n MyContainerapp -g MyResourceGroup """ helps['containerapp list'] = """ type: command - short-summary: List Containerapps. + short-summary: List container apps. examples: - - name: List Containerapps by subscription. + - name: List container apps in the current subscription. text: | az containerapp list - - name: List Containerapps by resource group. + - name: List container apps by resource group. text: | az containerapp list -g MyResourceGroup """ @@ -141,61 +96,62 @@ # Revision Commands helps['containerapp revision'] = """ type: group - short-summary: Commands to manage a Containerapp's revisions. + short-summary: Commands to manage revisions. """ helps['containerapp revision show'] = """ type: command - short-summary: Show details of a Containerapp's revision. + short-summary: Show details of a revision. examples: - - name: Show details of a Containerapp's revision. + - name: Show details of a revision. text: | - az containerapp revision show --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision show -n MyContainerapp -g MyResourceGroup \\ + --revision-name MyContainerappRevision """ helps['containerapp revision list'] = """ type: command - short-summary: List details of a Containerapp's revisions. + short-summary: List a container app's revisions. examples: - - name: List a Containerapp's revisions. + - name: List a container app's revisions. text: | - az containerapp revision list --revision-name MyContainerapp -g MyResourceGroup + az containerapp revision list -n MyContainerapp -g MyResourceGroup """ helps['containerapp revision restart'] = """ type: command - short-summary: Restart a Containerapps's revision. + short-summary: Restart a revision. examples: - - name: Restart a Containerapp's revision. + - name: Restart a revision. text: | - az containerapp revision restart --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ helps['containerapp revision activate'] = """ type: command - short-summary: Activates Containerapp's revision. + short-summary: Activate a revision. examples: - - name: Activate a Containerapp's revision. + - name: Activate a revision. text: | - az containerapp revision activate --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision activate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ helps['containerapp revision deactivate'] = """ type: command - short-summary: Deactivates Containerapp's revision. + short-summary: Deactivate a revision. examples: - - name: Deactivate a Containerapp's revision. + - name: Deactivate a revision. text: | - az containerapp revision deactivate --revision-name MyContainerappRevision -g MyResourceGroup + az containerapp revision deactivate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ helps['containerapp revision mode set'] = """ type: command - short-summary: Set the revision mode of a Containerapp. + short-summary: Set the revision mode of a container app. examples: - - name: Set the revision mode of a Containerapp. + - name: Set a container app to single revision mode. text: | - az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup + az containerapp revision mode set-n MyContainerapp -g MyResourceGroup --mode Single """ helps['containerapp revision copy'] = """ @@ -204,61 +160,62 @@ examples: - name: Create a revision based on a previous revision. text: | - az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi + az containerapp revision copy -n MyContainerapp -g MyResourceGroup \\ + --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ # Environment Commands helps['containerapp env'] = """ type: group - short-summary: Commands to manage Containerapp environments. + short-summary: Commands to manage Container Apps environments. """ helps['containerapp env create'] = """ type: command - short-summary: Create a Containerapp environment. + short-summary: Create a Container Apps environment. examples: - - name: Create a Containerapp Environment with an autogenerated Log Analytics + - 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 a Containerapp Environment with Log Analytics + --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 + --location "Canada Central" """ helps['containerapp env update'] = """ type: command - short-summary: Update a Containerapp environment. Currently Unsupported. + short-summary: Update a Container Apps environment. Currently Unsupported. """ helps['containerapp env delete'] = """ type: command - short-summary: Delete a Containerapp Environment. + short-summary: Delete a Container Apps environment. examples: - - name: Delete Containerapp Environment. - text: az containerapp env delete -g MyResourceGroup -n MyContainerappEnvironment + - 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 Containerapp environment. + short-summary: Show details of a Container Apps environment. examples: - - name: Show the details of a Containerapp Environment. + - 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 Containerapp environments by subscription or resource group. + short-summary: List Container Apps environments by subscription or resource group. examples: - - name: List Containerapp Environments by subscription. + - name: List environments in the current subscription. text: | az containerapp env list - - name: List Containerapp Environments by resource group. + - name: List environments by resource group. text: | az containerapp env list -g MyResourceGroup """ @@ -266,60 +223,64 @@ # Ingress Commands helps['containerapp ingress'] = """ type: group - short-summary: Commands to manage Containerapp ingress. + short-summary: Commands to manage ingress and traffic-splitting. """ helps['containerapp ingress traffic'] = """ type: subgroup - short-summary: Commands to manage Containerapp ingress traffic. + short-summary: Commands to manage traffic-splitting. """ helps['containerapp ingress show'] = """ type: command - short-summary: Show details of a Containerapp ingress. + short-summary: Show details of a container app's ingress. examples: - - name: Show the details of a Containerapp ingress. + - 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 Containerapp ingress. + short-summary: Enable ingress for a container app. examples: - - name: Enable Containerapp ingress. + - 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 + 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 Containerapp ingress. + short-summary: Disable ingress for a container app. examples: - - name: Disable Containerapp ingress. + - 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 Containerapp ingress traffic. + short-summary: Commands to manage traffic-splitting. """ helps['containerapp ingress traffic set'] = """ type: command - short-summary: Set Containerapp ingress traffic. + short-summary: Configure traffic-splitting for a container app. examples: - - name: Set Containerapp ingress traffic. + - 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 Containerapp ingress traffic. + short-summary: Show traffic-splitting configuration for a container app. examples: - - name: Show Containerapp ingress traffic. + - name: Show a container app's ingress traffic configuration. text: | az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup """ @@ -327,45 +288,43 @@ # Registry Commands helps['containerapp registry'] = """ type: group - short-summary: Commands to manage Containerapp registries. + short-summary: Commands to manage container registry information. """ helps['containerapp registry show'] = """ type: command - short-summary: Show details of a Containerapp registry. + short-summary: Show details of a container registry. examples: - - name: Show the details of a Containerapp registry. + - 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 registries assigned to a Containerapp. + short-summary: List container registries configured in a container app. examples: - - name: Show the details of a Containerapp registry. + - 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 Containerapp registry. + short-summary: Add or update a container registry's details. examples: - - name: Add a registry to a Containerapp. - text: | - az containerapp registry set -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io - - name: Update a Containerapp registry. + - 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 + az containerapp registry set -n MyContainerapp -g MyResourceGroup \\ + --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword """ helps['containerapp registry delete'] = """ type: command - short-summary: Delete a registry from a Containerapp. + short-summary: Remove a container registry's details. examples: - - name: Delete a registry from a Containerapp. + - name: Remove a registry from a Containerapp. text: | az containerapp registry delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ @@ -373,51 +332,51 @@ # Secret Commands helps['containerapp secret'] = """ type: group - short-summary: Commands to manage Containerapp secrets. + short-summary: Commands to manage secrets. """ helps['containerapp secret show'] = """ type: command - short-summary: Show details of a Containerapp secret. + short-summary: Show details of a secret. examples: - - name: Show the details of a Containerapp secret. + - 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 Containerapp. + short-summary: List the secrets of a container app. examples: - - name: List the secrets of a Containerapp. + - name: List the secrets of a container app. text: | az containerapp secret list -n MyContainerapp -g MyResourceGroup """ helps['containerapp secret delete'] = """ type: command - short-summary: Delete secrets from a Containerapp. + short-summary: Delete secrets from a container app. examples: - - name: Delete secrets from a Containerapp. + - name: Delete secrets from a container app. text: | az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 """ helps['containerapp secret set'] = """ type: command - short-summary: Create/update Containerapp secrets. + short-summary: Create/update secrets. examples: - - name: Add a secret to a Containerapp. + - name: Add secrets to a container app. text: | - az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName=MySecretValue - - name: Update a Containerapp secret. + 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 add'] = """ type: command - short-summary: Adds GitHub Actions to the Containerapp + 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 @@ -433,7 +392,7 @@ --service-principal-tenant-id 00000000-0000-0000-0000-00000000 --service-principal-client-secret ClientSecret --login-with-github - - name: Add GitHub Actions, using Dockerhub and log in to GitHub flow to retrieve personal access token. + - 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 @@ -445,20 +404,20 @@ helps['containerapp github-action delete'] = """ type: command - short-summary: Removes GitHub Actions from the Containerapp + short-summary: Remove a previously configured Container Apps GitHub Actions workflow from a repository. examples: - - name: Removes GitHub Actions, personal access token. + - name: Remove GitHub Actions using a personal access token. text: az containerapp github-action delete -g MyResourceGroup -n MyContainerapp --token MyAccessToken - - name: Removes GitHub Actions, using log in to GitHub flow to retrieve personal access token. + - 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 Containerapp + short-summary: Show the GitHub Actions configuration on a container app. examples: - - name: Show the GitHub Actions configuration on a Containerapp + - name: Show the GitHub Actions configuration on a Containerapp. text: az containerapp github-action show -g MyResourceGroup -n MyContainerapp """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index e6fb2908f67..b15851f2b66 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -26,54 +26,54 @@ def load_arguments(self, _): 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 containerapp's environment.") - c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a containerapp. All other parameters will be ignored') + 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 (Creates new revision)') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") - c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the Container image.") + c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the containerapp. Space-separated values in 'key=value' format. Empty string to clear existing values") - c.argument('startup_command', nargs='*', options_list=['--command'], help="A list of supported commands on the container app that will executed during container startup. Space-separated values e.g. \"/bin/queue\" \"mycommand\". Empty string to clear existing values") + c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") + 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='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') # Scale with self.argument_context('containerapp', arg_group='Scale (Creates new revision)') as c: - c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") # Dapr - with self.argument_context('containerapp', arg_group='Dapr (Creates new revision)') as c: + 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()) - c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="Tells Dapr the port your application is listening on.") + c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="The port Dapr uses to talk to the application.") c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") - c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="Tells Dapr which protocol your application is using.") + c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="The protocol Dapr uses to talk to the application.") c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: - c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the containerapp.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server'], help="The url of the registry, 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 container image registry server. 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 container image registry server") - c.argument('secrets', nargs='*', options_list=['--secrets', '-s'], help="A list of secret(s) for the containerapp. Space-separated values in 'key=value' format.") + c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the container app.") + c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--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, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="Ingress type that allows either internal or external traffic to the Containerapp.") + c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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.") - c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + 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, options_list=['--min-replicas'], help="The minimum number of containerapp replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of containerapp replicas.") + c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") + c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") with self.argument_context('containerapp env') as c: - c.argument('name', name_type, help='Name of the containerapp environment') + 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) @@ -94,21 +94,21 @@ def load_arguments(self, _): 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 managed environment.') + 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 managed Environment.') + 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 managed Environment.') + 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 "master" if not specified.') + 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 url of the registry, e.g. myregistry.azurecr.io') + 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') @@ -128,15 +128,15 @@ def load_arguments(self, _): 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="Ingress type that allows either internal or external traffic to the Containerapp.") + 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 Containerapp. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + 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 containerapp. Space-separated values in 'key=value' format.") + 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 delete') as c: - c.argument('secret_names', nargs='+', help="A list of secret(s) for the containerapp. Space-separated secret values names.") + c.argument('secret_names', nargs='+', help="A list of secret(s) for the container app. Space-separated secret values names.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f814103c875..c3277dad616 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1013,7 +1013,7 @@ def create_or_update_github_action(cmd, if branch: source_control_info["properties"]["branch"] = branch if not source_control_info["properties"]["branch"]: - source_control_info["properties"]["branch"] = "master" + source_control_info["properties"]["branch"] = "main" azure_credentials = None From abece414b33290648411962956f82f6ea5006f51 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 12:00:12 -0700 Subject: [PATCH 129/177] New 1.0.1 version --- src/containerapp/HISTORY.rst | 6 ++++++ src/containerapp/setup.py | 2 +- src/index.json | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 8c34bccfff8..f58df889075 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.1.1 +++++++ +* Various fixes for az containerapp create, update +* Added github actions support +* Added subgroups for ingress, registry, revision, secret + 0.1.0 ++++++ * Initial release. \ No newline at end of file diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index be4cd26f637..fa1b93b7448 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.0' +VERSION = '0.1.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index 99a9d4d7594..44e9bca1d09 100644 --- a/src/index.json +++ b/src/index.json @@ -12324,6 +12324,48 @@ "sha256Digest": "9a796d5187571990d27feb9efeedde38c194f13ea21cbf9ec06131196bfd821d" } ], + "containerapp": [ + { + "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.1-py2.py3-none-any.whl", + "filename": "containerapp-0.1.1-py2.py3-none-any.whl", + "metadata": { + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "extensions": { + "python.details": { + "contacts": [ + { + "email": "azpycli@microsoft.com", + "name": "Microsoft Corporation", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azure-cli-extensions" + } + } + }, + "extras": [], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "containerapp", + "run_requires": [ + { + "requires": [ + "azure-cli-core" + ] + } + ], + "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", + "version": "0.1.1" + }, + "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" + } + ], "cosmosdb-preview": [ { "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/cosmosdb_preview-0.1.0-py2.py3-none-any.whl", From 7d8b9ba5f8ee0ae612f938dd678bc3699937ba45 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 14 Mar 2022 17:00:17 -0400 Subject: [PATCH 130/177] Added identity commands + --assign-identity flag to containerapp create (#8) * Added identity show and assign. * Finisheed identity remove. * Added helps, updated identity remove to work with identity names instead of requiring identity resource ids. * Moved helper function to utils. * Require --identities flag when removing identities. * Added message for assign identity with no specified identity. * Added --assign-identity flag to containerapp create. * Moved assign-identity flag to containerapp create. * Fixed small logic error on remove identities when passing duplicate identities. Added warnings for certain edge cases. * Updated param definition for identity assign --identity default. * Added identity examples in help. * Made sure secrets were not removed when assigning identities. Added tolerance for [system] passed with capital letters. * Fixed error from merge. Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/_help.py | 36 ++++ .../azext_containerapp/_params.py | 9 + src/containerapp/azext_containerapp/_utils.py | 10 + .../azext_containerapp/commands.py | 7 + src/containerapp/azext_containerapp/custom.py | 202 +++++++++++++++++- 5 files changed, 261 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 4f6fd755cc7..724335e8711 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -220,6 +220,42 @@ az containerapp env list -g MyResourceGroup """ +# Identity Commands +helps['containerapp identity'] = """ + type: group + short-summary: Manage service (managed) identities for a containerapp +""" + +helps['containerapp identity assign'] = """ + type: command + short-summary: Assign a managed identity to a containerapp + long-summary: Managed identities can be user-assigned or system-assigned + examples: + - name: Assign system identity. + text: | + az containerapp identity assign + - name: Assign system and user identity. + text: | + az containerapp identity assign --identities [system] myAssignedId +""" + +helps['containerapp identity remove'] = """ + type: command + short-summary: Remove a managed identity from a containerapp + examples: + - name: Remove system identity. + text: | + az containerapp identity remove [system] + - name: Remove system and user identity. + text: | + az containerapp identity remove --identities [system] myAssignedId +""" + +helps['containerapp identity show'] = """ + type: command + short-summary: Show the containerapp's identity details +""" + # Ingress Commands helps['containerapp ingress'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index b15851f2b66..8435659f4d0 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -66,6 +66,9 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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('assign_identity', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity.") 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: @@ -103,6 +106,12 @@ def load_arguments(self, _): with self.argument_context('containerapp env show') as c: c.argument('name', name_type, help='Name of the Container Apps Environment.') + with self.argument_context('containerapp identity') as c: + c.argument('identities', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity.") + + with self.argument_context('containerapp identity assign') as c: + c.argument('identities', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity. Default is '[system]'.") + 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') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index a4e11f220fd..54994a71578 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -284,6 +284,16 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): 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"]: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 2ea2e48b04c..9a83db9df7e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -56,6 +56,13 @@ def load_command_table(self, _): # g.custom_command('update', 'update_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 identity') as g: + g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('show', 'show_managed_identity') + + 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_command('show', 'show_github_action', exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c3277dad616..db5bdb00db2 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -33,13 +33,19 @@ Dapr as DaprModel, ContainerResources as ContainerResourcesModel, Scale as ScaleModel, - Container as ContainerModel, GitHubActionConfiguration, RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, SourceControl as SourceControlModel) + 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_list_of_strings, 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) + _get_app_from_revision, raise_missing_token_suggestion, _infer_acr_credentials, _remove_registry_secret, _remove_secret, + _ensure_identity_resource_id) logger = get_logger(__name__) @@ -325,7 +331,8 @@ def create_containerapp(cmd, startup_command=None, args=None, tags=None, - no_wait=False): + no_wait=False, + assign_identity=[]): _validate_subscription_registered(cmd, "Microsoft.App") if yaml: @@ -403,6 +410,28 @@ def create_containerapp(cmd, config_def["ingress"] = ingress_def config_def["registries"] = [registries_def] if registries_def is not None else None + # Identity actions + identity_def = ManagedServiceIdentityModel + identity_def["type"] = "None" + + assign_system_identity = '[system]' in assign_identity + assign_user_identities = [x for x in assign_identity if x != '[system]'] + + if assign_system_identity and assign_user_identities: + identity_def["type"] = "SystemAssigned, UserAssigned" + elif assign_system_identity: + identity_def["type"] = "SystemAssigned" + elif assign_user_identities: + identity_def["type"] = "UserAssigned" + + if assign_user_identities: + identity_def["userAssignedIdentities"] = {} + subscription_id = get_subscription_id(cmd.cli_ctx) + + for r in assign_user_identities: + r = _ensure_identity_resource_id(subscription_id, resource_group_name, r) + identity_def["userAssignedIdentities"][r] = {} + scale_def = None if min_replicas is not None or max_replicas is not None: scale_def = ScaleModel @@ -445,6 +474,7 @@ def create_containerapp(cmd, containerapp_def = ContainerAppModel containerapp_def["location"] = location + containerapp_def["identity"] = identity_def containerapp_def["properties"]["managedEnvironmentId"] = managed_env containerapp_def["properties"]["configuration"] = config_def containerapp_def["properties"]["template"] = template_def @@ -935,6 +965,172 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): 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 CLIError("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 CLIError("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 CLIError("The containerapp {} has no system or user assigned identities.".format(name)) + + if remove_system_identity: + if containerapp_def["identity"]["type"] == "UserAssigned": + raise CLIError("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 id in remove_user_identities: + given_id = id + id = _ensure_identity_resource_id(subscription_id, resource_group_name, id) + wasRemoved = False + + for old_user_identity in containerapp_def["identity"]["userAssignedIdentities"]: + if old_user_identity.lower() == id.lower(): + containerapp_def["identity"]["userAssignedIdentities"].pop(old_user_identity) + wasRemoved = True + break + + if not wasRemoved: + raise CLIError("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, From bc8c58b3863d102f83fadfaf2683305d126455ea Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 14 Mar 2022 18:53:35 -0400 Subject: [PATCH 131/177] Dapr Commands (#23) * Added ingress subgroup. * Added help for ingress. * Fixed ingress traffic help. * Added registry commands. * Updated registry remove util to clear secrets if none remaining. Added warning when updating existing registry. Added registry help. * Changed registry delete to remove. * Added error message if user tries to remove non assigned registry. * Changed registry add back to registry set. * Added secret subgroup commands. * Removed yaml support from secret set. * Changed secret add to secret set. Updated consistency between secret set and secret delete. Added secret help. Require at least one secret passed with --secrets for secret commands. * Changed param name for secret delete from --secrets to --secret-names. Updated help. * Changed registry remove to registry delete. * Fixed bug in registry delete. * Added revision mode set and revision copy. * Added dapr enable and dapr disable. Need to test more. * Added list, show, set dapr component. Added dapr enable, disable. * Added delete dapr delete. * Added helps and param text. * Changed dapr delete to dapr remove to match with dapr set. * Commented out managed identity for whl file. * Uncommented. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_clients.py | 119 +++++++++++++++++ src/containerapp/azext_containerapp/_help.py | 78 +++++++++++ .../azext_containerapp/_models.py | 20 ++- .../azext_containerapp/_params.py | 7 + src/containerapp/azext_containerapp/_utils.py | 17 +++ .../azext_containerapp/commands.py | 9 ++ src/containerapp/azext_containerapp/custom.py | 122 +++++++++++++++++- 7 files changed, 369 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 8184e6d86e2..5a1e597523e 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -603,3 +603,122 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): 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): + #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{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, "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/_help.py b/src/containerapp/azext_containerapp/_help.py index 724335e8711..3a91fb32aca 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -164,6 +164,24 @@ --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ +helps['containerapp revision mode set'] = """ + type: command + short-summary: Set the revision mode of a Containerapp. + examples: + - name: Set the revision mode of a Containerapp. + text: | + az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp revision copy'] = """ + type: command + short-summary: Create a revision based on a previous revision. + examples: + - name: Create a revision based on a previous revision. + text: | + az containerapp revision copy -n MyContainerapp -g MyResourceGroup --cpu 0.75 --memory 1.5Gi +""" + # Environment Commands helps['containerapp env'] = """ type: group @@ -456,4 +474,64 @@ 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 Containerapp dapr. +""" + +helps['containerapp dapr enable'] = """ + type: command + short-summary: Enable dapr for a Containerapp. + examples: + - name: Enable dapr for a Containerapp. + 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 Containerapp. + examples: + - name: Disable dapr for a Containerapp. + text: | + az containerapp dapr disable -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp dapr list'] = """ + type: command + short-summary: List dapr components for a Containerapp environment. + examples: + - name: List dapr components for a Containerapp environment. + text: | + az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment +""" + +helps['containerapp dapr show'] = """ + type: command + short-summary: Show the details of a dapr component. + examples: + - name: Show the details of a dapr component. + text: | + az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" + +helps['containerapp dapr set'] = """ + type: command + short-summary: Create or update a dapr component. + examples: + - name: Create a dapr component. + text: | + az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName +""" + +helps['containerapp dapr remove'] = """ + type: command + short-summary: Remove a dapr componenet from a Containerapp environment. + examples: + - name: Remove a dapr componenet from a Containerapp environment. + text: | + az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 6440c677635..14d8e1a8fb3 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -181,6 +181,24 @@ "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, @@ -211,4 +229,4 @@ "clientSecret": None, # str "tenantId": None, #str "subscriptionId": None #str -} \ No newline at end of file +} diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 8435659f4d0..ab1ebe848bd 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -149,3 +149,10 @@ def load_arguments(self, _): with self.argument_context('containerapp secret delete') 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 dapr') 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="Tells 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', help="The dapr component environment name.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 54994a71578..14816a07915 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -414,6 +414,23 @@ def _remove_readonly_attributes(containerapp_def): 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 diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 9a83db9df7e..95e165d7e63 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -99,3 +99,12 @@ def load_command_table(self, _): g.custom_command('show', 'show_secret') g.custom_command('delete', 'delete_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()) + g.custom_command('list', 'list_dapr_components') + g.custom_command('show', 'show_dapr_component') + g.custom_command('set', 'create_or_update_dapr_component') + g.custom_command('remove', 'remove_dapr_component') + diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index db5bdb00db2..13d42b6be6b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from ._client_factory import handle_raw_exception -from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient from ._sdk_models import * from ._github_oauth import get_github_access_token from ._models import ( @@ -45,7 +45,7 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes) logger = get_logger(__name__) @@ -1949,4 +1949,122 @@ def set_secrets(cmd, name, resource_group_name, 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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + if 'dapr' not in containerapp_def['properties']: + containerapp_def['properties']['dapr'] = {} + + if dapr_app_id: + containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id + + if dapr_app_port: + containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port + + if dapr_app_protocol: + containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol + + containerapp_def['properties']['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"]['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 CLIError("The containerapp '{}' does not exist".format(name)) + + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + containerapp_def['properties']['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"]['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: + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + + #daprcomponent_def = _object_to_dict(daprcomponent_def) + 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: + raise CLIError("Dapr component not found.") + + 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) From 869c11b26c4f25a9fca4de03367e6200994ea8e5 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 13:20:04 -0700 Subject: [PATCH 132/177] Rename --image-name to --container-name --- .../azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/custom.py | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index ab1ebe848bd..cc2040b4a13 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -32,7 +32,7 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") - c.argument('image_name', type=str, options_list=['--image-name'], help="Name of the container.") + c.argument('container_name', type=str, options_list=['--container-name'], help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 13d42b6be6b..aab9338a524 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -307,7 +307,7 @@ def create_containerapp(cmd, resource_group_name, yaml=None, image=None, - image_name=None, + container_name=None, managed_env=None, min_replicas=None, max_replicas=None, @@ -445,7 +445,7 @@ def create_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name if image_name else name + 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) @@ -497,7 +497,7 @@ def update_containerapp(cmd, resource_group_name, yaml=None, image=None, - image_name=None, + container_name=None, min_replicas=None, max_replicas=None, ingress=None, @@ -551,7 +551,7 @@ def update_containerapp(cmd, update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None + update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None @@ -563,16 +563,16 @@ def update_containerapp(cmd, # Containers if update_map["container"]: - if not image_name: + if not container_name: if len(containerapp_def["properties"]["template"]["containers"]) == 1: - image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + 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() == image_name.lower(): + if c["name"].lower() == container_name.lower(): updating_existing_container = True if image is not None: @@ -618,7 +618,7 @@ def update_containerapp(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name + container_def["name"] = container_name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) @@ -1372,7 +1372,7 @@ def copy_revision(cmd, #label=None, yaml=None, image=None, - image_name=None, + container_name=None, min_replicas=None, max_replicas=None, env_vars=None, @@ -1416,7 +1416,7 @@ def copy_revision(cmd, update_map = {} update_map['ingress'] = traffic_weights update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or image_name or env_vars or cpu or memory or startup_command is not None or args is not None + update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None update_map['configuration'] = update_map['ingress'] if tags: @@ -1427,16 +1427,16 @@ def copy_revision(cmd, # Containers if update_map["container"]: - if not image_name: + if not container_name: if len(containerapp_def["properties"]["template"]["containers"]) == 1: - image_name = containerapp_def["properties"]["template"]["containers"][0]["name"] + 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() == image_name.lower(): + if c["name"].lower() == container_name.lower(): updating_existing_container = True if image is not None: @@ -1479,7 +1479,7 @@ def copy_revision(cmd, resources_def["memory"] = memory container_def = ContainerModel - container_def["name"] = image_name + container_def["name"] = container_name container_def["image"] = image if env_vars is not None: container_def["env"] = parse_env_var_flags(env_vars) From 0857b6bd62daeb39d1adc793133079cc7ac42e9d Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 13:42:48 -0700 Subject: [PATCH 133/177] Remove allowInsecure since it was messing with the api parsing --- src/containerapp/azext_containerapp/_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index 14d8e1a8fb3..b356adaa2a8 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -136,8 +136,7 @@ "targetPort": None, "transport": None, # 'auto', 'http', 'http2' "traffic": None, # TrafficWeight - "customDomains": None, # [CustomDomain] - "allowInsecure": None # Boolean + "customDomains": None # [CustomDomain] } RegistryCredentials = { From a0acb0153b45f4d9ecb66fcc846d35934d4206fa Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 15:11:33 -0700 Subject: [PATCH 134/177] Fix for env var being empty string --- src/containerapp/azext_containerapp/custom.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index aab9338a524..dc7bdcd5d05 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -394,7 +394,7 @@ def create_containerapp(cmd, registries_def = RegistryCredentialsModel # Infer credentials if not supplied and its azurecr - if not registry_user or not registry_pass: + 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 @@ -541,6 +541,14 @@ def update_containerapp(cmd, if not containerapp_def: raise CLIError("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"] = "" + # If ACR image and registry_server is not supplied, infer it if image and '.azurecr.io' in image: if not registry_server: @@ -704,7 +712,7 @@ def update_containerapp(cmd, raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") # Infer credentials if not supplied and its azurecr - if not registry_user or not registry_pass: + if registry_user is None or registry_pass is None: registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) # Check if updating existing registry @@ -1221,7 +1229,7 @@ def create_or_update_github_action(cmd, azure_credentials["subscriptionId"] = get_subscription_id(cmd.cli_ctx) # Registry - if not registry_username or not registry_password: + 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') @@ -1413,6 +1421,14 @@ def copy_revision(cmd, 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['ingress'] = traffic_weights update_map['scale'] = min_replicas or max_replicas From 0f4f38528c043b000312be81edcbdd4a09cdb4bf Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:23:46 -0700 Subject: [PATCH 135/177] Rename to --dapr-instrumentation-key, only infer ACR credentials if --registry-server is provided --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 12 +++++++++++- src/containerapp/azext_containerapp/custom.py | 14 ++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index cc2040b4a13..c592ed5363d 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -86,7 +86,7 @@ def load_arguments(self, _): 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=['--instrumentation-key'], help='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') + c.argument('instrumentation_key', options_list=['--dapr-instrumentation-key'], help='Azure Monitor 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. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 14816a07915..fa3ee8f2a50 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -493,7 +493,7 @@ def _get_app_from_revision(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 url is required if using Azure Container Registry, otherwise Registry username and password are required.') + 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] @@ -503,3 +503,13 @@ def _infer_acr_credentials(cmd, registry_server): 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)) + + +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/custom.py b/src/containerapp/azext_containerapp/custom.py index dc7bdcd5d05..527c5c56998 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -45,7 +45,7 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists) logger = get_logger(__name__) @@ -384,11 +384,6 @@ def create_containerapp(cmd, if secrets is not None: secrets_def = parse_secret_flags(secrets) - # If ACR image and registry_server is not supplied, infer it - if image and '.azurecr.io' in image: - if not registry_server: - registry_server = image.split('/')[0] - registries_def = None if registry_server is not None: registries_def = RegistryCredentialsModel @@ -549,11 +544,6 @@ def update_containerapp(cmd, if "value" not in e: e["value"] = "" - # If ACR image and registry_server is not supplied, infer it - if image and '.azurecr.io' in image: - if not registry_server: - registry_server = image.split('/')[0] - update_map = {} update_map['secrets'] = secrets is not None update_map['ingress'] = ingress or target_port or transport or traffic_weights @@ -712,7 +702,7 @@ def update_containerapp(cmd, raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") # Infer credentials if not supplied and its azurecr - if registry_user is None or registry_pass is None: + if (registry_user is None or registry_pass is None) and not _registry_exists(containerapp_def, registry_server): registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) # Check if updating existing registry From 144ce5765ab00a6beb966f294a0390c29a7b0e89 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:24:58 -0700 Subject: [PATCH 136/177] Remove az containerapp scale --- .../azext_containerapp/commands.py | 1 - src/containerapp/azext_containerapp/custom.py | 33 ------------------- 2 files changed, 34 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 95e165d7e63..8ee1f082671 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -44,7 +44,6 @@ def load_command_table(self, _): g.custom_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()) - g.custom_command('scale', 'scale_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 527c5c56998..c026aecfb6d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -749,39 +749,6 @@ def update_containerapp(cmd, handle_raw_exception(e) -def scale_containerapp(cmd, name, resource_group_name, min_replicas=None, max_replicas=None, no_wait=False): - 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 CLIError("The containerapp '{}' does not exist".format(name)) - - 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 scale 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") From 3b01ec6e7cea70aaeffc45dfd7cfcb11a24dce95 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:30:57 -0700 Subject: [PATCH 137/177] Fix delete containerapp errors --- .../azext_containerapp/_clients.py | 22 +++---------------- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 4 ++-- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 5a1e597523e..108ee5b004f 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -128,7 +128,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= return r.json() @classmethod - def delete(cls, cmd, resource_group_name, name, no_wait=False): + def delete(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) @@ -142,24 +142,8 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): 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') + if r.status_code == 202: + logger.warning('Containerapp successfully deleted') return @classmethod diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 8ee1f082671..4a8142c43a3 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -45,7 +45,7 @@ def load_command_table(self, _): 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c026aecfb6d..4454eb58477 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -773,11 +773,11 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) -def delete_containerapp(cmd, name, resource_group_name, no_wait=False): +def delete_containerapp(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") try: - return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) + return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) except CLIError as e: handle_raw_exception(e) From b671af3eefb545a30f7a11c45b4a1d05bc2055d3 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:53:08 -0700 Subject: [PATCH 138/177] Remove ingress, dapr flags from az containerapp update/revision copy --- src/containerapp/azext_containerapp/_utils.py | 2 +- src/containerapp/azext_containerapp/custom.py | 102 ++++-------------- 2 files changed, 23 insertions(+), 81 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index fa3ee8f2a50..297ce4904ba 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -459,7 +459,7 @@ def _is_valid_weight(weight): return False -def _update_traffic_Weights(containerapp_def, list_weights): +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"] = [] diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4454eb58477..1fcb7c2176b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -43,7 +43,7 @@ parse_secret_flags, store_as_secret_and_return_secret_ref, parse_list_of_strings, 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, + _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) @@ -495,10 +495,6 @@ def update_containerapp(cmd, container_name=None, min_replicas=None, max_replicas=None, - ingress=None, - target_port=None, - transport=None, - traffic_weights=None, revisions_mode=None, secrets=None, env_vars=None, @@ -507,11 +503,6 @@ def update_containerapp(cmd, registry_server=None, registry_user=None, registry_pass=None, - dapr_enabled=None, - dapr_app_port=None, - dapr_app_id=None, - dapr_app_protocol=None, - # dapr_components=None, revision_suffix=None, startup_command=None, args=None, @@ -520,9 +511,9 @@ def update_containerapp(cmd, _validate_subscription_registered(cmd, "Microsoft.App") if yaml: - if image or min_replicas or max_replicas or target_port or ingress or\ + if image or min_replicas or max_replicas 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\ + registry_user or registry_pass 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) @@ -546,12 +537,10 @@ def update_containerapp(cmd, update_map = {} update_map['secrets'] = secrets is not None - update_map['ingress'] = ingress or target_port or transport or traffic_weights update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None - update_map['dapr'] = dapr_enabled or dapr_app_port or dapr_app_id or dapr_app_protocol - update_map['configuration'] = update_map['secrets'] or update_map['ingress'] or update_map['registries'] or revisions_mode is not None + update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -644,46 +633,10 @@ def update_containerapp(cmd, if max_replicas is not None: containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - # Dapr - if update_map["dapr"]: - if "dapr" not in containerapp_def["properties"]["template"]: - containerapp_def["properties"]["template"]["dapr"] = {} - if dapr_enabled is not None: - containerapp_def["properties"]["template"]["dapr"]["daprEnabled"] = dapr_enabled - if dapr_app_id is not None: - containerapp_def["properties"]["template"]["dapr"]["appId"] = dapr_app_id - if dapr_app_port is not None: - containerapp_def["properties"]["template"]["dapr"]["appPort"] = dapr_app_port - if dapr_app_protocol is not None: - containerapp_def["properties"]["template"]["dapr"]["appProtocol"] = dapr_app_protocol - # Configuration if revisions_mode is not None: containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode - if update_map["ingress"]: - if "ingress" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["ingress"] = {} - - external_ingress = None - if ingress is not None: - if ingress.lower() == "internal": - external_ingress = False - elif ingress.lower() == "external": - external_ingress = True - - if external_ingress is not None: - containerapp_def["properties"]["configuration"]["ingress"]["external"] = external_ingress - - if target_port is not None: - containerapp_def["properties"]["configuration"]["ingress"]["targetPort"] = target_port - - if transport is not None: - containerapp_def["properties"]["configuration"]["ingress"]["transport"] = transport - - if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) if secrets is not None: @@ -1331,24 +1284,23 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): handle_raw_exception(e) def copy_revision(cmd, - name, - resource_group_name, - from_revision=None, - #label=None, - yaml=None, - image=None, - container_name=None, - min_replicas=None, - max_replicas=None, - env_vars=None, - cpu=None, - memory=None, - revision_suffix=None, - startup_command=None, - traffic_weights=None, - args=None, - tags=None, - no_wait=False): + name, + resource_group_name, + from_revision=None, + #label=None, + yaml=None, + image=None, + container_name=None, + min_replicas=None, + max_replicas=None, + env_vars=None, + 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 from_revision: @@ -1387,10 +1339,8 @@ def copy_revision(cmd, e["value"] = "" update_map = {} - update_map['ingress'] = traffic_weights update_map['scale'] = min_replicas or max_replicas update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None - update_map['configuration'] = update_map['ingress'] if tags: _add_or_update_tags(containerapp_def, tags) @@ -1480,14 +1430,6 @@ def copy_revision(cmd, if max_replicas is not None: containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - # Configuration - if update_map["ingress"]: - if "ingress" not in containerapp_def["properties"]["configuration"]: - containerapp_def["properties"]["configuration"]["ingress"] = {} - - if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) try: @@ -1621,7 +1563,7 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") if traffic_weights is not None: - _update_traffic_Weights(containerapp_def, traffic_weights) + _update_traffic_weights(containerapp_def, traffic_weights) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) From df1ae0b312dbe36b935a426286102eac03eb0c85 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:53:15 -0700 Subject: [PATCH 139/177] Fix revision list -o table --- src/containerapp/azext_containerapp/commands.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 4a8142c43a3..40e422bb532 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -27,10 +27,13 @@ def transform_containerapp_list_output(apps): def transform_revision_output(rev): props = ['name', 'replicas', 'active', 'createdTime'] - result = {k: rev[k] for k in rev if k in props} + result = {k: rev['properties'][k] for k in rev['properties'] if k in props} - if 'latestRevisionFqdn' in rev['template']: - result['fqdn'] = rev['template']['latestRevisionFqdn'] + if 'name' in rev: + result['name'] = rev['name'] + + if 'fqdn' in rev['properties']['template']: + result['fqdn'] = rev['properties']['template']['fqdn'] return result From 9962e290f27a06001e71a8c373d5568afc90482a Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:54:29 -0700 Subject: [PATCH 140/177] Help text fix --- src/containerapp/azext_containerapp/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 3a91fb32aca..228343f5dee 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -479,7 +479,7 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage Containerapp dapr. + short-summary: Commands to manage dapr. """ helps['containerapp dapr enable'] = """ From ef031f41178652ea566bc4e5f8ed9609718702fc Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 14 Mar 2022 16:59:07 -0700 Subject: [PATCH 141/177] Bump extension to 0.1.2 --- src/containerapp/HISTORY.rst | 6 ++++++ src/containerapp/setup.py | 2 +- src/index.json | 40 ++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index f58df889075..8400a3f0baf 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.1.2 +++++++ +* Various fixes for bugs found +* Dapr subgroup +* Managed Identity + 0.1.1 ++++++ * Various fixes for az containerapp create, update diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index fa1b93b7448..96524e9ab67 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.1' +VERSION = '0.1.2' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index 44e9bca1d09..fc191117ee7 100644 --- a/src/index.json +++ b/src/index.json @@ -12364,6 +12364,46 @@ "version": "0.1.1" }, "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" + }, + { + "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.2-py2.py3-none-any.whl", + "filename": "containerapp-0.1.2-py2.py3-none-any.whl", + "metadata": { + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.0.67", + "extensions": { + "python.details": { + "contacts": [ + { + "email": "azpycli@microsoft.com", + "name": "Microsoft Corporation", + "role": "author" + } + ], + "document_names": { + "description": "DESCRIPTION.rst" + }, + "project_urls": { + "Home": "https://github.com/Azure/azure-cli-extensions" + } + } + }, + "extras": [], + "generator": "bdist_wheel (0.30.0)", + "license": "MIT", + "metadata_version": "2.0", + "name": "containerapp", + "run_requires": [ + { + "requires": [ + "azure-cli-core" + ] + } + ], + "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", + "version": "0.1.2" + }, + "sha256Digest": "b1d4cc823f761cfb5469f8d53a9fa04bdc1493c3c5d5f3a90333876287e7b2f8" } ], "cosmosdb-preview": [ From a26df8cc8c97dff5191675a5aaeb39b19a114077 Mon Sep 17 00:00:00 2001 From: Anthony Chu Date: Tue, 15 Mar 2022 14:33:36 -0700 Subject: [PATCH 142/177] Update managed identities and Dapr help text (#25) * Update managed identities and Dapr help text * Update Dapr flags * Add secretref note --- src/containerapp/azext_containerapp/_help.py | 43 +++++++++++-------- .../azext_containerapp/_params.py | 12 +++--- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 228343f5dee..3a5fa25e5dc 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -241,13 +241,13 @@ # Identity Commands helps['containerapp identity'] = """ type: group - short-summary: Manage service (managed) identities for a containerapp + short-summary: Commands to manage managed identities. """ helps['containerapp identity assign'] = """ type: command - short-summary: Assign a managed identity to a containerapp - long-summary: Managed identities can be user-assigned or system-assigned + short-summary: Assign managed identity to a container app. + long-summary: Managed identities can be user-assigned or system-assigned. examples: - name: Assign system identity. text: | @@ -259,7 +259,7 @@ helps['containerapp identity remove'] = """ type: command - short-summary: Remove a managed identity from a containerapp + short-summary: Remove a managed identity from a container app. examples: - name: Remove system identity. text: | @@ -271,7 +271,7 @@ helps['containerapp identity show'] = """ type: command - short-summary: Show the containerapp's identity details + short-summary: Show managed identities of a container app. """ # Ingress Commands @@ -428,6 +428,11 @@ 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. @@ -479,59 +484,59 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage dapr. + short-summary: Commands to manage Dapr. """ helps['containerapp dapr enable'] = """ type: command - short-summary: Enable dapr for a Containerapp. + short-summary: Enable Dapr for a container app. examples: - - name: Enable dapr for a Containerapp. + - 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 Containerapp. + short-summary: Disable Dapr for a container app. examples: - - name: Disable dapr for a Containerapp. + - name: Disable Dapr for a container app. text: | az containerapp dapr disable -n MyContainerapp -g MyResourceGroup """ helps['containerapp dapr list'] = """ type: command - short-summary: List dapr components for a Containerapp environment. + short-summary: List Dapr components. examples: - - name: List dapr components for a Containerapp environment. + - name: List Dapr components for a Container Apps environment. text: | az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment """ helps['containerapp dapr show'] = """ type: command - short-summary: Show the details of a dapr component. + short-summary: Show the details of a Dapr component. examples: - - name: Show the details of a dapr component. + - name: Show the details of a Dapr component. text: | az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ helps['containerapp dapr set'] = """ type: command - short-summary: Create or update a dapr component. + short-summary: Create or update a Dapr component. examples: - - name: Create a dapr component. + - name: Create a Dapr component. text: | - az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName + az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml my-component.yaml --name MyDaprName """ helps['containerapp dapr remove'] = """ type: command - short-summary: Remove a dapr componenet from a Containerapp environment. + short-summary: Remove a Dapr component. examples: - - name: Remove a dapr componenet from a Containerapp environment. + - name: Remove a Dapr component. text: | az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment """ \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c592ed5363d..85ee7f4239e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -35,7 +35,7 @@ def load_arguments(self, _): c.argument('container_name', type=str, options_list=['--container-name'], help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars'], help="A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values") + c.argument('env_vars', nargs='*', options_list=['--env-vars'], 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='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') @@ -151,8 +151,8 @@ def load_arguments(self, _): 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 dapr') 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="Tells 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', help="The dapr component environment name.") + c.argument('dapr_app_id', help="The Dapr app id.") + c.argument('dapr_app_port', help="The port Dapr uses to talk to the application.") + c.argument('dapr_app_protocol', help="The protocol Dapr uses to talk to the application. Allowed values: grpc, http.") + c.argument('dapr_component_name', help="The Dapr component name.") + c.argument('environment_name', help="The Container Apps environment name.") From ea45ec87468d8dea468bfc48938712064c5b6852 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:51:13 -0400 Subject: [PATCH 143/177] Env var options + various bug fixes (#26) * Moved dapr arguments to env as a subgroup. * Added env variable options. * Changed revision mode set to revision set-mode. * Added env var options to revision copy. * Fixed revision copy bug related to env secret refs. * Changed registry and secret delete to remove. Added registry param helps. Removed replica from table output and added trafficWeight. * Updating warning text. * Updated warning text once more. * Made name optional for revision copy if from-revision flag is passed. Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/_help.py | 102 +++++++------ .../azext_containerapp/_params.py | 30 +++- src/containerapp/azext_containerapp/_utils.py | 27 +++- .../azext_containerapp/commands.py | 21 ++- src/containerapp/azext_containerapp/custom.py | 138 ++++++++++++++---- 5 files changed, 214 insertions(+), 104 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 3a5fa25e5dc..cb5126c3f61 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -145,13 +145,13 @@ az containerapp revision deactivate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision """ -helps['containerapp revision mode set'] = """ +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 mode set-n MyContainerapp -g MyResourceGroup --mode Single + az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single """ helps['containerapp revision copy'] = """ @@ -164,15 +164,6 @@ --from-revision PreviousRevisionName --cpu 0.75 --memory 1.5Gi """ -helps['containerapp revision mode set'] = """ - type: command - short-summary: Set the revision mode of a Containerapp. - examples: - - name: Set the revision mode of a Containerapp. - text: | - az containerapp revision set --mode Single -n MyContainerapp -g MyResourceGroup -""" - helps['containerapp revision copy'] = """ type: command short-summary: Create a revision based on a previous revision. @@ -238,6 +229,47 @@ az containerapp env list -g MyResourceGroup """ +helps['containerapp env dapr-component'] = """ + type: group + short-summary: Commands to manage Container App environment dapr components. +""" + +helps['containerapp env dapr-component list'] = """ + type: command + short-summary: List dapr components for a Containerapp environment. + examples: + - name: List dapr components for a Containerapp environment. + text: | + az containerapp env dapr-component list -g MyResourceGroup --environment-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 MyDaprComponenetName --environment-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 --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName +""" + +helps['containerapp env dapr-component remove'] = """ + type: command + short-summary: Remove a dapr componenet from a Containerapp environment. + examples: + - name: Remove a dapr componenet from a Containerapp environment. + text: | + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment +""" + # Identity Commands helps['containerapp identity'] = """ type: group @@ -374,13 +406,13 @@ """ -helps['containerapp registry delete'] = """ +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 delete -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io + az containerapp registry remove -n MyContainerapp -g MyResourceGroup --server MyContainerappRegistry.azurecr.io """ # Secret Commands @@ -407,13 +439,13 @@ az containerapp secret list -n MyContainerapp -g MyResourceGroup """ -helps['containerapp secret delete'] = """ +helps['containerapp secret remove'] = """ type: command - short-summary: Delete secrets from a container app. + short-summary: Remove secrets from a container app. examples: - - name: Delete secrets from a container app. + - name: Remove secrets from a container app. text: | - az containerapp secret delete -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 + az containerapp secret remove -n MyContainerapp -g MyResourceGroup --secret-names MySecret MySecret2 """ helps['containerapp secret set'] = """ @@ -504,39 +536,3 @@ text: | az containerapp dapr disable -n MyContainerapp -g MyResourceGroup """ - -helps['containerapp dapr list'] = """ - type: command - short-summary: List Dapr components. - examples: - - name: List Dapr components for a Container Apps environment. - text: | - az containerapp dapr list -g MyResourceGroup --environment-name MyEnvironment -""" - -helps['containerapp dapr show'] = """ - type: command - short-summary: Show the details of a Dapr component. - examples: - - name: Show the details of a Dapr component. - text: | - az containerapp dapr show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment -""" - -helps['containerapp dapr set'] = """ - type: command - short-summary: Create or update a Dapr component. - examples: - - name: Create a Dapr component. - text: | - az containerapp dapr set -g MyResourceGroup --environment-name MyEnv --yaml my-component.yaml --name MyDaprName -""" - -helps['containerapp dapr remove'] = """ - type: command - short-summary: Remove a Dapr component. - examples: - - name: Remove a Dapr component. - text: | - az containerapp dapr delete -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment -""" \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 85ee7f4239e..1d3e3b6dc27 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -40,6 +40,13 @@ def load_arguments(self, _): c.argument('args', nargs='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') + # Env vars + with self.argument_context('containerapp', arg_group='Environment variables (Creates new revision)') as c: + c.argument('set_env_vars', options_list=['--set-env-vars, --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 (Creates new revision)') as c: c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") @@ -147,12 +154,21 @@ def load_arguments(self, _): 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 delete') as c: + 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 dapr') as c: - c.argument('dapr_app_id', help="The Dapr app id.") - c.argument('dapr_app_port', help="The port Dapr uses to talk to the application.") - c.argument('dapr_app_protocol', help="The protocol Dapr uses to talk to the application. Allowed values: grpc, http.") - c.argument('dapr_component_name', help="The Dapr component name.") - c.argument('environment_name', help="The Container Apps environment name.") + 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="Tells 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') + diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 297ce4904ba..1c5a10e5d29 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -116,6 +116,12 @@ def parse_secret_flags(secret_list): 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 store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass, update_existing_secret=False): if registry_pass.startswith("secretref:"): @@ -328,7 +334,7 @@ def _remove_secret(containerapp_def, secret_name): containerapp_def["properties"]["configuration"]["secrets"].pop(i) break -def _add_or_update_env_vars(existing_env_vars, new_env_vars): +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 @@ -336,6 +342,8 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars): 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"])) if "value" in new_env_var: existing_env_var["value"] = new_env_var["value"] @@ -350,8 +358,25 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars): # 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"])) 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 in range(0, len(existing_env_vars)): + existing_env_var = existing_env_vars[i] + 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)) def _add_or_update_tags(containerapp_def, tags): if 'tags' not in containerapp_def: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 40e422bb532..9fd58c7575c 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -26,7 +26,7 @@ def transform_containerapp_list_output(apps): def transform_revision_output(rev): - props = ['name', 'replicas', 'active', 'createdTime'] + props = ['name', 'active', 'createdTime', 'trafficWeight'] result = {k: rev['properties'][k] for k in rev['properties'] if k in props} if 'name' in rev: @@ -50,7 +50,6 @@ def load_command_table(self, _): g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) - with self.command_group('containerapp env') as g: g.custom_command('show', 'show_managed_environment') g.custom_command('list', 'list_managed_environments') @@ -58,13 +57,17 @@ def load_command_table(self, _): # g.custom_command('update', 'update_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_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 identity') as g: g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('show', 'show_managed_identity') - 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_command('show', 'show_github_action', exception_handler=ex_handler_factory()) @@ -77,9 +80,7 @@ def load_command_table(self, _): g.custom_command('restart', 'restart_revision') g.custom_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()) - - with self.command_group('containerapp revision mode') as g: - g.custom_command('set', 'set_revision_mode', 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()) @@ -94,19 +95,15 @@ def load_command_table(self, _): g.custom_command('set', 'set_registry', exception_handler=ex_handler_factory()) g.custom_command('show', 'show_registry') g.custom_command('list', 'list_registry') - g.custom_command('delete', 'delete_registry', exception_handler=ex_handler_factory()) + 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_command('show', 'show_secret') - g.custom_command('delete', 'delete_secrets', exception_handler=ex_handler_factory()) + 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()) - g.custom_command('list', 'list_dapr_components') - g.custom_command('show', 'show_dapr_component') - g.custom_command('set', 'create_or_update_dapr_component') - g.custom_command('remove', 'remove_dapr_component') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1fcb7c2176b..961fbd500f0 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -45,7 +45,7 @@ _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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, _update_revision_env_secretrefs) logger = get_logger(__name__) @@ -126,8 +126,10 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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() @@ -497,7 +499,10 @@ def update_containerapp(cmd, max_replicas=None, revisions_mode=None, secrets=None, - env_vars=None, + set_env_vars=None, + remove_env_vars=None, + replace_env_vars=None, + remove_all_env_vars=False, cpu=None, memory=None, registry_server=None, @@ -512,7 +517,7 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or secrets or env_vars or cpu or memory or registry_server or\ + revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ registry_user or registry_pass 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') @@ -539,7 +544,7 @@ def update_containerapp(cmd, update_map['secrets'] = secrets is not None update_map['registries'] = registry_server or registry_user or registry_pass update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or container_name or env_vars is not None or cpu or memory or startup_command is not None or args is not None + 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 update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None if tags: @@ -564,13 +569,28 @@ def update_containerapp(cmd, if image is not None: c["image"] = image - if env_vars is not None: - if isinstance(env_vars, list) and not env_vars: + + if set_env_vars is not None: + if "env" not in c or not c["env"]: c["env"] = [] - else: - if "env" not in c or not c["env"]: - c["env"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + # 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 @@ -607,8 +627,23 @@ def update_containerapp(cmd, container_def = ContainerModel container_def["name"] = container_name container_def["image"] = image - if env_vars is not None: - container_def["env"] = parse_env_var_flags(env_vars) + 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 @@ -1284,16 +1319,19 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): handle_raw_exception(e) def copy_revision(cmd, - name, resource_group_name, from_revision=None, #label=None, + name=None, yaml=None, image=None, container_name=None, min_replicas=None, max_replicas=None, - env_vars=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, @@ -1303,12 +1341,16 @@ def copy_revision(cmd, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") - if not from_revision: - from_revision = containerapp_def["properties"]["latestRevisionName"] + 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\ - env_vars or cpu or memory 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) @@ -1322,13 +1364,15 @@ def copy_revision(cmd, if not containerapp_def: raise CLIError("The containerapp '{}' does not exist".format(name)) - 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) + 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) - containerapp_def["properties"]["template"] = r["properties"]["template"] + _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"]: @@ -1340,7 +1384,7 @@ def copy_revision(cmd, update_map = {} update_map['scale'] = min_replicas or max_replicas - update_map['container'] = image or container_name or env_vars or cpu or memory or startup_command is not None or args is not None + 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) @@ -1364,10 +1408,28 @@ def copy_revision(cmd, if image is not None: c["image"] = image - if env_vars is not None: + + 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"] = [] - _add_or_update_env_vars(c["env"], parse_env_var_flags(env_vars)) + # 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 @@ -1404,8 +1466,22 @@ def copy_revision(cmd, container_def = ContainerModel container_def["name"] = container_name container_def["image"] = image - if env_vars is not None: - container_def["env"] = parse_env_var_flags(env_vars) + + 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 @@ -1706,7 +1782,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password except Exception as e: handle_raw_exception(e) -def delete_registry(cmd, name, resource_group_name, server, no_wait=False): +def remove_registry(cmd, name, resource_group_name, server, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1789,7 +1865,7 @@ def show_secret(cmd, name, resource_group_name, secret_name): return secret raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) -def delete_secrets(cmd, name, resource_group_name, secret_names, no_wait = False): +def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait = False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None From 43acd4b08faa6066e2911f49a1dff61e46723e97 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Fri, 18 Mar 2022 13:05:12 -0400 Subject: [PATCH 144/177] Fixed style issues, various bug fixes (#27) * Moved dapr arguments to env as a subgroup. * Added env variable options. * Changed revision mode set to revision set-mode. * Added env var options to revision copy. * Fixed revision copy bug related to env secret refs. * Changed registry and secret delete to remove. Added registry param helps. Removed replica from table output and added trafficWeight. * Updating warning text. * Updated warning text once more. * Made name optional for revision copy if from-revision flag is passed. * Fixed whitespace style issues. * Styled clients and utils to pass pylint. * Finished client.py pylint fixes. * Fixed pylint issues. * Fixed flake8 commands and custom. * Fixed flake issues in src. * Added license header to _sdk_models. * Added confirmation for containerapp delete. Co-authored-by: Haroon Feisal --- .../azext_containerapp/__init__.py | 3 +- .../azext_containerapp/_client_factory.py | 4 +- .../azext_containerapp/_clients.py | 33 +- .../azext_containerapp/_github_oauth.py | 6 +- src/containerapp/azext_containerapp/_help.py | 11 +- .../azext_containerapp/_models.py | 101 ++--- .../azext_containerapp/_params.py | 16 +- .../azext_containerapp/_sdk_models.py | 10 +- src/containerapp/azext_containerapp/_utils.py | 142 ++++--- .../azext_containerapp/_validators.py | 16 +- .../azext_containerapp/commands.py | 17 +- src/containerapp/azext_containerapp/custom.py | 397 ++++++++---------- 12 files changed, 383 insertions(+), 373 deletions(-) diff --git a/src/containerapp/azext_containerapp/__init__.py b/src/containerapp/azext_containerapp/__init__.py index f772766731c..dcff6d86def 100644 --- a/src/containerapp/azext_containerapp/__init__.py +++ b/src/containerapp/azext_containerapp/__init__.py @@ -2,6 +2,7 @@ # 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 @@ -16,7 +17,7 @@ def __init__(self, cli_ctx=None): operations_tmpl='azext_containerapp.custom#{}', client_factory=None) super(ContainerappCommandsLoader, self).__init__(cli_ctx=cli_ctx, - custom_command_type=containerapp_custom) + custom_command_type=containerapp_custom) def load_command_table(self, args): from azext_containerapp.commands import load_command_table diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index f998486c63e..9a249cdbe7e 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -2,6 +2,7 @@ # 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 @@ -13,7 +14,6 @@ def ex_handler_factory(no_throw=False): def _polish_bad_errors(ex): import json - from knack.util import CLIError try: content = json.loads(ex.response.content) if 'message' in content: @@ -63,11 +63,13 @@ 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 diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 108ee5b004f..2dc138a6031 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -2,11 +2,12 @@ # 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 sys import api_version 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 @@ -15,8 +16,8 @@ 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 +POLLING_TIMEOUT = 60 # how many seconds before exiting +POLLING_SECONDS = 2 # how many seconds between requests class PollingAnimation(): @@ -37,7 +38,7 @@ def flush(self): sys.stdout.write("\033[K") -def poll(cmd, request_url, poll_if_status): +def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-return-statements try: start = time.time() end = time.time() + POLLING_TIMEOUT @@ -53,19 +54,17 @@ def poll(cmd, request_url, poll_if_status): r = send_raw_request(cmd.cli_ctx, "GET", request_url) r2 = r.json() - if not "properties" in r2 or not "provisioningState" in r2["properties"] or not r2["properties"]["provisioningState"].lower() == poll_if_status: + 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: + except Exception as e: # pylint: disable=broad-except animation.flush() - if poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete - return - - raise e + if not poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete + raise e class ContainerAppClient(): @@ -144,7 +143,6 @@ def delete(cls, cmd, resource_group_name, name): if r.status_code == 202: logger.warning('Containerapp successfully deleted') - return @classmethod def show(cls, cmd, resource_group_name, name): @@ -222,7 +220,6 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) @classmethod def list_secrets(cls, cmd, resource_group_name, name): - secrets = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -338,6 +335,7 @@ def deactivate_revision(cls, cmd, resource_group_name, container_app_name, name) 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): @@ -413,7 +411,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) if no_wait: - return # API doesn't return JSON (it returns no content) + 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( @@ -506,6 +504,7 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) return env_list + class GitHubActionClient(): @classmethod def create_or_update(cls, cmd, resource_group_name, name, github_action_envelope, headers, no_wait=False): @@ -552,7 +551,6 @@ def show(cls, cmd, resource_group_name, name): r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() - #TODO @classmethod def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager @@ -569,7 +567,7 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): 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) + 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( @@ -588,10 +586,10 @@ def delete(cls, cmd, resource_group_name, name, headers, no_wait=False): 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): - #create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.App/managedEnvironments/{environmentName}/daprComponents/{name}'} management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager api_version = NEW_API_VERSION @@ -639,7 +637,7 @@ def delete(cls, cmd, resource_group_name, environment_name, name, no_wait=False) r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) if no_wait: - return # API doesn't return JSON (it returns no content) + 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( @@ -705,4 +703,3 @@ def list(cls, cmd, resource_group_name, environment_name, formatter=lambda x: x) 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 index 3df73a6b1aa..659d43afc39 100644 --- a/src/containerapp/azext_containerapp/_github_oauth.py +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -2,6 +2,7 @@ # 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 @@ -22,6 +23,7 @@ "workflow" ] + def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument if scope_list: for scope in scope_list: @@ -81,6 +83,6 @@ def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-arg 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)) + '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') \ No newline at end of file + 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 index cb5126c3f61..a4a71960f02 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -151,7 +151,7 @@ examples: - name: Set a container app to single revision mode. text: | - az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single + az containerapp revision set-mode -n MyContainerapp -g MyResourceGroup --mode Single """ helps['containerapp revision copy'] = """ @@ -368,7 +368,7 @@ examples: - name: Show a container app's ingress traffic configuration. text: | - az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup + az containerapp ingress traffic show -n MyContainerapp -g MyResourceGroup """ # Registry Commands @@ -392,7 +392,7 @@ examples: - name: List container registries configured in a container app. text: | - az containerapp registry list -n MyContainerapp -g MyResourceGroup + az containerapp registry list -n MyContainerapp -g MyResourceGroup """ helps['containerapp registry set'] = """ @@ -403,7 +403,6 @@ text: | az containerapp registry set -n MyContainerapp -g MyResourceGroup \\ --server MyExistingContainerappRegistry.azurecr.io --username MyRegistryUsername --password MyRegistryPassword - """ helps['containerapp registry remove'] = """ @@ -454,10 +453,10 @@ examples: - name: Add secrets to a container app. text: | - az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MySecretName1=MySecretValue1 MySecretName2=MySecretValue2 + 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 + az containerapp secret set -n MyContainerapp -g MyResourceGroup --secrets MyExistingSecretName=MyNewSecretValue """ helps['containerapp github-action'] = """ diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index b356adaa2a8..d00798765c5 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -3,6 +3,7 @@ # 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, @@ -17,7 +18,7 @@ "tags": None, "properties": { "daprAIInstrumentationKey": None, - "vnetConfiguration": None, # VnetConfiguration + "vnetConfiguration": None, # VnetConfiguration "internalLoadBalancerEnabled": None, "appLogsConfiguration": None } @@ -63,15 +64,15 @@ "name": None, "command": None, "args": None, - "env": None, # [EnvironmentVar] - "resources": None, # ContainerResources - "volumeMounts": None, # [VolumeMount] + "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 + "storageType": "EmptyDir", # AzureFile or EmptyDir + "storageName": None # None for EmptyDir, otherwise name of storage resource } ScaleRuleAuth = { @@ -82,25 +83,25 @@ QueueScaleRule = { "queueName": None, "queueLength": None, - "auth": None # ScaleRuleAuth + "auth": None # ScaleRuleAuth } CustomScaleRule = { "type": None, "metadata": {}, - "auth": None # ScaleRuleAuth + "auth": None # ScaleRuleAuth } HttpScaleRule = { "metadata": {}, - "auth": None # ScaleRuleAuth + "auth": None # ScaleRuleAuth } ScaleRule = { "name": None, - "azureQueue": None, # QueueScaleRule - "customScaleRule": None, # CustomScaleRule - "httpScaleRule": None, # HttpScaleRule + "azureQueue": None, # QueueScaleRule + "customScaleRule": None, # CustomScaleRule + "httpScaleRule": None, # HttpScaleRule } Secret = { @@ -111,7 +112,7 @@ Scale = { "minReplicas": None, "maxReplicas": None, - "rules": [] # list of ScaleRule + "rules": [] # list of ScaleRule } TrafficWeight = { @@ -126,7 +127,7 @@ CustomDomain = { "name": None, - "bindingType": None, # BindingType + "bindingType": None, # BindingType "certificateId": None } @@ -134,9 +135,9 @@ "fqdn": None, "external": False, "targetPort": None, - "transport": None, # 'auto', 'http', 'http2' - "traffic": None, # TrafficWeight - "customDomains": None # [CustomDomain] + "transport": None, # 'auto', 'http', 'http2' + "traffic": None, # TrafficWeight + "customDomains": None # [CustomDomain] } RegistryCredentials = { @@ -147,17 +148,17 @@ Template = { "revisionSuffix": None, - "containers": None, # [Container] + "containers": None, # [Container] "scale": Scale, "dapr": Dapr, - "volumes": None # [Volume] + "volumes": None # [Volume] } Configuration = { - "secrets": None, # [Secret] - "activeRevisionsMode": None, # 'multiple' or 'single' - "ingress": None, # Ingress - "registries": None # [RegistryCredentials] + "secrets": None, # [Secret] + "activeRevisionsMode": None, # 'multiple' or 'single' + "ingress": None, # Ingress + "registries": None # [RegistryCredentials] } UserAssignedIdentity = { @@ -165,26 +166,26 @@ } ManagedServiceIdentity = { - "type": None, # 'None', 'SystemAssigned', 'UserAssigned', 'SystemAssigned,UserAssigned' - "userAssignedIdentities": None # {string: UserAssignedIdentity} + "type": None, # 'None', 'SystemAssigned', 'UserAssigned', 'SystemAssigned,UserAssigned' + "userAssignedIdentities": None # {string: UserAssignedIdentity} } ContainerApp = { "location": None, - "identity": None, # ManagedServiceIdentity + "identity": None, # ManagedServiceIdentity "properties": { "managedEnvironmentId": None, - "configuration": None, # Configuration - "template": None # Template + "configuration": None, # Configuration + "template": None # Template }, "tags": None } DaprComponent = { "properties": { - "componentType": None, #String + "componentType": None, # String "version": None, - "ignoreErrors": None, + "ignoreErrors": None, "initTimeout": None, "secrets": None, "metadata": None, @@ -193,39 +194,39 @@ } DaprMetadata = { - "key": None, #str - "value": None, #str - "secret_ref": None #str + "key": None, # str + "value": None, # str + "secret_ref": None # str } SourceControl = { "properties": { - "repoUrl": None, - "branch": None, - "githubActionConfiguration": None # [GitHubActionConfiguration] + "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": 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 + "registryUrl": None, # str + "registryUserName": None, # str + "registryPassword": None # str } AzureCredentials = { - "clientId": None, # str - "clientSecret": None, # str - "tenantId": None, #str - "subscriptionId": None #str + "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 index 1d3e3b6dc27..169b65edbe5 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -2,18 +2,19 @@ # 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 +# pylint: disable=line-too-long, too-many-statements, consider-using-f-string from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import (resource_group_name_type, get_location_type, - get_resource_name_completion_list, file_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 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']) @@ -73,7 +74,7 @@ def load_arguments(self, _): c.argument('ingress', validator=validate_ingress, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") c.argument('target_port', type=int, validator=validate_target_port, options_list=['--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('assign_identity', nargs='+', help="Space-separated identities. Use '[system]' to refer to the system assigned identity.") 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'") @@ -101,7 +102,7 @@ def load_arguments(self, _): 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') + 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.') @@ -135,7 +136,7 @@ def load_arguments(self, _): 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.') @@ -162,7 +163,7 @@ def load_arguments(self, _): c.argument('dapr_app_port', help="The port of your app.") c.argument('dapr_app_protocol', help="Tells 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.") + 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.") @@ -171,4 +172,3 @@ def load_arguments(self, _): 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') - diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py index 9472034039d..b34325cdb9c 100644 --- a/src/containerapp/azext_containerapp/_sdk_models.py +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -1,9 +1,15 @@ +# -------------------------------------------------------------------------------------------- +# 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 @@ -196,8 +202,8 @@ class ProxyResource(Resource): 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, **kwargs): - super(ProxyResource, self).__init__(**kwargs) + # def __init__(self, **kwargs): + # super(ProxyResource, self).__init__(**kwargs) class AuthConfig(ProxyResource): diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 1c5a10e5d29..b1b3fa9bf9a 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -2,15 +2,14 @@ # 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 distutils.filelist import findall -from operator import is_ +from urllib.parse import urlparse from azure.cli.command_modules.appservice.custom import (_get_acr_cred) -from azure.cli.core.azclierror import (ResourceNotFoundError, ValidationError, RequiredArgumentMissingError) +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 urllib.parse import urlparse 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 @@ -38,7 +37,7 @@ def _validate_subscription_registered(cmd, resource_provider, subscription_id=No subscription_id, resource_provider, resource_provider)) except ValidationError as ex: raise ex - except Exception: + except Exception: # pylint: disable=broad-except pass @@ -62,7 +61,7 @@ def _ensure_location_allowed(cmd, location, resource_provider, resource_type): location, resource_provider, resource_type)) except ValidationError as ex: raise ex - except Exception: + except Exception: # pylint: disable=broad-except pass @@ -76,7 +75,7 @@ def parse_env_var_flags(env_list, is_update_containerapp=False): 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])) + 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 @@ -104,7 +103,7 @@ def parse_secret_flags(secret_list): 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])) + 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 = [] @@ -116,13 +115,23 @@ def parse_secret_flags(secret_list): return secret_var_def + def _update_revision_env_secretrefs(containers, name): for container in containers: - if "env" in container: + 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 @@ -139,33 +148,34 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ 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)) - secrets_list.append({ - "name": registry_secret_name, - "value": registry_pass - }) + 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 + 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. " @@ -173,6 +183,7 @@ def raise_missing_token_suggestion(): "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 @@ -184,20 +195,23 @@ def _get_default_log_analytics_location(cmd): if res and getattr(res, 'resource_type', "") == "workspaces": res_locations = getattr(res, 'locations', []) - if len(res_locations): + if len(res_locations) > 0: location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") if location: return location - except Exception: + except Exception: # pylint: disable=broad-except return default_location return default_location + # Generate random 4 character string def _new_tiny_guid(): - import random, string + 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 @@ -229,7 +243,7 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc log_analytics_location = location try: _ensure_location_allowed(cmd, log_analytics_location, "Microsoft.OperationalInsights", "workspaces") - except Exception: + except Exception: # pylint: disable=broad-except log_analytics_location = _get_default_log_analytics_location(cmd) from azure.cli.core.commands import LongRunningOperation @@ -237,7 +251,7 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc 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)) + 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) @@ -248,10 +262,10 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc 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") + 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 + 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) @@ -285,11 +299,12 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): secrets = [] try: secrets = ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name) - except Exception as e: + 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): @@ -301,6 +316,7 @@ def _ensure_identity_resource_id(subscription_id, resource_group, resource): 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"] = [] @@ -312,28 +328,31 @@ def _add_or_update_secrets(containerapp_def, add_secrets): 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): + 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 i in range(0, len(containerapp_def["properties"]["configuration"]["secrets"])): - existing_secret = containerapp_def["properties"]["configuration"]["secrets"][i] + 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(i) + 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: @@ -343,7 +362,7 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars, is_add=False): 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"])) + 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"] @@ -359,16 +378,17 @@ def _add_or_update_env_vars(existing_env_vars, new_env_vars, is_add=False): # 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"])) + 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 in range(0, len(existing_env_vars)): - existing_env_var = existing_env_vars[i] + 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) @@ -376,7 +396,25 @@ def _remove_env_vars(existing_env_vars, remove_env_vars): # 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)) + 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: @@ -439,6 +477,7 @@ def _remove_readonly_attributes(containerapp_def): elif unneeded_property in containerapp_def['properties']: del containerapp_def['properties'][unneeded_property] + def _remove_dapr_readonly_attributes(daprcomponent_def): unneeded_properties = [ "id", @@ -457,13 +496,14 @@ def _remove_dapr_readonly_attributes(daprcomponent_def): 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 import collections for key, val in new_dict.items(): if isinstance(val, collections.Mapping): - tmp = update_nested_dictionary(orig_dict.get(key, { }), val) + tmp = update_nested_dictionary(orig_dict.get(key, {}), val) orig_dict[key] = tmp elif isinstance(val, list): if new_dict[key]: @@ -477,7 +517,7 @@ def update_nested_dictionary(orig_dict, new_dict): def _is_valid_weight(weight): try: n = int(weight) - if n >= 0 and n <= 100: + if 0 <= n <= 100: return True return False except ValueError: @@ -527,7 +567,7 @@ def _infer_acr_credentials(cmd, registry_server): 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)) + 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): diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 916d9eb5b57..e7fe0435a11 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -2,9 +2,9 @@ # 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 unicodedata import name -from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) +from azure.cli.core.azclierror import (ValidationError) def _is_number(s): @@ -14,6 +14,7 @@ def _is_number(s): except ValueError: return False + def validate_memory(namespace): memory = namespace.memory @@ -26,13 +27,15 @@ def validate_memory(namespace): 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: - raise ValidationError("Usage error: --cpu must be a number eg. \"0.5\"") + 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 @@ -48,6 +51,7 @@ def validate_managed_env_name_or_id(cmd, namespace): name=namespace.managed_env ) + def validate_registry_server(namespace): if "create" in namespace.command.lower(): if namespace.registry_server: @@ -55,24 +59,28 @@ def validate_registry_server(namespace): 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: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 9fd58c7575c..87a892201a8 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -# pylint: disable=line-too-long -from azure.cli.core.commands import CliCommandType -from msrestazure.tools import is_valid_resource_id, parse_resource_id +# 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 @@ -15,7 +15,7 @@ def transform_containerapp_output(app): try: result['fqdn'] = app['properties']['configuration']['ingress']['fqdn'] - except Exception: + except: result['fqdn'] = None return result @@ -48,7 +48,7 @@ def load_command_table(self, _): 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', exception_handler=ex_handler_factory()) + g.custom_command('delete', 'delete_containerapp', confirmation=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: g.custom_command('show', 'show_managed_environment') @@ -70,8 +70,8 @@ def load_command_table(self, _): 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_command('show', 'show_github_action', exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_github_action', exception_handler=ex_handler_factory()) + g.custom_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') @@ -86,7 +86,7 @@ def load_command_table(self, _): g.custom_command('enable', 'enable_ingress', exception_handler=ex_handler_factory()) g.custom_command('disable', 'disable_ingress', exception_handler=ex_handler_factory()) g.custom_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_command('show', 'show_ingress_traffic') @@ -106,4 +106,3 @@ def load_command_table(self, _): 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 index 961fbd500f0..d19ff49ea69 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2,23 +2,21 @@ # 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, ResourceNotFoundError, ValidationError) +from azure.cli.core.azclierror import (RequiredArgumentMissingError, ValidationError) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.core.util import sdk_no_wait from knack.util import CLIError from knack.log import get_logger -from urllib.parse import urlparse from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError -from azure.cli.command_modules.appservice.custom import _get_acr_cred -from urllib.parse import urlparse from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient -from ._sdk_models import * +# from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, @@ -33,19 +31,19 @@ Dapr as DaprModel, ContainerResources as ContainerResourcesModel, Scale as ScaleModel, - Container as ContainerModel, - GitHubActionConfiguration, - RegistryInfo as RegistryInfoModel, - AzureCredentials as AzureCredentialsModel, + 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_list_of_strings, 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) + 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__) @@ -70,19 +68,20 @@ def load_yaml_file(file_name): import errno try: - with open(file_name) as stream: + 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 CLIError('{} does not exist'.format(file_name)) + raise CLIError('{} does not exist'.format(file_name)) from ex raise except (yaml.parser.ParserError, UnicodeDecodeError) as ex: - raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) + raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) from ex def create_deserializer(): from msrest import Deserializer - import sys, inspect + import sys + import inspect sdkClasses = inspect.getmembers(sys.modules["azext_containerapp._sdk_models"]) deserializer = {} @@ -95,7 +94,7 @@ def create_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: + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') if not yaml_containerapp.get('name'): @@ -114,7 +113,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev containerapp_def = None try: current_containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except Exception as ex: + except Exception: pass if not current_containerapp_def: @@ -126,17 +125,16 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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 @@ -158,54 +156,6 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev _remove_additional_attributes(current_containerapp_def) _remove_readonly_attributes(current_containerapp_def) - ''' - # Not sure if update should replace items that are a list, or do createOrUpdate. This commented out section is the implementation for createOrUpdate. - # (If a property is a list, do createOrUpdate, rather than just replace with new list) - - if 'properties' in containerapp_def and 'template' in containerapp_def['properties']: - # Containers - if 'containers' in containerapp_def['properties']['template'] and containerapp_def['properties']['template']['containers']: - for new_container in containerapp_def['properties']['template']['containers']: - if "name" not in new_container or not new_container["name"]: - raise ValidationError("The container name is not specified.") - - # Check if updating existing container - updating_existing_container = False - for existing_container in current_containerapp_def["properties"]["template"]["containers"]: - if existing_container['name'].lower() == new_container['name'].lower(): - updating_existing_container = True - - if 'image' in new_container and new_container['image']: - existing_container['image'] = new_container['image'] - if 'env' in new_container and new_container['env']: - if 'env' not in existing_container or not existing_container['env']: - existing_container['env'] = [] - _add_or_update_env_vars(existing_container['env'], new_container['env']) - if 'command' in new_container and new_container['command']: - existing_container['command'] = new_container['command'] - if 'args' in new_container and new_container['args']: - existing_container['args'] = new_container['args'] - if 'resources' in new_container and new_container['resources']: - if 'cpu' in new_container['resources'] and new_container['resources']['cpu'] is not None: - existing_container['resources']['cpu'] = new_container['resources']['cpu'] - if 'memory' in new_container['resources'] and new_container['resources']['memory'] is not None: - existing_container['resources']['memory'] = new_container['resources']['memory'] - - # If not updating existing container, add as new container - if not updating_existing_container: - current_containerapp_def["properties"]["template"]["containers"].append(new_container) - - # Traffic Weights - - # Secrets - - # Registries - - # Scale rules - - # Source Controls - - ''' 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) @@ -222,7 +172,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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: + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') if not yaml_containerapp.get('name'): @@ -244,7 +194,7 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= containerapp_def = deserializer('ContainerApp', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples 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 @@ -271,7 +221,7 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= env_rg = None env_info = None - if (is_valid_resource_id(env_id)): + 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'] @@ -334,14 +284,14 @@ def create_containerapp(cmd, args=None, tags=None, no_wait=False, - assign_identity=[]): + assign_identity=None): _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\ - location or startup_command or args or tags: + 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) @@ -351,6 +301,9 @@ def create_containerapp(cmd, if managed_env is None: raise RequiredArgumentMissingError('Usage error: --environment is required if not using --yaml') + if assign_identity is None: + assign_identity = [] + # Validate managed environment parsed_managed_env = parse_resource_id(managed_env) managed_env_name = parsed_managed_env['name'] @@ -416,19 +369,19 @@ def create_containerapp(cmd, if assign_system_identity and assign_user_identities: identity_def["type"] = "SystemAssigned, UserAssigned" - elif assign_system_identity: + elif assign_system_identity: identity_def["type"] = "SystemAssigned" - elif assign_user_identities: + elif assign_user_identities: identity_def["type"] = "UserAssigned" if assign_user_identities: identity_def["userAssignedIdentities"] = {} subscription_id = get_subscription_id(cmd.cli_ctx) - + for r in assign_user_identities: r = _ensure_identity_resource_id(subscription_id, resource_group_name, r) - identity_def["userAssignedIdentities"][r] = {} - + identity_def["userAssignedIdentities"][r] = {} # pylint: disable=unsupported-assignment-operation + scale_def = None if min_replicas is not None or max_replicas is not None: scale_def = ScaleModel @@ -517,9 +470,9 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ - registry_user or registry_pass or\ - startup_command or args or tags: + revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ + registry_user or registry_pass 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) @@ -771,20 +724,20 @@ def delete_containerapp(cmd, name, resource_group_name): 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, - app_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): + name, + resource_group_name, + logs_customer_id=None, + logs_key=None, + location=None, + instrumentation_key=None, + infrastructure_subnet_resource_id=None, + app_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) @@ -793,7 +746,7 @@ def create_managed_environment(cmd, # Microsoft.ContainerService RP registration is required for vnet enabled environments if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: - if (is_valid_resource_id(app_subnet_resource_id)): + if is_valid_resource_id(app_subnet_resource_id): parsed_app_subnet_resource_id = parse_resource_id(app_subnet_resource_id) subnet_subscription = parsed_app_subnet_resource_id["subscription"] _validate_subscription_registered(cmd, "Microsoft.ContainerService", subnet_subscription) @@ -862,28 +815,12 @@ def create_managed_environment(cmd, def update_managed_environment(cmd, - name, - resource_group_name, - tags=None, - no_wait=False): + name, + resource_group_name, + tags=None, + no_wait=False): raise CLIError('Containerapp env update is not yet supported.') - _validate_subscription_registered(cmd, "Microsoft.App") - - managed_env_def = ManagedEnvironmentModel - managed_env_def["tags"] = tags - - try: - r = ManagedEnvironmentClient.update( - 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 update in progress. Please monitor the creation using `az containerapp env show -n {} -g {}`'.format(name, resource_group_name)) - - return r - except Exception as e: - handle_raw_exception(e) - def show_managed_environment(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -925,9 +862,9 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ 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_system_identity = '[system]' in identities assign_user_identities = [x for x in identities if x != '[system]'] containerapp_def = None @@ -944,7 +881,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) # If identity not returned - try: + try: containerapp_def["identity"] containerapp_def["identity"]["type"] except: @@ -956,30 +893,30 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ # Assign correct type try: - if containerapp_def["identity"]["type"] != "None": + 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: + else: if assign_system_identity and assign_user_identities: containerapp_def["identity"]["type"] = "SystemAssigned,UserAssigned" - elif assign_system_identity: + elif assign_system_identity: containerapp_def["identity"]["type"] = "SystemAssigned" - elif assign_user_identities: + elif assign_user_identities: containerapp_def["identity"]["type"] = "UserAssigned" - except: - # Always returns "type": "None" when CA has no previous identities + except: + # Always returns "type": "None" when CA has no previous identities pass - + if assign_user_identities: - try: + try: containerapp_def["identity"]["userAssignedIdentities"] - except: + 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") @@ -987,7 +924,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ containerapp_def["identity"]["userAssignedIdentities"][r] logger.warning("User identity {} is already assigned to containerapp".format(old_id)) except: - containerapp_def["identity"]["userAssignedIdentities"][r] = {} + 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) @@ -997,7 +934,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ 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") @@ -1010,7 +947,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= 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: @@ -1024,7 +961,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) # If identity not returned - try: + try: containerapp_def["identity"] containerapp_def["identity"]["type"] except: @@ -1041,17 +978,17 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= if remove_user_identities: subscription_id = get_subscription_id(cmd.cli_ctx) - try: + try: containerapp_def["identity"]["userAssignedIdentities"] - except: + except: containerapp_def["identity"]["userAssignedIdentities"] = {} - for id in remove_user_identities: - given_id = id - id = _ensure_identity_resource_id(subscription_id, resource_group_name, id) + 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() == id.lower(): + if old_user_identity.lower() == remove_id.lower(): containerapp_def["identity"]["userAssignedIdentities"].pop(old_user_identity) wasRemoved = True break @@ -1062,14 +999,14 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= 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") @@ -1080,10 +1017,12 @@ def show_managed_identity(cmd, name, resource_group_name): try: return r["identity"] - except: + except: r["identity"] = {} r["identity"]["type"] = "None" return r["identity"] + + def create_or_update_github_action(cmd, name, resource_group_name, @@ -1109,13 +1048,13 @@ def create_or_update_github_action(cmd, try: # Verify github repo from github import Github, GithubException - from github.GithubException import BadCredentialsException, UnknownObjectException + 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 @@ -1129,32 +1068,31 @@ def create_or_update_github_action(cmd, 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 CLIError(error_msg) + raise CLIError(error_msg) from e logger.warning('Verified GitHub repo and branch') - except BadCredentialsException: + except BadCredentialsException as e: raise CLIError("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.") + "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 CLIError(error_msg) + raise CLIError(error_msg) from e except CLIError as clierror: raise clierror - except Exception as ex: + 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 = client.get_source_control_info(resource_group_name, name).properties 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\"') + 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 @@ -1185,7 +1123,7 @@ def create_or_update_github_action(cmd, 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') + 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 @@ -1201,13 +1139,13 @@ def create_or_update_github_action(cmd, 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) + 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) @@ -1217,7 +1155,7 @@ def show_github_action(cmd, name, resource_group_name): 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: + try: github_action_config = GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) except Exception as e: handle_raw_exception(e) @@ -1236,13 +1174,13 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ try: # Verify github repo from github import Github, GithubException - from github.GithubException import BadCredentialsException, UnknownObjectException + 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 @@ -1250,21 +1188,21 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ github_repo = g.get_repo(repo) if not github_repo.permissions.push or not github_repo.permissions.maintain: raise CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) - except BadCredentialsException: + except BadCredentialsException as e: raise CLIError("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.") + "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 CLIError(error_msg) + raise CLIError(error_msg) from e except CLIError as clierror: raise clierror - except Exception as ex: + 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: @@ -1309,6 +1247,7 @@ def activate_revision(cmd, resource_group_name, revision_name, name=None): 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) @@ -1318,10 +1257,11 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): except CLIError as e: handle_raw_exception(e) + def copy_revision(cmd, resource_group_name, from_revision=None, - #label=None, + # label=None, name=None, yaml=None, image=None, @@ -1351,7 +1291,7 @@ def copy_revision(cmd, 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: + 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) @@ -1519,6 +1459,7 @@ def copy_revision(cmd, 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") @@ -1539,9 +1480,10 @@ def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): 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: + except Exception as e: handle_raw_exception(e) + def show_ingress(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1556,10 +1498,11 @@ def show_ingress(cmd, name, resource_group_name): try: return containerapp_def["properties"]["configuration"]["ingress"] - except: - raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) + except Exception as e: + raise CLIError("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): +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 @@ -1585,7 +1528,7 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, 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) @@ -1594,9 +1537,10 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, 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"] - except Exception as e: + 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") @@ -1614,13 +1558,14 @@ def disable_ingress(cmd, name, resource_group_name, no_wait=False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) try: - r = ContainerAppClient.create_or_update( + 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: + 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") @@ -1635,8 +1580,8 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait try: containerapp_def["properties"]["configuration"]["ingress"] - except: - raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") + except Exception as e: + raise CLIError("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) @@ -1647,9 +1592,10 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait 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: + except Exception as e: handle_raw_exception(e) + def show_ingress_traffic(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1664,8 +1610,9 @@ def show_ingress_traffic(cmd, name, resource_group_name): try: return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] - except: - raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") + except Exception as e: + raise CLIError("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") @@ -1681,8 +1628,8 @@ def show_registry(cmd, name, resource_group_name, server): try: containerapp_def["properties"]["configuration"]["registries"] - except: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) + except Exception as e: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) from e registries_def = containerapp_def["properties"]["configuration"]["registries"] @@ -1691,6 +1638,7 @@ def show_registry(cmd, name, resource_group_name, server): return r raise CLIError("The containerapp {} does not have specified registry assigned.".format(name)) + def list_registry(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1705,8 +1653,9 @@ def list_registry(cmd, name, resource_group_name): try: return containerapp_def["properties"]["configuration"]["registries"] - except: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) + except Exception as e: + raise CLIError("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") @@ -1741,7 +1690,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password 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') + 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 @@ -1770,10 +1719,9 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password server, password, update_existing_secret=True) - # Should this be false? ^ 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) @@ -1782,6 +1730,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password 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") @@ -1797,18 +1746,17 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) registries_def = None - registry = None try: containerapp_def["properties"]["configuration"]["registries"] - except: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) + except Exception as e: + raise CLIError("The containerapp {} has no assigned registries.".format(name)) from e registries_def = containerapp_def["properties"]["configuration"]["registries"] wasRemoved = False - for i in range(0, len(registries_def)): - r = registries_def[i] + 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"]) @@ -1827,8 +1775,9 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): logger.warning("Registry successfully removed.") return r["properties"]["configuration"]["registries"] # No registries to return, so return nothing - except Exception as e: - return + except Exception: + pass + def list_secrets(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App") @@ -1844,8 +1793,9 @@ def list_secrets(cmd, name, resource_group_name): try: return ContainerAppClient.list_secrets(cmd=cmd, resource_group_name=resource_group_name, name=name)["value"] - except: - raise CLIError("The containerapp {} has no assigned secrets.".format(name)) + except Exception as e: + raise CLIError("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") @@ -1865,7 +1815,8 @@ def show_secret(cmd, name, resource_group_name, secret_name): return secret raise CLIError("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): + +def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1900,10 +1851,10 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait = False except Exception as e: handle_raw_exception(e) -def set_secrets(cmd, name, resource_group_name, secrets, - #secrets=None, - #yaml=None, - no_wait = False): + +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: @@ -1911,7 +1862,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, # if not secrets: # secrets = [] - + # if yaml: # yaml_secrets = load_yaml_file(yaml).split(' ') # try: @@ -1940,6 +1891,7 @@ def set_secrets(cmd, name, resource_group_name, 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") @@ -1956,13 +1908,13 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= if 'dapr' not in containerapp_def['properties']: containerapp_def['properties']['dapr'] = {} - + if dapr_app_id: containerapp_def['properties']['dapr']['dapr_app_id'] = dapr_app_id - + if dapr_app_port: containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port - + if dapr_app_protocol: containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol @@ -1975,7 +1927,8 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= except Exception as e: handle_raw_exception(e) -def disable_dapr(cmd, name, resource_group_name, no_wait=False): + +def disable_dapr(cmd, name, resource_group_name, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1998,21 +1951,24 @@ def disable_dapr(cmd, name, resource_group_name, no_wait=False): 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: + if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') # Deserialize the yaml into a DaprComponent object. Need this since we're not using SDK @@ -2022,9 +1978,8 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, daprcomponent_def = deserializer('DaprComponent', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') from ex - #daprcomponent_def = _object_to_dict(daprcomponent_def) 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 @@ -2044,13 +1999,14 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, 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: + try: DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) - except: - raise CLIError("Dapr component not found.") + except Exception as e: + raise CLIError("Dapr component not found.") from e try: r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) @@ -2058,4 +2014,3 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ return r except Exception as e: handle_raw_exception(e) - From a607ed90bfcf543921110d2eb5cf2dca9b6cc1b3 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Fri, 18 Mar 2022 14:52:03 -0400 Subject: [PATCH 145/177] Update src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py Co-authored-by: Xing Zhou --- .../tests/latest/test_containerapp_scenario.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py index f18855ca4eb..8605f1fe426 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -6,7 +6,7 @@ import os import unittest -from azure_devtools.scenario_tests import AllowLargeResponse +from azure.cli.testsdk.scenario_tests import AllowLargeResponse from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) From 9652f3e1740bb008f3845127dc46d98aa1602ab2 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:09:30 -0400 Subject: [PATCH 146/177] Specific Error Types + Bugfixes (Help, remove app-subnet-resource-id, removed env-var alias, added help text for --name) (#28) * Moved dapr arguments to env as a subgroup. * Added env variable options. * Changed revision mode set to revision set-mode. * Added env var options to revision copy. * Fixed revision copy bug related to env secret refs. * Changed registry and secret delete to remove. Added registry param helps. Removed replica from table output and added trafficWeight. * Updating warning text. * Updated warning text once more. * Made name optional for revision copy if from-revision flag is passed. * Fixed whitespace style issues. * Styled clients and utils to pass pylint. * Finished client.py pylint fixes. * Fixed pylint issues. * Fixed flake8 commands and custom. * Fixed flake issues in src. * Added license header to _sdk_models. * Added confirmation for containerapp delete. * Update helps for identity, revision. Removed env-var alias for set-env-vars. Added name param help. * Removed app-subnet-resource-id. * Updated infrastructure subnet param help. * Check if containerapp resource exists before attempting to delete. * Added check before deleting managed env. * Changed error types to be more specific. * Removed check before deletion. Removed comments. Co-authored-by: Haroon Feisal --- .../azext_containerapp/_client_factory.py | 11 +- src/containerapp/azext_containerapp/_help.py | 18 ++- .../azext_containerapp/_params.py | 6 +- src/containerapp/azext_containerapp/custom.py | 150 ++++++++---------- 4 files changed, 87 insertions(+), 98 deletions(-) diff --git a/src/containerapp/azext_containerapp/_client_factory.py b/src/containerapp/azext_containerapp/_client_factory.py index 9a249cdbe7e..4e8ad424138 100644 --- a/src/containerapp/azext_containerapp/_client_factory.py +++ b/src/containerapp/azext_containerapp/_client_factory.py @@ -6,8 +6,7 @@ from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.profiles import ResourceType - -from knack.util import CLIError +from azure.cli.core.azclierror import CLIInternalError # pylint: disable=inconsistent-return-statements @@ -21,7 +20,7 @@ def _polish_bad_errors(ex): elif 'Message' in content: detail = content['Message'] - ex = CLIError(detail) + ex = CLIInternalError(detail) except Exception: # pylint: disable=broad-except pass if no_throw: @@ -45,13 +44,13 @@ def handle_raw_exception(e): if 'code' in jsonError and 'message' in jsonError: code = jsonError['code'] message = jsonError['message'] - raise CLIError('({}) {}'.format(code, message)) + raise CLIInternalError('({}) {}'.format(code, message)) elif "Message" in jsonError: message = jsonError["Message"] - raise CLIError(message) + raise CLIInternalError(message) elif "message" in jsonError: message = jsonError["message"] - raise CLIError(message) + raise CLIInternalError(message) raise e diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a4a71960f02..a306f2f6bd7 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -124,7 +124,7 @@ examples: - name: Restart a revision. text: | - az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision + az containerapp revision restart -n MyContainerapp -g MyResourceGroup --revision MyContainerappRevision """ helps['containerapp revision activate'] = """ @@ -133,7 +133,7 @@ examples: - name: Activate a revision. text: | - az containerapp revision activate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision + az containerapp revision activate -g MyResourceGroup --revision MyContainerappRevision """ helps['containerapp revision deactivate'] = """ @@ -142,7 +142,7 @@ examples: - name: Deactivate a revision. text: | - az containerapp revision deactivate -n MyContainerapp -g MyResourceGroup --revision-name MyContainerappRevision + az containerapp revision deactivate -g MyResourceGroup --revision MyContainerappRevision """ helps['containerapp revision set-mode'] = """ @@ -158,10 +158,15 @@ type: command short-summary: Create a revision based on a previous revision. examples: - - name: Create a revision based on a previous revision. + - 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 + """ helps['containerapp revision copy'] = """ @@ -231,7 +236,7 @@ helps['containerapp env dapr-component'] = """ type: group - short-summary: Commands to manage Container App environment dapr components. + short-summary: Commmands to manage dapr components on the Container App environment. """ helps['containerapp env dapr-component list'] = """ @@ -284,6 +289,9 @@ - name: Assign system identity. text: | az containerapp identity assign + - name: Assign user identity. + text: | + az containerapp identity assign --identities myAssignedId - name: Assign system and user identity. text: | az containerapp identity assign --identities [system] myAssignedId diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 169b65edbe5..0179e1f77f7 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -21,7 +21,7 @@ def load_arguments(self, _): with self.argument_context('containerapp') as c: # Base arguments - c.argument('name', name_type, metavar='NAME', id_part='name') + 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)) @@ -43,7 +43,7 @@ def load_arguments(self, _): # Env vars with self.argument_context('containerapp', arg_group='Environment variables (Creates new revision)') as c: - c.argument('set_env_vars', options_list=['--set-env-vars, --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('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.") @@ -97,7 +97,7 @@ def load_arguments(self, _): c.argument('instrumentation_key', options_list=['--dapr-instrumentation-key'], help='Azure Monitor 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. This subnet must be in the same VNET as the subnet defined in appSubnetResourceId.') + 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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d19ff49ea69..657b5995e03 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -6,9 +6,8 @@ from urllib.parse import urlparse from azure.cli.command_modules.appservice.custom import (_get_acr_cred) -from azure.cli.core.azclierror import (RequiredArgumentMissingError, ValidationError) +from azure.cli.core.azclierror import (RequiredArgumentMissingError, ValidationError, ResourceNotFoundError, CLIInternalError, InvalidArgumentValueError) from azure.cli.core.commands.client_factory import get_subscription_id -from knack.util import CLIError from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id @@ -72,10 +71,10 @@ def load_yaml_file(file_name): return yaml.safe_load(stream) except (IOError, OSError) as ex: if getattr(ex, 'errno', 0) == errno.ENOENT: - raise CLIError('{} does not exist'.format(file_name)) from ex + raise ValidationError('{} does not exist'.format(file_name)) from ex raise except (yaml.parser.ParserError, UnicodeDecodeError) as ex: - raise CLIError('Error parsing {} ({})'.format(file_name, str(ex))) from ex + raise ValidationError('Error parsing {} ({})'.format(file_name, str(ex))) from ex def create_deserializer(): @@ -123,7 +122,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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: + except CLIInternalError as e: handle_raw_exception(e) _update_revision_env_secretrefs(r["properties"]["template"]["containers"], name) current_containerapp_def["properties"]["template"] = r["properties"]["template"] @@ -483,7 +482,7 @@ def update_containerapp(cmd, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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"]: @@ -695,7 +694,7 @@ def show_containerapp(cmd, name, resource_group_name): try: return ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -710,7 +709,7 @@ def list_containerapp(cmd, resource_group_name=None): containerapps = ContainerAppClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return containerapps - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -719,7 +718,7 @@ def delete_containerapp(cmd, name, resource_group_name): try: return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -731,7 +730,6 @@ def create_managed_environment(cmd, location=None, instrumentation_key=None, infrastructure_subnet_resource_id=None, - app_subnet_resource_id=None, docker_bridge_cidr=None, platform_reserved_cidr=None, platform_reserved_dns_ip=None, @@ -744,15 +742,6 @@ def create_managed_environment(cmd, _validate_subscription_registered(cmd, "Microsoft.App") _ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments") - # Microsoft.ContainerService RP registration is required for vnet enabled environments - if infrastructure_subnet_resource_id is not None or app_subnet_resource_id is not None: - if is_valid_resource_id(app_subnet_resource_id): - parsed_app_subnet_resource_id = parse_resource_id(app_subnet_resource_id) - subnet_subscription = parsed_app_subnet_resource_id["subscription"] - _validate_subscription_registered(cmd, "Microsoft.ContainerService", subnet_subscription) - else: - raise ValidationError('Subnet resource ID is invalid.') - 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) @@ -773,19 +762,12 @@ def create_managed_environment(cmd, if instrumentation_key is not None: managed_env_def["properties"]["daprAIInstrumentationKey"] = instrumentation_key - if infrastructure_subnet_resource_id or app_subnet_resource_id or docker_bridge_cidr or platform_reserved_cidr or platform_reserved_dns_ip: + 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: - if not app_subnet_resource_id: - raise ValidationError('App subnet resource ID needs to be supplied with infrastructure subnet resource ID.') vnet_config_def["infrastructureSubnetId"] = infrastructure_subnet_resource_id - if app_subnet_resource_id is not None: - if not infrastructure_subnet_resource_id: - raise ValidationError('Infrastructure subnet resource ID needs to be supplied with app subnet resource ID.') - vnet_config_def["runtimeSubnetId"] = app_subnet_resource_id - if docker_bridge_cidr is not None: vnet_config_def["dockerBridgeCidr"] = docker_bridge_cidr @@ -798,8 +780,8 @@ def create_managed_environment(cmd, managed_env_def["properties"]["vnetConfiguration"] = vnet_config_def if internal_only: - if not infrastructure_subnet_resource_id or not app_subnet_resource_id: - raise ValidationError('Infrastructure subnet resource ID and App subnet resource ID need to be supplied for internal only environments.') + 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: @@ -819,7 +801,7 @@ def update_managed_environment(cmd, resource_group_name, tags=None, no_wait=False): - raise CLIError('Containerapp env update is not yet supported.') + raise CLIInternalError('Containerapp env update is not yet supported.') def show_managed_environment(cmd, name, resource_group_name): @@ -827,7 +809,7 @@ def show_managed_environment(cmd, name, resource_group_name): try: return ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -842,7 +824,7 @@ def list_managed_environments(cmd, resource_group_name=None): managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return managed_envs - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -851,7 +833,7 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): try: return ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -876,7 +858,7 @@ def assign_managed_identity(cmd, name, resource_group_name, identities=None, no_ pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -956,7 +938,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -969,11 +951,11 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= containerapp_def["identity"]["type"] = "None" if containerapp_def["identity"]["type"] == "None": - raise CLIError("The containerapp {} has no system or user assigned identities.".format(name)) + raise InvalidArgumentValueError("The containerapp {} has no system or user assigned identities.".format(name)) if remove_system_identity: if containerapp_def["identity"]["type"] == "UserAssigned": - raise CLIError("The containerapp {} has no system assigned identities.".format(name)) + 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: @@ -994,7 +976,7 @@ def remove_managed_identity(cmd, name, resource_group_name, identities, no_wait= break if not wasRemoved: - raise CLIError("The containerapp does not have specified user identity '{}' assigned, so it cannot be removed.".format(given_id)) + 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 @@ -1012,7 +994,7 @@ def show_managed_identity(cmd, name, resource_group_name): try: r = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) try: @@ -1061,25 +1043,25 @@ def create_or_update_github_action(cmd, try: github_repo = g.get_repo(repo) if not github_repo.permissions.push or not github_repo.permissions.maintain: - raise CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) + 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 CLIError(error_msg) from e + raise CLIInternalError(error_msg) from e logger.warning('Verified GitHub repo and branch') except BadCredentialsException as e: - raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " + 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 CLIError(error_msg) from e - except CLIError as clierror: + raise CLIInternalError(error_msg) from e + except CLIInternalError 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 @@ -1187,17 +1169,17 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ try: github_repo = g.get_repo(repo) if not github_repo.permissions.push or not github_repo.permissions.maintain: - raise CLIError("The token does not have appropriate access rights to repository {}.".format(repo)) + raise ValidationError("The token does not have appropriate access rights to repository {}.".format(repo)) except BadCredentialsException as e: - raise CLIError("Could not authenticate to the repository. Please create a Personal Access Token and use " + 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 CLIError(error_msg) from e - except CLIError as clierror: + raise CLIInternalError(error_msg) from e + except CLIInternalError 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 @@ -1214,7 +1196,7 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ 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: + except CLIInternalError as e: handle_raw_exception(e) @@ -1224,7 +1206,7 @@ def show_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1234,7 +1216,7 @@ def restart_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.restart_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1244,7 +1226,7 @@ def activate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.activate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1254,7 +1236,7 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.deactivate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIError as e: + except CLIInternalError as e: handle_raw_exception(e) @@ -1302,12 +1284,12 @@ def copy_revision(cmd, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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: + except CLIInternalError as e: # Error handle the case where revision not found? handle_raw_exception(e) @@ -1470,7 +1452,7 @@ def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = mode.lower() @@ -1494,12 +1476,12 @@ def show_ingress(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: return containerapp_def["properties"]["configuration"]["ingress"] except Exception as e: - raise CLIError("The containerapp '{}' does not have ingress enabled.".format(name)) from 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 @@ -1512,7 +1494,7 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) external_ingress = None if type is not None: @@ -1551,7 +1533,7 @@ def disable_ingress(cmd, name, resource_group_name, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) containerapp_def["properties"]["configuration"]["ingress"] = None @@ -1576,12 +1558,12 @@ def set_ingress_traffic(cmd, name, resource_group_name, traffic_weights, no_wait pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: containerapp_def["properties"]["configuration"]["ingress"] except Exception as e: - raise CLIError("Ingress must be enabled to set ingress traffic. Try running `az containerapp ingress -h` for more info.") from 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) @@ -1606,12 +1588,12 @@ def show_ingress_traffic(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: return containerapp_def["properties"]["configuration"]["ingress"]["traffic"] except Exception as e: - raise CLIError("Ingress must be enabled to show ingress traffic. Try running `az containerapp ingress -h` for more info.") from 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): @@ -1624,19 +1606,19 @@ def show_registry(cmd, name, resource_group_name, server): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: containerapp_def["properties"]["configuration"]["registries"] except Exception as e: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) from 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 CLIError("The containerapp {} does not have specified registry assigned.".format(name)) + raise InvalidArgumentValueError("The containerapp {} does not have specified registry assigned.".format(name)) def list_registry(cmd, name, resource_group_name): @@ -1649,12 +1631,12 @@ def list_registry(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) try: return containerapp_def["properties"]["configuration"]["registries"] except Exception as e: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) from 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): @@ -1667,7 +1649,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1741,7 +1723,7 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1750,7 +1732,7 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): try: containerapp_def["properties"]["configuration"]["registries"] except Exception as e: - raise CLIError("The containerapp {} has no assigned registries.".format(name)) from e + raise ValidationError("The containerapp {} has no assigned registries.".format(name)) from e registries_def = containerapp_def["properties"]["configuration"]["registries"] @@ -1764,7 +1746,7 @@ def remove_registry(cmd, name, resource_group_name, server, no_wait=False): break if not wasRemoved: - raise CLIError("Containerapp does not have registry server {} assigned.".format(server)) + 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") @@ -1789,12 +1771,12 @@ def list_secrets(cmd, name, resource_group_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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 CLIError("The containerapp {} has no assigned secrets.".format(name)) from e + raise ValidationError("The containerapp {} has no assigned secrets.".format(name)) from e def show_secret(cmd, name, resource_group_name, secret_name): @@ -1807,13 +1789,13 @@ def show_secret(cmd, name, resource_group_name, secret_name): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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 CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + 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): @@ -1826,7 +1808,7 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1838,7 +1820,7 @@ def remove_secrets(cmd, name, resource_group_name, secret_names, no_wait=False): wasRemoved = True break if not wasRemoved: - raise CLIError("The containerapp {} does not have a secret assigned with name {}.".format(name, secret_name)) + 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) @@ -1868,7 +1850,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, # try: # parse_secret_flags(yaml_secrets) # except: - # raise CLIError("YAML secrets must be a list of secrets in key=value format, delimited by new line.") + # 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()) @@ -1879,7 +1861,7 @@ def set_secrets(cmd, name, resource_group_name, secrets, pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + 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)) @@ -1902,7 +1884,7 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -1938,7 +1920,7 @@ def disable_dapr(cmd, name, resource_group_name, no_wait=False): pass if not containerapp_def: - raise CLIError("The containerapp '{}' does not exist".format(name)) + raise ResourceNotFoundError("The containerapp '{}' does not exist".format(name)) _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) @@ -2006,7 +1988,7 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ try: DaprComponentClient.show(cmd, resource_group_name, environment_name, name=dapr_component_name) except Exception as e: - raise CLIError("Dapr component not found.") from e + raise ResourceNotFoundError("Dapr component not found.") from e try: r = DaprComponentClient.delete(cmd, resource_group_name, environment_name, name=dapr_component_name) From 46b5a948435934f32da0043fcaab2e539a98cae9 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 10:45:42 -0700 Subject: [PATCH 147/177] Reset to 0.1.0 version, remove unneeded options-list --- src/containerapp/HISTORY.rst | 14 +--- .../azext_containerapp/_params.py | 38 ++++----- src/containerapp/setup.py | 2 +- src/index.json | 82 ------------------- 4 files changed, 21 insertions(+), 115 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 8400a3f0baf..1c139576ba0 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,18 +3,6 @@ Release History =============== -0.1.2 -++++++ -* Various fixes for bugs found -* Dapr subgroup -* Managed Identity - -0.1.1 -++++++ -* Various fixes for az containerapp create, update -* Added github actions support -* Added subgroups for ingress, registry, revision, secret - 0.1.0 ++++++ -* Initial release. \ No newline at end of file +* Initial release. diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 0179e1f77f7..96ec69e5d8b 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -33,13 +33,13 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container (Creates new revision)') 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, options_list=['--container-name'], help="Name of the container.") - c.argument('cpu', type=float, validator=validate_cpu, options_list=['--cpu'], help="Required CPU in cores, e.g. 0.5") - c.argument('memory', type=str, validator=validate_memory, options_list=['--memory'], help="Required memory, e.g. 1.0Gi") - c.argument('env_vars', nargs='*', options_list=['--env-vars'], 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('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='*', options_list=['--args'], 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, options_list=['--revision-suffix'], help='User friendly suffix that is appended to the revision name') + 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 (Creates new revision)') as c: @@ -50,29 +50,29 @@ def load_arguments(self, _): # Scale with self.argument_context('containerapp', arg_group='Scale (Creates new revision)') as c: - c.argument('min_replicas', type=int, options_list=['--min-replicas'], help="The minimum number of replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") + 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()) - c.argument('dapr_app_port', type=int, options_list=['--dapr-app-port'], help="The port Dapr uses to talk to the application.") - c.argument('dapr_app_id', type=str, options_list=['--dapr-app-id'], help="The Dapr application identifier.") - c.argument('dapr_app_protocol', type=str, arg_type=get_enum_type(['http', 'grpc']), options_list=['--dapr-app-protocol'], help="The protocol Dapr uses to talk to the application.") - c.argument('dapr_components', options_list=['--dapr-components'], help="The name of a yaml file containing a list of dapr components.") + 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.") + c.argument('dapr_components', help="The name of a yaml file containing a list of dapr components.") # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: - c.argument('revisions_mode', arg_type=get_enum_type(['single', 'multiple']), options_list=['--revisions-mode'], help="The active revisions mode for the container app.") - c.argument('registry_server', type=str, validator=validate_registry_server, options_list=['--registry-server'], help="The container registry server hostname, e.g. myregistry.azurecr.io.") + 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, options_list=['--ingress'], default=None, arg_type=get_enum_type(['internal', 'external']), help="The ingress type.") - c.argument('target_port', type=int, validator=validate_target_port, options_list=['--target-port'], help="The application port used for ingress traffic.") + 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: @@ -80,8 +80,8 @@ def load_arguments(self, _): 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, options_list=['--min-replicas'], help="The minimum number of replicas.") - c.argument('max_replicas', type=int, options_list=['--max-replicas'], help="The maximum number of replicas.") + 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.') @@ -94,7 +94,7 @@ def load_arguments(self, _): 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='Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry') + 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.') diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 96524e9ab67..be4cd26f637 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.1.2' +VERSION = '0.1.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers diff --git a/src/index.json b/src/index.json index fc191117ee7..99a9d4d7594 100644 --- a/src/index.json +++ b/src/index.json @@ -12324,88 +12324,6 @@ "sha256Digest": "9a796d5187571990d27feb9efeedde38c194f13ea21cbf9ec06131196bfd821d" } ], - "containerapp": [ - { - "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.1-py2.py3-none-any.whl", - "filename": "containerapp-0.1.1-py2.py3-none-any.whl", - "metadata": { - "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67", - "extensions": { - "python.details": { - "contacts": [ - { - "email": "azpycli@microsoft.com", - "name": "Microsoft Corporation", - "role": "author" - } - ], - "document_names": { - "description": "DESCRIPTION.rst" - }, - "project_urls": { - "Home": "https://github.com/Azure/azure-cli-extensions" - } - } - }, - "extras": [], - "generator": "bdist_wheel (0.30.0)", - "license": "MIT", - "metadata_version": "2.0", - "name": "containerapp", - "run_requires": [ - { - "requires": [ - "azure-cli-core" - ] - } - ], - "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", - "version": "0.1.1" - }, - "sha256Digest": "9ca28bacd772b8c516d7d682ffe94665ff777774ab89602d4ca73c4ba16e0b9b" - }, - { - "downloadUrl": "https://containerappcli.blob.core.windows.net/containerapp/containerapp-0.1.2-py2.py3-none-any.whl", - "filename": "containerapp-0.1.2-py2.py3-none-any.whl", - "metadata": { - "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67", - "extensions": { - "python.details": { - "contacts": [ - { - "email": "azpycli@microsoft.com", - "name": "Microsoft Corporation", - "role": "author" - } - ], - "document_names": { - "description": "DESCRIPTION.rst" - }, - "project_urls": { - "Home": "https://github.com/Azure/azure-cli-extensions" - } - } - }, - "extras": [], - "generator": "bdist_wheel (0.30.0)", - "license": "MIT", - "metadata_version": "2.0", - "name": "containerapp", - "run_requires": [ - { - "requires": [ - "azure-cli-core" - ] - } - ], - "summary": "Microsoft Azure Command-Line Tools Containerapp Extension", - "version": "0.1.2" - }, - "sha256Digest": "b1d4cc823f761cfb5469f8d53a9fa04bdc1493c3c5d5f3a90333876287e7b2f8" - } - ], "cosmosdb-preview": [ { "downloadUrl": "https://azurecliprod.blob.core.windows.net/cli-extensions/cosmosdb_preview-0.1.0-py2.py3-none-any.whl", From a8e75ba4f2f20a0da6a27b87ed9e9ab7c4907120 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 10:59:15 -0700 Subject: [PATCH 148/177] Update min cli core version --- src/containerapp/azext_containerapp/azext_metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/azext_metadata.json b/src/containerapp/azext_containerapp/azext_metadata.json index 001f223de90..cf7b8927a07 100644 --- a/src/containerapp/azext_containerapp/azext_metadata.json +++ b/src/containerapp/azext_containerapp/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.isPreview": true, - "azext.minCliCoreVersion": "2.0.67" + "azext.minCliCoreVersion": "2.15.0" } From c1288b73294dc4e2b2744589045d8768432d6dbf Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:21:25 -0400 Subject: [PATCH 149/177] Fixed style issues. (#30) Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/custom.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 657b5995e03..06a9c922c3b 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1054,8 +1054,8 @@ def create_or_update_github_action(cmd, 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 + "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']: @@ -1172,8 +1172,8 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ 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 + "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']: From d4fbdaefee2ab07f5287ed9222bd2c17e16b620c Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 11:42:48 -0700 Subject: [PATCH 150/177] Fix linter issues --- src/containerapp/azext_containerapp/_help.py | 24 +++++-------------- .../azext_containerapp/_params.py | 7 ++++-- .../azext_containerapp/commands.py | 1 - 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a306f2f6bd7..2c6a5009069 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -64,14 +64,6 @@ text: az containerapp delete -g MyResourceGroup -n MyContainerapp """ -helps['containerapp scale'] = """ - type: command - short-summary: Set the min and max replicas for a container app (latest revision in multiple revisions mode). - examples: - - name: Scale a container's latest revision. - text: az containerapp scale -g MyResourceGroup -n MyContainerapp --min-replicas 1 --max-replicas 2 -""" - helps['containerapp show'] = """ type: command short-summary: Show details of a container app. @@ -106,7 +98,7 @@ - name: Show details of a revision. text: | az containerapp revision show -n MyContainerapp -g MyResourceGroup \\ - --revision-name MyContainerappRevision + --revision MyContainerappRevision """ helps['containerapp revision list'] = """ @@ -200,10 +192,6 @@ --location "Canada Central" """ -helps['containerapp env update'] = """ - type: command - short-summary: Update a Container Apps environment. Currently Unsupported. -""" helps['containerapp env delete'] = """ type: command @@ -245,7 +233,7 @@ examples: - name: List dapr components for a Containerapp environment. text: | - az containerapp env dapr-component list -g MyResourceGroup --environment-name MyEnvironment + az containerapp env dapr-component list -g MyResourceGroup --name MyEnvironment """ helps['containerapp env dapr-component show'] = """ @@ -254,7 +242,7 @@ examples: - name: Show the details of a dapr component. text: | - az containerapp env dapr-component show -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment + az containerapp env dapr-component show -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment """ helps['containerapp env dapr-component set'] = """ @@ -263,7 +251,7 @@ examples: - name: Create a dapr component. text: | - az containerapp env dapr-component set -g MyResourceGroup --environment-name MyEnv --yaml MyYAMLPath --name MyDaprName + az containerapp env dapr-component set -g MyResourceGroup --name MyEnv --yaml MyYAMLPath --dapr-component-name MyDaprComponentName """ helps['containerapp env dapr-component remove'] = """ @@ -272,7 +260,7 @@ examples: - name: Remove a dapr componenet from a Containerapp environment. text: | - az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --environment-name MyEnvironment + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --name MyEnvironment """ # Identity Commands @@ -303,7 +291,7 @@ examples: - name: Remove system identity. text: | - az containerapp identity remove [system] + az containerapp identity remove --identities [system] - name: Remove system and user identity. text: | az containerapp identity remove --identities [system] myAssignedId diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 96ec69e5d8b..e2006b28187 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -2,7 +2,7 @@ # 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 +# pylint: disable=line-too-long, too-many-statements, consider-using-f-string, option-length-too-long from knack.arguments import CLIArgumentType @@ -55,7 +55,7 @@ def load_arguments(self, _): # 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()) + 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.") @@ -155,6 +155,9 @@ def load_arguments(self, _): 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.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 87a892201a8..f2f67098d34 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -54,7 +54,6 @@ def load_command_table(self, _): g.custom_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('update', 'update_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: From 48f2eb9a9358f1ce23c745620d3da32a8eaba1aa Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 11:50:11 -0700 Subject: [PATCH 151/177] Use custom-show-command --- .../azext_containerapp/commands.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index f2f67098d34..4cff20cf47e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -44,32 +44,32 @@ def transform_revision_list_output(revs): def load_command_table(self, _): with self.command_group('containerapp') as g: - g.custom_command('show', 'show_containerapp', table_transformer=transform_containerapp_output) + 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('delete', 'delete_containerapp', confirmation=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp env') as g: - g.custom_command('show', 'show_managed_environment') + 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_command('show', 'show_dapr_component') + 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 identity') as g: g.custom_command('assign', 'assign_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('remove', 'remove_managed_identity', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('show', 'show_managed_identity') + g.custom_show_command('show', 'show_managed_identity') 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_command('show', 'show_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: @@ -77,28 +77,28 @@ def load_command_table(self, _): 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_command('show', 'show_revision', table_transformer=transform_revision_output, exception_handler=ex_handler_factory()) + 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_command('show', 'show_ingress') + 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_command('show', 'show_ingress_traffic') + 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_command('show', 'show_registry') + 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_command('show', 'show_secret') + 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()) From f19323fc37162be05e313029d06daa2a50bec1cc Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 21 Mar 2022 15:02:54 -0400 Subject: [PATCH 152/177] Removed --ids from revision, secret, registry list. --- src/containerapp/azext_containerapp/_params.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index e2006b28187..4c7d90c5e11 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -175,3 +175,12 @@ def load_arguments(self, _): 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) From 0f402d86cd427e3cb33551a23f3cc25c52445f1f Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 12:30:29 -0700 Subject: [PATCH 153/177] Add linter exclusions --- linter_exclusions.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) 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: From 8f006f1820bd17412ee4a9d4cc2a445fb4ad2644 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 12:38:28 -0700 Subject: [PATCH 154/177] Fix polling on delete containerapp --- .../azext_containerapp/_clients.py | 27 ++++++++++++++++--- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 4 +-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 2dc138a6031..ada66cacf0d 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -63,7 +63,9 @@ def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-retu except Exception as e: # pylint: disable=broad-except animation.flush() - if not poll_if_status == "scheduledfordelete": # Catch "not found" errors if polling for delete + delete_statuses = ["scheduledfordelete", "cancelled"] + + if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete raise e @@ -127,7 +129,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= return r.json() @classmethod - def delete(cls, cmd, resource_group_name, name): + 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) @@ -141,8 +143,25 @@ def delete(cls, cmd, resource_group_name, name): r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) - if r.status_code == 202: - logger.warning('Containerapp successfully deleted') + 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): diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 4cff20cf47e..bdc2b14cb1b 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -48,7 +48,7 @@ def load_command_table(self, _): 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()) g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('delete', 'delete_containerapp', confirmation=True, exception_handler=ex_handler_factory()) + 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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 06a9c922c3b..aadaccde746 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -713,11 +713,11 @@ def list_containerapp(cmd, resource_group_name=None): handle_raw_exception(e) -def delete_containerapp(cmd, name, resource_group_name): +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) + return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) except CLIInternalError as e: handle_raw_exception(e) From f259b6ffec5e1350177ede2e271cf111013a354e Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 12:52:44 -0700 Subject: [PATCH 155/177] Fix error handling --- src/containerapp/azext_containerapp/custom.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index aadaccde746..ae119aa5c92 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -6,7 +6,13 @@ 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, CLIInternalError, InvalidArgumentValueError) +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 @@ -122,7 +128,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev if from_revision: try: r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) - except CLIInternalError as e: + 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"] @@ -694,7 +700,7 @@ def show_containerapp(cmd, name, resource_group_name): try: return ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -709,7 +715,7 @@ def list_containerapp(cmd, resource_group_name=None): containerapps = ContainerAppClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return containerapps - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -718,7 +724,7 @@ def delete_containerapp(cmd, name, resource_group_name, no_wait=False): try: return ContainerAppClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -809,7 +815,7 @@ def show_managed_environment(cmd, name, resource_group_name): try: return ManagedEnvironmentClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -824,7 +830,7 @@ def list_managed_environments(cmd, resource_group_name=None): managed_envs = ManagedEnvironmentClient.list_by_resource_group(cmd=cmd, resource_group_name=resource_group_name) return managed_envs - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -833,7 +839,7 @@ def delete_managed_environment(cmd, name, resource_group_name, no_wait=False): try: return ManagedEnvironmentClient.delete(cmd=cmd, name=name, resource_group_name=resource_group_name, no_wait=no_wait) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -994,7 +1000,7 @@ def show_managed_identity(cmd, name, resource_group_name): try: r = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) try: @@ -1061,7 +1067,7 @@ def create_or_update_github_action(cmd, if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) raise CLIInternalError(error_msg) from e - except CLIInternalError as clierror: + 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 @@ -1179,7 +1185,7 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ if e.data and e.data['message']: error_msg += " Error: {}".format(e.data['message']) raise CLIInternalError(error_msg) from e - except CLIInternalError as clierror: + 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 @@ -1196,7 +1202,7 @@ def delete_github_action(cmd, name, resource_group_name, token=None, login_with_ def list_revisions(cmd, name, resource_group_name): try: return ContainerAppClient.list_revisions(cmd=cmd, resource_group_name=resource_group_name, name=name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1206,7 +1212,7 @@ def show_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1216,7 +1222,7 @@ def restart_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.restart_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1226,7 +1232,7 @@ def activate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.activate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1236,7 +1242,7 @@ def deactivate_revision(cmd, resource_group_name, revision_name, name=None): try: return ContainerAppClient.deactivate_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=revision_name) - except CLIInternalError as e: + except CLIError as e: handle_raw_exception(e) @@ -1289,7 +1295,7 @@ def copy_revision(cmd, if from_revision: try: r = ContainerAppClient.show_revision(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, name=from_revision) - except CLIInternalError as e: + except CLIError as e: # Error handle the case where revision not found? handle_raw_exception(e) From c96f1e580fb5f5118716ad19bcc1b958ff3cd374 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 13:09:04 -0700 Subject: [PATCH 156/177] Add Container App Service --- src/service_name.json | 5 +++++ 1 file changed, 5 insertions(+) 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", From 3b823cf47cbcc87f70707f1b2833850c121975ac Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Mon, 21 Mar 2022 13:17:20 -0700 Subject: [PATCH 157/177] Fix flake linter --- src/containerapp/azext_containerapp/_clients.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index ada66cacf0d..77cf596c8bf 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -65,7 +65,7 @@ def poll(cmd, request_url, poll_if_status): # pylint: disable=inconsistent-retu delete_statuses = ["scheduledfordelete", "cancelled"] - if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete + if poll_if_status not in delete_statuses: # Catch "not found" errors if polling for delete raise e @@ -144,7 +144,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): r = send_raw_request(cmd.cli_ctx, "DELETE", request_url) if no_wait: - return # API doesn't return JSON (it returns no content) + 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( @@ -162,7 +162,6 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): pass logger.warning('Containerapp successfully deleted') - @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager From 0e5552dba6c73d1593af5c605db639e14cd5b741 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 07:57:12 -0700 Subject: [PATCH 158/177] Fix help text --- src/containerapp/azext_containerapp/_help.py | 29 ++++++++++--------- .../azext_containerapp/_params.py | 11 ++++--- src/containerapp/azext_containerapp/custom.py | 1 - 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 2c6a5009069..8fefb932f53 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -9,7 +9,7 @@ helps['containerapp'] = """ type: group - short-summary: Commands to manage Azure Container Apps. + short-summary: Manage Azure Container Apps. """ helps['containerapp create'] = """ @@ -21,17 +21,18 @@ 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 myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --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 myregistry.azurecr.io/my-app:v1.0 --environment MyContainerappEnv \\ + --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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples @@ -224,43 +225,43 @@ helps['containerapp env dapr-component'] = """ type: group - short-summary: Commmands to manage dapr components on the Container App environment. + 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 a Containerapp environment. + short-summary: List Dapr components for an environment. examples: - - name: List dapr components for a Containerapp environment. + - 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. + short-summary: Show the details of a Dapr component. examples: - - name: Show the details of a dapr component. + - 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. + short-summary: Create or update a Dapr component. examples: - - name: Create a dapr component. + - 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 componenet from a Containerapp environment. + short-summary: Remove a Dapr component from an environment. examples: - - name: Remove a dapr componenet from a Containerapp environment. + - name: Remove a Dapr component from a Container Apps environment. text: | - az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponenetName --name MyEnvironment + az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment """ # Identity Commands @@ -511,7 +512,7 @@ # Dapr Commands helps['containerapp dapr'] = """ type: group - short-summary: Commands to manage Dapr. + short-summary: Commands to manage Dapr. To manage Dapr components, see `az containerapp env dapr-component`. """ helps['containerapp dapr enable'] = """ diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 4c7d90c5e11..52ec7310492 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -31,7 +31,7 @@ def load_arguments(self, _): 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 (Creates new revision)') as c: + 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") @@ -42,14 +42,14 @@ def load_arguments(self, _): 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 (Creates new revision)') as c: + 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 (Creates new revision)') as c: + 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.") @@ -59,7 +59,6 @@ def load_arguments(self, _): 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.") - c.argument('dapr_components', help="The name of a yaml file containing a list of dapr components.") # Configuration with self.argument_context('containerapp', arg_group='Configuration') as c: @@ -162,10 +161,10 @@ def load_arguments(self, _): 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_id', help="The Dapr app ID.") c.argument('dapr_app_port', help="The port of your app.") c.argument('dapr_app_protocol', help="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") - c.argument('dapr_component_name', help="The dapr component name.") + 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: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ae119aa5c92..11840025404 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -283,7 +283,6 @@ def create_containerapp(cmd, dapr_app_port=None, dapr_app_id=None, dapr_app_protocol=None, - # dapr_components=None, revision_suffix=None, startup_command=None, args=None, From 51c540bb52c10e2e3ed5ef6d69c4c5184a2356f6 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 08:26:19 -0700 Subject: [PATCH 159/177] Mark extension as preview --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 52ec7310492..c70de7f12ac 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -163,7 +163,7 @@ def load_arguments(self, _): 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="Tells Dapr which protocol your application is using. Allowed values: grpc, http.") + 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.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index bdc2b14cb1b..97da07cefc0 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -43,7 +43,7 @@ def transform_revision_list_output(revs): def load_command_table(self, _): - with self.command_group('containerapp') as g: + 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()) From ff2ba4044269a42da2a23bfbd7d6e6fa136acff8 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 08:56:33 -0700 Subject: [PATCH 160/177] Add python 3.9 and 3.10 as supported --- src/containerapp/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index be4cd26f637..e23b0011367 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -29,6 +29,8 @@ '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', ] From c45cbd00edb0336cf17f53ae85ca6abd64dc4745 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 09:44:53 -0700 Subject: [PATCH 161/177] Remove registries and secrets from az containerapp update, in favor of registry and secret subgroup --- src/containerapp/azext_containerapp/custom.py | 62 +------------------ 1 file changed, 2 insertions(+), 60 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 11840025404..4a7ffd95912 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -455,16 +455,12 @@ def update_containerapp(cmd, min_replicas=None, max_replicas=None, revisions_mode=None, - secrets=None, set_env_vars=None, remove_env_vars=None, replace_env_vars=None, remove_all_env_vars=False, cpu=None, memory=None, - registry_server=None, - registry_user=None, - registry_pass=None, revision_suffix=None, startup_command=None, args=None, @@ -474,8 +470,7 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or secrets or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or registry_server or\ - registry_user or registry_pass or\ + revisions_mode 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) @@ -498,11 +493,9 @@ def update_containerapp(cmd, e["value"] = "" update_map = {} - update_map['secrets'] = secrets is not None - update_map['registries'] = registry_server or registry_user or registry_pass 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 - update_map['configuration'] = update_map['secrets'] or update_map['registries'] or revisions_mode is not None + update_map['configuration'] = revisions_mode is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -631,57 +624,6 @@ def update_containerapp(cmd, _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - if secrets is not None: - _add_or_update_secrets(containerapp_def, parse_secret_flags(secrets)) - - if update_map["registries"]: - 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 registry_server: - raise ValidationError("Usage error: --registry-server is required when adding or updating a registry") - - # Infer credentials if not supplied and its azurecr - if (registry_user is None or registry_pass is None) and not _registry_exists(containerapp_def, registry_server): - registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) - - # Check if updating existing registry - updating_existing_registry = False - for r in registries_def: - if r['server'].lower() == registry_server.lower(): - updating_existing_registry = True - - if registry_user: - r["username"] = registry_user - if registry_pass: - r["passwordSecretRef"] = store_as_secret_and_return_secret_ref( - containerapp_def["properties"]["configuration"]["secrets"], - r["username"], - r["server"], - registry_pass, - update_existing_secret=True) - - # If not updating existing registry, add as new registry - if not updating_existing_registry: - if not(registry_server is not None and registry_user is not None and registry_pass is not None): - raise ValidationError("Usage error: --registry-server, --registry-password and --registry-username are required when adding a registry") - - registry = RegistryCredentialsModel - registry["server"] = registry_server - registry["username"] = registry_user - registry["passwordSecretRef"] = store_as_secret_and_return_secret_ref( - containerapp_def["properties"]["configuration"]["secrets"], - registry_user, - registry_server, - registry_pass, - 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) From 126878c1fbc735c93b06de1be40926fa5cf4aed2 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 10:07:08 -0700 Subject: [PATCH 162/177] Fix YAML not working --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4a7ffd95912..633e50544c9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -21,7 +21,7 @@ from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient -# from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import +from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, From e64cbef79a56c6947c5dff9f726d18b94b4b2e87 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 10:39:36 -0700 Subject: [PATCH 163/177] Move import to inside deserialize function --- src/containerapp/azext_containerapp/custom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 633e50544c9..a5d40c3c2d9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -21,7 +21,6 @@ from ._client_factory import handle_raw_exception from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient, DaprComponentClient -from ._sdk_models import * # pylint: disable=wildcard-import, unused-wildcard-import from ._github_oauth import get_github_access_token from ._models import ( ManagedEnvironment as ManagedEnvironmentModel, @@ -84,6 +83,7 @@ def load_yaml_file(file_name): def create_deserializer(): + from ._sdk_models import ContainerApp # pylint: disable=unused-import from msrest import Deserializer import sys import inspect From 40d112c63fb888731dc52ff52beeb83e757f3191 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 11:04:22 -0700 Subject: [PATCH 164/177] Dapr moved from Template to Configuration --- src/containerapp/azext_containerapp/_models.py | 2 +- .../azext_containerapp/_sdk_models.py | 8 ++++---- src/containerapp/azext_containerapp/custom.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index d00798765c5..d4b26d94b32 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -150,7 +150,6 @@ "revisionSuffix": None, "containers": None, # [Container] "scale": Scale, - "dapr": Dapr, "volumes": None # [Volume] } @@ -158,6 +157,7 @@ "secrets": None, # [Secret] "activeRevisionsMode": None, # 'multiple' or 'single' "ingress": None, # Ingress + "dapr": Dapr, "registries": None # [RegistryCredentials] } diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py index b34325cdb9c..dd93bfce7c2 100644 --- a/src/containerapp/azext_containerapp/_sdk_models.py +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -851,6 +851,8 @@ class Configuration(Model): ~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] @@ -860,6 +862,7 @@ class Configuration(Model): '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]'}, } @@ -868,6 +871,7 @@ def __init__(self, **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) @@ -3175,8 +3179,6 @@ class Template(Model): :type containers: list[~commondefinitions.models.Container] :param scale: Scaling properties for the Container App. :type scale: ~commondefinitions.models.Scale - :param dapr: Dapr configuration for the Container App. - :type dapr: ~commondefinitions.models.Dapr :param volumes: List of volume definitions for the Container App. :type volumes: list[~commondefinitions.models.Volume] """ @@ -3185,7 +3187,6 @@ class Template(Model): 'revision_suffix': {'key': 'revisionSuffix', 'type': 'str'}, 'containers': {'key': 'containers', 'type': '[Container]'}, 'scale': {'key': 'scale', 'type': 'Scale'}, - 'dapr': {'key': 'dapr', 'type': 'Dapr'}, 'volumes': {'key': 'volumes', 'type': '[Volume]'}, } @@ -3194,7 +3195,6 @@ def __init__(self, **kwargs): self.revision_suffix = kwargs.get('revision_suffix', None) self.containers = kwargs.get('containers', None) self.scale = kwargs.get('scale', None) - self.dapr = kwargs.get('dapr', None) self.volumes = kwargs.get('volumes', None) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a5d40c3c2d9..7bb2b8340ca 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -358,11 +358,20 @@ def create_containerapp(cmd, 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 # Identity actions identity_def = ManagedServiceIdentityModel @@ -410,18 +419,9 @@ def create_containerapp(cmd, if resources_def is not None: container_def["resources"] = resources_def - dapr_def = None - if dapr_enabled: - dapr_def = DaprModel - dapr_def["daprEnabled"] = True - dapr_def["appId"] = dapr_app_id - dapr_def["appPort"] = dapr_app_port - dapr_def["appProtocol"] = dapr_app_protocol - template_def = TemplateModel template_def["containers"] = [container_def] template_def["scale"] = scale_def - template_def["dapr"] = dapr_def if revision_suffix is not None: template_def["revisionSuffix"] = revision_suffix From bade2b113ed774d5827c81e8e62126928508e663 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 11:29:36 -0700 Subject: [PATCH 165/177] Use aka.ms link for containerapps yaml --- src/containerapp/azext_containerapp/_help.py | 2 +- src/containerapp/azext_containerapp/custom.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 8fefb932f53..dde874e3e55 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -35,7 +35,7 @@ --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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples + - 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 \\ diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 7bb2b8340ca..606639bd873 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -100,7 +100,7 @@ def create_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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + 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 @@ -139,7 +139,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev containerapp_def = deserializer('ContainerApp', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') from 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 @@ -178,7 +178,7 @@ def update_containerapp_yaml(cmd, name, resource_group_name, file_name, from_rev 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://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + 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 @@ -199,7 +199,7 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= containerapp_def = deserializer('ContainerApp', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') from 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 @@ -219,7 +219,7 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= # Validate managed environment if not containerapp_def["properties"].get('managedEnvironmentId'): - raise RequiredArgumentMissingError('managedEnvironmentId is required. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + 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 @@ -1898,7 +1898,7 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, yaml_containerapp = load_yaml_file(yaml) if type(yaml_containerapp) != dict: # pylint: disable=unidiomatic-typecheck - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') + 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 @@ -1907,7 +1907,7 @@ def create_or_update_dapr_component(cmd, resource_group_name, environment_name, daprcomponent_def = deserializer('DaprComponent', yaml_containerapp) except DeserializationError as ex: - raise ValidationError('Invalid YAML provided. Please see https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples for a valid containerapps YAML spec.') from 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)) From 0922c6845b7e31834afede5079b4c3ba74a356ce Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 22 Mar 2022 15:30:41 -0400 Subject: [PATCH 166/177] Updated dapr enable/disable to current spec. --- src/containerapp/azext_containerapp/custom.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 606639bd873..d92d07918c8 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1835,19 +1835,22 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - if 'dapr' not in containerapp_def['properties']: - containerapp_def['properties']['dapr'] = {} + 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']['dapr']['dapr_app_id'] = dapr_app_id + containerapp_def['properties']['configuration']['dapr']['dapr_app_id'] = dapr_app_id if dapr_app_port: - containerapp_def['properties']['dapr']['dapr_app_port'] = dapr_app_port + containerapp_def['properties']['configuration']['dapr']['dapr_app_port'] = dapr_app_port if dapr_app_protocol: - containerapp_def['properties']['dapr']['dapr_app_protocol'] = dapr_app_protocol + containerapp_def['properties']['configuration']['dapr']['dapr_app_protocol'] = dapr_app_protocol - containerapp_def['properties']['dapr']['enabled'] = True + containerapp_def['properties']['configuration']['dapr']['enabled'] = True try: r = ContainerAppClient.create_or_update( @@ -1871,7 +1874,13 @@ def disable_dapr(cmd, name, resource_group_name, no_wait=False): _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - containerapp_def['properties']['dapr']['enabled'] = False + 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( From 2badc74f1fbfb6f35775fa9c3823c87db06dd97c Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 22 Mar 2022 16:10:17 -0400 Subject: [PATCH 167/177] Fixed oversight. --- src/containerapp/azext_containerapp/custom.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d92d07918c8..d39fa38865a 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1855,7 +1855,7 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= 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"]['dapr'] + return r["properties"]['configuration']['dapr'] except Exception as e: handle_raw_exception(e) @@ -1885,7 +1885,7 @@ def disable_dapr(cmd, name, resource_group_name, no_wait=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"]['dapr'] + return r["properties"]['configuration']['dapr'] except Exception as e: handle_raw_exception(e) From 2bf3686b26f53f5a01164fc93c3ad092cd28bf1b Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Tue, 22 Mar 2022 21:10:16 -0700 Subject: [PATCH 168/177] Remove revisions-mode from containerapp update --- src/containerapp/azext_containerapp/custom.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d39fa38865a..8a87619b69f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -454,7 +454,6 @@ def update_containerapp(cmd, container_name=None, min_replicas=None, max_replicas=None, - revisions_mode=None, set_env_vars=None, remove_env_vars=None, replace_env_vars=None, @@ -470,7 +469,7 @@ def update_containerapp(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory 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) @@ -495,7 +494,6 @@ def update_containerapp(cmd, 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 - update_map['configuration'] = revisions_mode is not None if tags: _add_or_update_tags(containerapp_def, tags) @@ -618,10 +616,6 @@ def update_containerapp(cmd, if max_replicas is not None: containerapp_def["properties"]["template"]["scale"]["maxReplicas"] = max_replicas - # Configuration - if revisions_mode is not None: - containerapp_def["properties"]["configuration"]["activeRevisionsMode"] = revisions_mode - _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) try: From eec4e1a9ac79c53c25e7b51e0a1e8fc8414dee14 Mon Sep 17 00:00:00 2001 From: Haroon Feisal <38823870+haroonf@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:28:02 -0400 Subject: [PATCH 169/177] Fixed dapr enable property names. (#47) Co-authored-by: Haroon Feisal --- src/containerapp/azext_containerapp/custom.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 8a87619b69f..a4cac4fdb77 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1836,13 +1836,13 @@ def enable_dapr(cmd, name, resource_group_name, dapr_app_id=None, dapr_app_port= containerapp_def['properties']['configuration']['dapr'] = {} if dapr_app_id: - containerapp_def['properties']['configuration']['dapr']['dapr_app_id'] = dapr_app_id + containerapp_def['properties']['configuration']['dapr']['appId'] = dapr_app_id if dapr_app_port: - containerapp_def['properties']['configuration']['dapr']['dapr_app_port'] = dapr_app_port + containerapp_def['properties']['configuration']['dapr']['appPort'] = dapr_app_port if dapr_app_protocol: - containerapp_def['properties']['configuration']['dapr']['dapr_app_protocol'] = dapr_app_protocol + containerapp_def['properties']['configuration']['dapr']['appProtocol'] = dapr_app_protocol containerapp_def['properties']['configuration']['dapr']['enabled'] = True From c43d1caad2aecab24796d46c680279a33b865ce7 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 23 Mar 2022 11:57:58 -0700 Subject: [PATCH 170/177] Fix exceptions with using --yaml in containerapp create/update --- src/containerapp/azext_containerapp/_utils.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index b1b3fa9bf9a..7376167f592 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -429,7 +429,14 @@ def _add_or_update_tags(containerapp_def, tags): def _object_to_dict(obj): import json - return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) + 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): @@ -499,10 +506,10 @@ def _remove_dapr_readonly_attributes(daprcomponent_def): 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 - import collections + from collections.abc import Mapping for key, val in new_dict.items(): - if isinstance(val, collections.Mapping): + if isinstance(val, Mapping): tmp = update_nested_dictionary(orig_dict.get(key, {}), val) orig_dict[key] = tmp elif isinstance(val, list): From a0e7ca1c6497f78dd22e260c89aac9df60d4eb21 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Mar 2022 07:41:49 -0700 Subject: [PATCH 171/177] Rename history msg --- src/containerapp/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 1c139576ba0..a6011cf44c5 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -5,4 +5,4 @@ Release History 0.1.0 ++++++ -* Initial release. +* Initial release for Container App support with Microsoft.App RP. From 5f68333c9d2d28331ef1fdaed4637681c4f7b1d4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Mar 2022 08:23:45 -0700 Subject: [PATCH 172/177] Include fqdn in containerapp table output --- src/containerapp/azext_containerapp/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 97da07cefc0..dd6f2d067dc 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -46,8 +46,8 @@ 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()) - g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) + 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: From 0b6fb6f9136cee4973ab570a2e9bf614ad4deb02 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 23 Mar 2022 14:13:13 -0400 Subject: [PATCH 173/177] Added ingress messages. --- src/containerapp/azext_containerapp/custom.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index a4cac4fdb77..1f49d3c9302 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -254,6 +254,11 @@ def create_containerapp_yaml(cmd, name, resource_group_name, file_name, no_wait= 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) @@ -441,6 +446,11 @@ def create_containerapp(cmd, 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) @@ -732,6 +742,8 @@ def create_managed_environment(cmd, 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) @@ -1459,6 +1471,7 @@ def enable_ingress(cmd, name, resource_group_name, type, target_port, transport, 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) From 2f07b6ed95debcc01cad234572133bbcbaf428d0 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Mar 2022 10:16:11 -0700 Subject: [PATCH 174/177] Revert history msg --- src/containerapp/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index a6011cf44c5..1c139576ba0 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -5,4 +5,4 @@ Release History 0.1.0 ++++++ -* Initial release for Container App support with Microsoft.App RP. +* Initial release. From 1798852f13296f7ef09b7f28a3d5989ad51e2ef1 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 24 Mar 2022 15:09:05 -0400 Subject: [PATCH 175/177] Reduced redundant code between revision copy and containerapp update. --- src/containerapp/azext_containerapp/custom.py | 250 ++++++------------ 1 file changed, 78 insertions(+), 172 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5f354d81960..65f8b397dbe 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -470,12 +470,57 @@ def update_containerapp(cmd, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") + return update_containerapp_logic(cmd, + name, + resource_group_name, + yaml, + image, + container_name, + min_replicas, + max_replicas, + revisions_mode, + set_env_vars, + remove_env_vars, + replace_env_vars, + remove_all_env_vars, + cpu, + memory, + revision_suffix, + startup_command, + args, + tags, + no_wait) + + +def update_containerapp_logic(cmd, + name, + resource_group_name, + yaml=None, + image=None, + container_name=None, + min_replicas=None, + max_replicas=None, + revisions_mode=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, + from_revision=None): + _validate_subscription_registered(cmd, "Microsoft.App") + if yaml: if image or min_replicas or max_replicas or\ revisions_mode 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) + return update_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait, from_revision=from_revision) containerapp_def = None try: @@ -486,6 +531,16 @@ def update_containerapp(cmd, 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"]: @@ -1197,6 +1252,7 @@ def copy_revision(cmd, yaml=None, image=None, container_name=None, + revisions_mode=None, min_replicas=None, max_replicas=None, set_env_vars=None, @@ -1218,177 +1274,27 @@ def copy_revision(cmd, 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) + return update_containerapp_logic(cmd, + name, + resource_group_name, + yaml, + image, + container_name, + min_replicas, + max_replicas, + revisions_mode, + set_env_vars, + remove_env_vars, + replace_env_vars, + remove_all_env_vars, + cpu, + memory, + revision_suffix, + startup_command, + args, + tags, + no_wait, + from_revision) def set_revision_mode(cmd, resource_group_name, name, mode, no_wait=False): From a06d619893a6c9c18547d04a1a5098f81a9bff96 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 24 Mar 2022 15:30:40 -0400 Subject: [PATCH 176/177] Fixed merge issues. --- src/containerapp/azext_containerapp/_help.py | 4 ---- .../azext_containerapp/_models.py | 7 ------ .../azext_containerapp/_sdk_models.py | 22 ------------------- src/containerapp/azext_containerapp/_utils.py | 11 ---------- 4 files changed, 44 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 1ba5e1c5a9c..dde874e3e55 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -35,11 +35,7 @@ --image my-app:v1.0 --environment MyContainerappEnv \\ --secrets mysecret=secretvalue1 anothersecret="secret value 2" \\ --env-vars GREETING="Hello, world" SECRETENV=secretref:anothersecret -<<<<<<< HEAD - - name: Create a container app using a YAML configuration. Example YAML configuration - https://docs.microsoft.com/azure/container-apps/azure-resource-manager-api-spec#examples -======= - name: Create a container app using a YAML configuration. Example YAML configuration - https://aka.ms/azure-container-apps-yaml ->>>>>>> containerapp text: | az containerapp create -n MyContainerapp -g MyResourceGroup \\ --environment MyContainerappEnv \\ diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index a9d7bffaa90..d4b26d94b32 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -150,10 +150,6 @@ "revisionSuffix": None, "containers": None, # [Container] "scale": Scale, -<<<<<<< HEAD - "dapr": Dapr, -======= ->>>>>>> containerapp "volumes": None # [Volume] } @@ -161,10 +157,7 @@ "secrets": None, # [Secret] "activeRevisionsMode": None, # 'multiple' or 'single' "ingress": None, # Ingress -<<<<<<< HEAD -======= "dapr": Dapr, ->>>>>>> containerapp "registries": None # [RegistryCredentials] } diff --git a/src/containerapp/azext_containerapp/_sdk_models.py b/src/containerapp/azext_containerapp/_sdk_models.py index 407b6503f5f..dd93bfce7c2 100644 --- a/src/containerapp/azext_containerapp/_sdk_models.py +++ b/src/containerapp/azext_containerapp/_sdk_models.py @@ -851,11 +851,8 @@ class Configuration(Model): ~commondefinitions.models.ActiveRevisionsMode :param ingress: Ingress configurations. :type ingress: ~commondefinitions.models.Ingress -<<<<<<< HEAD -======= :param dapr: Dapr configuration for the Container App. :type dapr: ~commondefinitions.models.Dapr ->>>>>>> containerapp :param registries: Collection of private container registry credentials for containers used by the Container app :type registries: list[~commondefinitions.models.RegistryCredentials] @@ -865,10 +862,7 @@ class Configuration(Model): 'secrets': {'key': 'secrets', 'type': '[Secret]'}, 'active_revisions_mode': {'key': 'activeRevisionsMode', 'type': 'str'}, 'ingress': {'key': 'ingress', 'type': 'Ingress'}, -<<<<<<< HEAD -======= 'dapr': {'key': 'dapr', 'type': 'Dapr'}, ->>>>>>> containerapp 'registries': {'key': 'registries', 'type': '[RegistryCredentials]'}, } @@ -877,10 +871,7 @@ def __init__(self, **kwargs): self.secrets = kwargs.get('secrets', None) self.active_revisions_mode = kwargs.get('active_revisions_mode', None) self.ingress = kwargs.get('ingress', None) -<<<<<<< HEAD -======= self.dapr = kwargs.get('dapr', None) ->>>>>>> containerapp self.registries = kwargs.get('registries', None) @@ -3188,11 +3179,6 @@ class Template(Model): :type containers: list[~commondefinitions.models.Container] :param scale: Scaling properties for the Container App. :type scale: ~commondefinitions.models.Scale -<<<<<<< HEAD - :param dapr: Dapr configuration for the Container App. - :type dapr: ~commondefinitions.models.Dapr -======= ->>>>>>> containerapp :param volumes: List of volume definitions for the Container App. :type volumes: list[~commondefinitions.models.Volume] """ @@ -3201,10 +3187,6 @@ class Template(Model): 'revision_suffix': {'key': 'revisionSuffix', 'type': 'str'}, 'containers': {'key': 'containers', 'type': '[Container]'}, 'scale': {'key': 'scale', 'type': 'Scale'}, -<<<<<<< HEAD - 'dapr': {'key': 'dapr', 'type': 'Dapr'}, -======= ->>>>>>> containerapp 'volumes': {'key': 'volumes', 'type': '[Volume]'}, } @@ -3213,10 +3195,6 @@ def __init__(self, **kwargs): self.revision_suffix = kwargs.get('revision_suffix', None) self.containers = kwargs.get('containers', None) self.scale = kwargs.get('scale', None) -<<<<<<< HEAD - self.dapr = kwargs.get('dapr', None) -======= ->>>>>>> containerapp self.volumes = kwargs.get('volumes', None) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 67527104298..7376167f592 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -429,9 +429,6 @@ def _add_or_update_tags(containerapp_def, tags): def _object_to_dict(obj): import json -<<<<<<< HEAD - return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) -======= import datetime def default_handler(x): @@ -440,7 +437,6 @@ def default_handler(x): return x.__dict__ return json.loads(json.dumps(obj, default=default_handler)) ->>>>>>> containerapp def _to_camel_case(snake_str): @@ -510,17 +506,10 @@ def _remove_dapr_readonly_attributes(daprcomponent_def): 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 -<<<<<<< HEAD - import collections - - for key, val in new_dict.items(): - if isinstance(val, collections.Mapping): -======= from collections.abc import Mapping for key, val in new_dict.items(): if isinstance(val, Mapping): ->>>>>>> containerapp tmp = update_nested_dictionary(orig_dict.get(key, {}), val) orig_dict[key] = tmp elif isinstance(val, list): From 636d3f7ff30d57144fd8e5766395587d3c517ae7 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 24 Mar 2022 15:35:06 -0400 Subject: [PATCH 177/177] Fixed merge conflicts, moved helper function --- .../azext_containerapp/commands.py | 5 -- src/containerapp/azext_containerapp/custom.py | 87 +++++++++---------- 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 9d8cd67e37e..dd6f2d067dc 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -46,13 +46,8 @@ 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) -<<<<<<< HEAD - g.custom_command('create', 'create_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) - g.custom_command('update', 'update_containerapp', supports_no_wait=True, exception_handler=ex_handler_factory()) -======= 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) ->>>>>>> containerapp 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: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 226acfbe4c1..52620d08e1d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -456,48 +456,6 @@ def create_containerapp(cmd, 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") - - return update_containerapp_logic(cmd, - name, - resource_group_name, - yaml, - image, - container_name, - min_replicas, - max_replicas, - set_env_vars, - remove_env_vars, - replace_env_vars, - remove_all_env_vars, - cpu, - memory, - revision_suffix, - startup_command, - args, - tags, - no_wait) - - def update_containerapp_logic(cmd, name, resource_group_name, @@ -506,7 +464,6 @@ def update_containerapp_logic(cmd, container_name=None, min_replicas=None, max_replicas=None, - revisions_mode=None, set_env_vars=None, remove_env_vars=None, replace_env_vars=None, @@ -523,7 +480,7 @@ def update_containerapp_logic(cmd, if yaml: if image or min_replicas or max_replicas or\ - revisions_mode or set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory 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, from_revision=from_revision) @@ -694,6 +651,48 @@ def update_containerapp_logic(cmd, 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") + + return update_containerapp_logic(cmd, + name, + resource_group_name, + yaml, + image, + container_name, + min_replicas, + max_replicas, + set_env_vars, + remove_env_vars, + replace_env_vars, + remove_all_env_vars, + cpu, + memory, + revision_suffix, + startup_command, + args, + tags, + no_wait) + + def show_containerapp(cmd, name, resource_group_name): _validate_subscription_registered(cmd, "Microsoft.App")