From 0bfdddd7048a321df0eae3944d7103626fad51a2 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Wed, 2 Feb 2022 20:27:02 -0800 Subject: [PATCH 001/158] 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 002/158] 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 003/158] 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 004/158] 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 005/158] 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 006/158] 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 007/158] 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 008/158] 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 009/158] 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 010/158] 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 011/158] 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 012/158] 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 013/158] 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 014/158] 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 015/158] 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 016/158] 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 017/158] 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 018/158] 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 019/158] 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 020/158] 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 021/158] 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 022/158] 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 023/158] 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 024/158] 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 025/158] 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 026/158] 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 027/158] 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 028/158] 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 029/158] 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 030/158] 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 031/158] 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 032/158] 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 033/158] 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 034/158] 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 035/158] 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 036/158] 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 037/158] 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 038/158] 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 039/158] 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 040/158] 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 041/158] 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 042/158] 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 043/158] 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 044/158] 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 045/158] 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 046/158] 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 047/158] 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 048/158] 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 049/158] 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 050/158] 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 051/158] 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 052/158] 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 053/158] 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 054/158] 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 055/158] 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 056/158] 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 057/158] 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 058/158] 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 059/158] 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 060/158] 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 061/158] 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 062/158] 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 063/158] 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 064/158] 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 065/158] 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 066/158] 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 067/158] 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 068/158] 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 069/158] 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 070/158] 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 071/158] 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 072/158] 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 073/158] 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 074/158] 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 075/158] 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 076/158] 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 077/158] 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 078/158] 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 079/158] 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 080/158] 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 081/158] 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 082/158] 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 083/158] 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 084/158] 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 085/158] 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 086/158] 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 087/158] 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 088/158] 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 089/158] 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 090/158] 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 ad6ff2706abb83726a8ae30bf4bd92187b84b8a4 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Mar 2022 14:19:11 -0700 Subject: [PATCH 091/158] Add basic test case --- src/containerapp/HISTORY.rst | 4 +- .../recordings/test_containerapp_e2e.yaml | 2490 +++++++++++++++++ .../latest/test_containerapp_scenario.py | 28 +- 3 files changed, 2519 insertions(+), 3 deletions(-) create mode 100644 src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 1c139576ba0..b66465a832a 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -5,4 +5,6 @@ Release History 0.1.0 ++++++ -* Initial release. +* Initial release for Container App support with Microsoft.App RP. +* Subgroup commands for dapr, github-action, ingress, registry, revision & secrets +* Various bugfixes for create & update commands diff --git a/src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml b/src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml new file mode 100644 index 00000000000..f6e414d6149 --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml @@ -0,0 +1,2490 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001","name":"clitest.rg000001","type":"Microsoft.Resources/resourceGroups","location":"centraluseuap","tags":{"product":"azurecli","cause":"automation","date":"2022-03-24T21:10:42Z"},"properties":{"provisioningState":"Succeeded"}}' + headers: + cache-control: + - no-cache + content-length: + - '317' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:10:42 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North + Central US (Stage)","Central US EUAP","Canada Central","West Europe","North + Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '2714' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:10:42 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North + Central US (Stage)","Central US EUAP","Canada Central","West Europe","North + Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '2714' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:10:43 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights","namespace":"Microsoft.OperationalInsights","authorizations":[{"applicationId":"d2a0a418-0aac-4541-82b2-b3142c89da77","roleDefinitionId":"86695298-2eb9-48a7-9ec3-2fdb38b6878b"},{"applicationId":"ca7f3f0b-7d91-482c-8e09-c5d840d0eac5","roleDefinitionId":"5d5a2e56-9835-44aa-93db-d2f19e155438"}],"resourceTypes":[{"resourceType":"workspaces","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2021-06-01","2021-03-01-privatepreview","2020-10-01","2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"querypacks","locations":["West + Central US","East US","South Central US","North Europe","West Europe","Southeast + Asia","West US 2","UK South","Canada Central","Central India","Japan East","Australia + East","Korea Central","France Central","Central US","East US 2","East Asia","West + US","South Africa North","North Central US","Brazil South","Switzerland North","Norway + East","Australia Southeast","Australia Central 2","Germany West Central","Switzerland + West","UAE Central","UK West","Brazil Southeast","Japan West","UAE North","Australia + Central","France South","South India","Jio India Central","Jio India West","Canada + East","West US 3","Sweden Central","Korea South"],"apiVersions":["2019-09-01-preview","2019-09-01"],"capabilities":"SupportsTags, + SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"locations/operationStatuses","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/scopedPrivateLinkProxies","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-03-01-preview","capabilities":"None"},{"resourceType":"workspaces/query","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/metadata","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/dataSources","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/linkedStorageAccounts","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/tables","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia East","Australia + Central","France Central","Korea Central","North Europe","Central US","East + Asia","East US 2","South Central US","North Central US","West US","UK West","South + Africa North","Brazil South","Switzerland North","Switzerland West","Germany + West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2020-08-01","2020-03-01-preview","2017-04-26-preview"],"capabilities":"None"},{"resourceType":"workspaces/storageInsightConfigs","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia East","Australia + Central","France Central","Korea Central","North Europe","Central US","East + Asia","East US 2","South Central US","North Central US","West US","UK West","South + Africa North","Brazil South","Switzerland North","Switzerland West","Germany + West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"storageInsightConfigs","locations":[],"apiVersions":["2020-08-01","2020-03-01-preview","2014-10-10"],"capabilities":"SupportsExtension"},{"resourceType":"workspaces/linkedServices","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"linkTargets","locations":["East + US"],"apiVersions":["2020-03-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"deletedWorkspaces","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"operations","locations":[],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview","2015-11-01-preview","2014-11-10"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"clusters","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Switzerland North","Switzerland West","Germany West Central","Australia + Central 2","UAE Central","Brazil South","UAE North","Japan West","Brazil Southeast","Norway + East","Norway West","France South","South India","Jio India Central","Jio + India West","Canada East","West US 3","Sweden Central","Korea South"],"apiVersions":["2021-06-01","2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2021-06-01","capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SystemAssignedResourceIdentity, SupportsTags, + SupportsLocation"},{"resourceType":"workspaces/dataExports","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '12146' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:10:43 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights","namespace":"Microsoft.OperationalInsights","authorizations":[{"applicationId":"d2a0a418-0aac-4541-82b2-b3142c89da77","roleDefinitionId":"86695298-2eb9-48a7-9ec3-2fdb38b6878b"},{"applicationId":"ca7f3f0b-7d91-482c-8e09-c5d840d0eac5","roleDefinitionId":"5d5a2e56-9835-44aa-93db-d2f19e155438"}],"resourceTypes":[{"resourceType":"workspaces","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2021-06-01","2021-03-01-privatepreview","2020-10-01","2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"querypacks","locations":["West + Central US","East US","South Central US","North Europe","West Europe","Southeast + Asia","West US 2","UK South","Canada Central","Central India","Japan East","Australia + East","Korea Central","France Central","Central US","East US 2","East Asia","West + US","South Africa North","North Central US","Brazil South","Switzerland North","Norway + East","Australia Southeast","Australia Central 2","Germany West Central","Switzerland + West","UAE Central","UK West","Brazil Southeast","Japan West","UAE North","Australia + Central","France South","South India","Jio India Central","Jio India West","Canada + East","West US 3","Sweden Central","Korea South"],"apiVersions":["2019-09-01-preview","2019-09-01"],"capabilities":"SupportsTags, + SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"locations/operationStatuses","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/scopedPrivateLinkProxies","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-03-01-preview","capabilities":"None"},{"resourceType":"workspaces/query","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/metadata","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/dataSources","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/linkedStorageAccounts","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/tables","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia East","Australia + Central","France Central","Korea Central","North Europe","Central US","East + Asia","East US 2","South Central US","North Central US","West US","UK West","South + Africa North","Brazil South","Switzerland North","Switzerland West","Germany + West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2020-08-01","2020-03-01-preview","2017-04-26-preview"],"capabilities":"None"},{"resourceType":"workspaces/storageInsightConfigs","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia East","Australia + Central","France Central","Korea Central","North Europe","Central US","East + Asia","East US 2","South Central US","North Central US","West US","UK West","South + Africa North","Brazil South","Switzerland North","Switzerland West","Germany + West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"storageInsightConfigs","locations":[],"apiVersions":["2020-08-01","2020-03-01-preview","2014-10-10"],"capabilities":"SupportsExtension"},{"resourceType":"workspaces/linkedServices","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"linkTargets","locations":["East + US"],"apiVersions":["2020-03-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"deletedWorkspaces","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"operations","locations":[],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview","2015-11-01-preview","2014-11-10"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"clusters","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Switzerland North","Switzerland West","Germany West Central","Australia + Central 2","UAE Central","Brazil South","UAE North","Japan West","Brazil Southeast","Norway + East","Norway West","France South","South India","Jio India Central","Jio + India West","Canada East","West US 3","Sweden Central","Korea South"],"apiVersions":["2021-06-01","2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2021-06-01","capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SystemAssignedResourceIdentity, SupportsTags, + SupportsLocation"},{"resourceType":"workspaces/dataExports","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '12146' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:10:43 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights","namespace":"Microsoft.OperationalInsights","authorizations":[{"applicationId":"d2a0a418-0aac-4541-82b2-b3142c89da77","roleDefinitionId":"86695298-2eb9-48a7-9ec3-2fdb38b6878b"},{"applicationId":"ca7f3f0b-7d91-482c-8e09-c5d840d0eac5","roleDefinitionId":"5d5a2e56-9835-44aa-93db-d2f19e155438"}],"resourceTypes":[{"resourceType":"workspaces","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2021-06-01","2021-03-01-privatepreview","2020-10-01","2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"querypacks","locations":["West + Central US","East US","South Central US","North Europe","West Europe","Southeast + Asia","West US 2","UK South","Canada Central","Central India","Japan East","Australia + East","Korea Central","France Central","Central US","East US 2","East Asia","West + US","South Africa North","North Central US","Brazil South","Switzerland North","Norway + East","Australia Southeast","Australia Central 2","Germany West Central","Switzerland + West","UAE Central","UK West","Brazil Southeast","Japan West","UAE North","Australia + Central","France South","South India","Jio India Central","Jio India West","Canada + East","West US 3","Sweden Central","Korea South"],"apiVersions":["2019-09-01-preview","2019-09-01"],"capabilities":"SupportsTags, + SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"locations/operationStatuses","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/scopedPrivateLinkProxies","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-03-01-preview","capabilities":"None"},{"resourceType":"workspaces/query","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/metadata","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/dataSources","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/linkedStorageAccounts","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/tables","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia East","Australia + Central","France Central","Korea Central","North Europe","Central US","East + Asia","East US 2","South Central US","North Central US","West US","UK West","South + Africa North","Brazil South","Switzerland North","Switzerland West","Germany + West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2020-08-01","2020-03-01-preview","2017-04-26-preview"],"capabilities":"None"},{"resourceType":"workspaces/storageInsightConfigs","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia East","Australia + Central","France Central","Korea Central","North Europe","Central US","East + Asia","East US 2","South Central US","North Central US","West US","UK West","South + Africa North","Brazil South","Switzerland North","Switzerland West","Germany + West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"storageInsightConfigs","locations":[],"apiVersions":["2020-08-01","2020-03-01-preview","2014-10-10"],"capabilities":"SupportsExtension"},{"resourceType":"workspaces/linkedServices","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"linkTargets","locations":["East + US"],"apiVersions":["2020-03-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"deletedWorkspaces","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"operations","locations":[],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview","2015-11-01-preview","2014-11-10"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"clusters","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Switzerland North","Switzerland West","Germany West Central","Australia + Central 2","UAE Central","Brazil South","UAE North","Japan West","Brazil Southeast","Norway + East","Norway West","France South","South India","Jio India Central","Jio + India West","Canada East","West US 3","Sweden Central","Korea South"],"apiVersions":["2021-06-01","2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2021-06-01","capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SystemAssignedResourceIdentity, SupportsTags, + SupportsLocation"},{"resourceType":"workspaces/dataExports","locations":["East + US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan + East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia + East","France Central","Korea Central","North Europe","Central US","East Asia","East + US 2","South Central US","North Central US","West US","UK West","South Africa + North","Brazil South","Switzerland North","Switzerland West","Germany West + Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil + Southeast","Norway East","Norway West","France South","South India","Jio India + Central","Jio India West","Canada East","West US 3","Sweden Central","Korea + South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '12146' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:10:44 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: '{"location": "eastus", "properties": {"publicNetworkAccessForIngestion": + "Enabled", "publicNetworkAccessForQuery": "Enabled"}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + Content-Length: + - '126' + Content-Type: + - application/json + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-mgmt-loganalytics/12.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/Microsoft.OperationalInsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf?api-version=2021-06-01 + response: + body: + string: "{\r\n \"properties\": {\r\n \"source\": \"Azure\",\r\n \"customerId\": + \"ebb686fa-c4ff-41b4-837a-f4998cd19621\",\r\n \"provisioningState\": \"Creating\",\r\n + \ \"sku\": {\r\n \"name\": \"pergb2018\",\r\n \"lastSkuUpdate\": + \"Thu, 24 Mar 2022 21:10:50 GMT\"\r\n },\r\n \"retentionInDays\": 30,\r\n + \ \"features\": {\r\n \"legacy\": 0,\r\n \"searchVersion\": 1,\r\n + \ \"enableLogAccessUsingOnlyResourcePermissions\": true\r\n },\r\n + \ \"workspaceCapping\": {\r\n \"dailyQuotaGb\": -1.0,\r\n \"quotaNextResetTime\": + \"Fri, 25 Mar 2022 06:00:00 GMT\",\r\n \"dataIngestionStatus\": \"RespectQuota\"\r\n + \ },\r\n \"publicNetworkAccessForIngestion\": \"Enabled\",\r\n \"publicNetworkAccessForQuery\": + \"Enabled\",\r\n \"createdDate\": \"Thu, 24 Mar 2022 21:10:50 GMT\",\r\n + \ \"modifiedDate\": \"Thu, 24 Mar 2022 21:10:50 GMT\"\r\n },\r\n \"id\": + \"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/microsoft.operationalinsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n + \ \"name\": \"workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n \"type\": \"Microsoft.OperationalInsights/workspaces\",\r\n + \ \"location\": \"eastus\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '1114' + content-type: + - application/json + date: + - Thu, 24 Mar 2022 21:10:51 GMT + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + x-powered-by: + - ASP.NET + - ASP.NET + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-mgmt-loganalytics/12.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/Microsoft.OperationalInsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf?api-version=2021-06-01 + response: + body: + string: "{\r\n \"properties\": {\r\n \"source\": \"Azure\",\r\n \"customerId\": + \"ebb686fa-c4ff-41b4-837a-f4998cd19621\",\r\n \"provisioningState\": \"Succeeded\",\r\n + \ \"sku\": {\r\n \"name\": \"pergb2018\",\r\n \"lastSkuUpdate\": + \"Thu, 24 Mar 2022 21:10:50 GMT\"\r\n },\r\n \"retentionInDays\": 30,\r\n + \ \"features\": {\r\n \"legacy\": 0,\r\n \"searchVersion\": 1,\r\n + \ \"enableLogAccessUsingOnlyResourcePermissions\": true\r\n },\r\n + \ \"workspaceCapping\": {\r\n \"dailyQuotaGb\": -1.0,\r\n \"quotaNextResetTime\": + \"Fri, 25 Mar 2022 06:00:00 GMT\",\r\n \"dataIngestionStatus\": \"RespectQuota\"\r\n + \ },\r\n \"publicNetworkAccessForIngestion\": \"Enabled\",\r\n \"publicNetworkAccessForQuery\": + \"Enabled\",\r\n \"createdDate\": \"Thu, 24 Mar 2022 21:10:50 GMT\",\r\n + \ \"modifiedDate\": \"Thu, 24 Mar 2022 21:10:51 GMT\"\r\n },\r\n \"id\": + \"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/microsoft.operationalinsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n + \ \"name\": \"workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n \"type\": \"Microsoft.OperationalInsights/workspaces\",\r\n + \ \"location\": \"eastus\"\r\n}" + headers: + cache-control: + - no-cache + content-length: + - '1115' + content-type: + - application/json + date: + - Thu, 24 Mar 2022 21:11:21 GMT + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + Content-Length: + - '0' + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-mgmt-loganalytics/12.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/Microsoft.OperationalInsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf/sharedKeys?api-version=2020-08-01 + response: + body: + string: "{\r\n \"primarySharedKey\": \"9tkPsi59o6/57v7/XCFxhMQIa22hPh1ZnTGUNVc4VbQHElDzKjPyVAiP+RvOTbvhie1nfDarykeIW2GyoeYO5A==\",\r\n + \ \"secondarySharedKey\": \"nOM5I4SOclSgHHRmHxZCO0nYMaTZiiN9/5S0If4qIjQmO8dRZsX/3NN6ue+UDMr6nNL3LcZhtKimnexYkIJmzg==\"\r\n}" + headers: + cache-control: + - no-cache + cachecontrol: + - no-cache + content-length: + - '235' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:11:22 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-ams-apiversion: + - WebAPI1.0 + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + x-powered-by: + - ASP.NET + - ASP.NET + status: + code: 200 + message: OK +- request: + body: '{"location": "centraluseuap", "tags": null, "properties": {"daprAIInstrumentationKey": + null, "vnetConfiguration": null, "internalLoadBalancerEnabled": false, "appLogsConfiguration": + {"destination": "log-analytics", "logAnalyticsConfiguration": {"customerId": + "ebb686fa-c4ff-41b4-837a-f4998cd19621", "sharedKey": "9tkPsi59o6/57v7/XCFxhMQIa22hPh1ZnTGUNVc4VbQHElDzKjPyVAiP+RvOTbvhie1nfDarykeIW2GyoeYO5A=="}}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + Content-Length: + - '406' + Content-Type: + - application/json + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719Z","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719Z"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + azure-asyncoperation: + - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App/locations/centraluseuap/managedEnvironmentOperationStatuses/6bb42c28-e754-4541-9479-fcfe8efa5a3f?api-version=2022-01-01-preview&azureAsyncOperation=true + cache-control: + - no-cache + content-length: + - '782' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:11:24 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-async-operation-timeout: + - PT15M + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + x-powered-by: + - ASP.NET + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:11:24 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:11:52 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:11:55 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:11:57 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:12:00 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:12:03 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '780' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:12:05 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env create + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Succeeded","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '782' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:12:09 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env list + Connection: + - keep-alive + ParameterSetName: + - -g + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North + Central US (Stage)","Central US EUAP","Canada Central","West Europe","North + Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '2714' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:08 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp env list + Connection: + - keep-alive + ParameterSetName: + - -g + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments?api-version=2022-01-01-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Succeeded","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}]}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '794' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:10 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North + Central US (Stage)","Central US EUAP","Canada Central","West Europe","North + Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '2714' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:10 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Succeeded","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' + headers: + api-supported-versions: + - 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '782' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:10 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North + Central US (Stage)","Central US EUAP","Canada Central","West Europe","North + Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '2714' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:11 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: '{"location": "centraluseuap", "identity": {"type": "None", "userAssignedIdentities": + null}, "properties": {"managedEnvironmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003", + "configuration": {"secrets": null, "activeRevisionsMode": "single", "ingress": + null, "dapr": null, "registries": null}, "template": {"revisionSuffix": null, + "containers": [{"image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", + "name": "containerapp-e2e000002", "command": null, "args": null, "env": null, + "resources": null, "volumeMounts": null}], "scale": null, "volumes": null}}, + "tags": null}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + Content-Length: + - '702' + Content-Type: + - application/json + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896Z","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896Z"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + azure-asyncoperation: + - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App/locations/centraluseuap/containerappOperationStatuses/263df507-52e1-466f-8d32-7b11626cceb5?api-version=2022-01-01-preview&azureAsyncOperation=true + cache-control: + - no-cache + content-length: + - '1171' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:15 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-ms-async-operation-timeout: + - PT15M + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + x-powered-by: + - ASP.NET + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:16 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:20 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:23 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:25 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:28 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:30 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:33 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:36 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:39 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:41 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:44 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:46 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:49 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1224' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:52 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp create + Connection: + - keep-alive + ParameterSetName: + - -g -n --environment + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"Succeeded","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1223' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:13:55 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp show + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, + CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North + Central US (Stage)","Canada Central","West Europe","North Europe","East US","East + US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North + Central US (Stage)","Central US EUAP","Canada Central","West Europe","North + Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '2714' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:14:55 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + CommandName: + - containerapp show + Connection: + - keep-alive + ParameterSetName: + - -g -n + User-Agent: + - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central + US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"Succeeded","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' + headers: + api-supported-versions: + - 2021-03-01, 2022-01-01-preview, 2022-03-01 + cache-control: + - no-cache + content-length: + - '1223' + content-type: + - application/json; charset=utf-8 + date: + - Thu, 24 Mar 2022 21:14:56 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - Microsoft-IIS/10.0 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +version: 1 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 8605f1fe426..f0fa3042f8f 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -4,14 +4,38 @@ # -------------------------------------------------------------------------------------------- import os +import time import unittest from azure.cli.testsdk.scenario_tests import AllowLargeResponse -from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, JMESPathCheck) TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) class ContainerappScenarioTest(ScenarioTest): - pass \ No newline at end of file + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="centraluseuap") + def test_containerapp_e2e(self, resource_group): + containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) + env_name = self.create_random_name(prefix='containerapp-e2e-env', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + # Sleep in case env create takes a while + time.sleep(60) + self.cmd('containerapp env list -g {}'.format(resource_group), checks=[ + JMESPathCheck('length(@)', 1), + JMESPathCheck('[0].name', env_name), + ]) + + self.cmd('containerapp create -g {} -n {} --environment {}'.format(resource_group, containerapp_name, env_name), checks=[ + JMESPathCheck('name', containerapp_name) + ]) + + # Sleep in case containerapp create takes a while + time.sleep(60) + self.cmd('containerapp show -g {} -n {}'.format(resource_group, containerapp_name), checks=[ + JMESPathCheck('name', containerapp_name) + ]) From a4d1ec2be2998b4d9e2d28a4b0b010da390bc04f Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Mar 2022 14:24:02 -0700 Subject: [PATCH 092/158] Remove managed-identity support for first release of CLI --- src/containerapp/azext_containerapp/_help.py | 39 ------------------- .../azext_containerapp/_params.py | 7 ---- .../azext_containerapp/commands.py | 5 --- src/containerapp/azext_containerapp/custom.py | 29 +------------- 4 files changed, 1 insertion(+), 79 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index dde874e3e55..baf70ae24b0 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -264,45 +264,6 @@ az containerapp env dapr-component remove -g MyResourceGroup --dapr-component-name MyDaprComponentName --name MyEnvironment """ -# Identity Commands -helps['containerapp identity'] = """ - type: group - short-summary: Commands to manage managed identities. -""" - -helps['containerapp identity assign'] = """ - type: command - 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: | - 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 -""" - -helps['containerapp identity remove'] = """ - type: command - short-summary: Remove a managed identity from a container app. - examples: - - name: Remove system identity. - text: | - az containerapp identity remove --identities [system] - - name: Remove system and user identity. - text: | - az containerapp identity remove --identities [system] myAssignedId -""" - -helps['containerapp identity show'] = """ - type: command - short-summary: Show managed identities of a container app. -""" - # Ingress Commands helps['containerapp ingress'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c70de7f12ac..6e0ee6918d4 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -75,7 +75,6 @@ def load_arguments(self, _): 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: @@ -113,12 +112,6 @@ 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/commands.py b/src/containerapp/azext_containerapp/commands.py index dd6f2d067dc..c5b924287f8 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -62,11 +62,6 @@ def load_command_table(self, _): g.custom_command('set', 'create_or_update_dapr_component') g.custom_command('remove', 'remove_dapr_component') - with self.command_group('containerapp 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_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_show_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 1f49d3c9302..15cea81f0ff 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -292,8 +292,7 @@ def create_containerapp(cmd, startup_command=None, args=None, tags=None, - no_wait=False, - assign_identity=None): + no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") if yaml: @@ -310,9 +309,6 @@ 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'] @@ -378,28 +374,6 @@ def create_containerapp(cmd, config_def["registries"] = [registries_def] if registries_def is not None else None config_def["dapr"] = dapr_def - # 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] = {} # pylint: disable=unsupported-assignment-operation - scale_def = None if min_replicas is not None or max_replicas is not None: scale_def = ScaleModel @@ -433,7 +407,6 @@ 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 From b0aab4f9e41f467d6d377b4d57449fdb12d8b4a7 Mon Sep 17 00:00:00 2001 From: Calvin Chan Date: Thu, 24 Mar 2022 14:38:22 -0700 Subject: [PATCH 093/158] Need to investigate test flakiness --- .../recordings/test_containerapp_e2e.yaml | 2490 ----------------- .../latest/test_containerapp_scenario.py | 1 + 2 files changed, 1 insertion(+), 2490 deletions(-) delete mode 100644 src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml diff --git a/src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml b/src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml deleted file mode 100644 index f6e414d6149..00000000000 --- a/src/containerapp/azext_containerapp/tests/latest/recordings/test_containerapp_e2e.yaml +++ /dev/null @@ -1,2490 +0,0 @@ -interactions: -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001","name":"clitest.rg000001","type":"Microsoft.Resources/resourceGroups","location":"centraluseuap","tags":{"product":"azurecli","cause":"automation","date":"2022-03-24T21:10:42Z"},"properties":{"provisioningState":"Succeeded"}}' - headers: - cache-control: - - no-cache - content-length: - - '317' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:10:42 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North - Central US (Stage)","Central US EUAP","Canada Central","West Europe","North - Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '2714' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:10:42 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North - Central US (Stage)","Central US EUAP","Canada Central","West Europe","North - Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '2714' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:10:43 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights","namespace":"Microsoft.OperationalInsights","authorizations":[{"applicationId":"d2a0a418-0aac-4541-82b2-b3142c89da77","roleDefinitionId":"86695298-2eb9-48a7-9ec3-2fdb38b6878b"},{"applicationId":"ca7f3f0b-7d91-482c-8e09-c5d840d0eac5","roleDefinitionId":"5d5a2e56-9835-44aa-93db-d2f19e155438"}],"resourceTypes":[{"resourceType":"workspaces","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2021-06-01","2021-03-01-privatepreview","2020-10-01","2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"querypacks","locations":["West - Central US","East US","South Central US","North Europe","West Europe","Southeast - Asia","West US 2","UK South","Canada Central","Central India","Japan East","Australia - East","Korea Central","France Central","Central US","East US 2","East Asia","West - US","South Africa North","North Central US","Brazil South","Switzerland North","Norway - East","Australia Southeast","Australia Central 2","Germany West Central","Switzerland - West","UAE Central","UK West","Brazil Southeast","Japan West","UAE North","Australia - Central","France South","South India","Jio India Central","Jio India West","Canada - East","West US 3","Sweden Central","Korea South"],"apiVersions":["2019-09-01-preview","2019-09-01"],"capabilities":"SupportsTags, - SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"locations/operationStatuses","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/scopedPrivateLinkProxies","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-03-01-preview","capabilities":"None"},{"resourceType":"workspaces/query","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/metadata","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/dataSources","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/linkedStorageAccounts","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/tables","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia East","Australia - Central","France Central","Korea Central","North Europe","Central US","East - Asia","East US 2","South Central US","North Central US","West US","UK West","South - Africa North","Brazil South","Switzerland North","Switzerland West","Germany - West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2020-08-01","2020-03-01-preview","2017-04-26-preview"],"capabilities":"None"},{"resourceType":"workspaces/storageInsightConfigs","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia East","Australia - Central","France Central","Korea Central","North Europe","Central US","East - Asia","East US 2","South Central US","North Central US","West US","UK West","South - Africa North","Brazil South","Switzerland North","Switzerland West","Germany - West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"storageInsightConfigs","locations":[],"apiVersions":["2020-08-01","2020-03-01-preview","2014-10-10"],"capabilities":"SupportsExtension"},{"resourceType":"workspaces/linkedServices","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"linkTargets","locations":["East - US"],"apiVersions":["2020-03-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"deletedWorkspaces","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"operations","locations":[],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview","2015-11-01-preview","2014-11-10"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"clusters","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Switzerland North","Switzerland West","Germany West Central","Australia - Central 2","UAE Central","Brazil South","UAE North","Japan West","Brazil Southeast","Norway - East","Norway West","France South","South India","Jio India Central","Jio - India West","Canada East","West US 3","Sweden Central","Korea South"],"apiVersions":["2021-06-01","2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2021-06-01","capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SystemAssignedResourceIdentity, SupportsTags, - SupportsLocation"},{"resourceType":"workspaces/dataExports","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '12146' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:10:43 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights","namespace":"Microsoft.OperationalInsights","authorizations":[{"applicationId":"d2a0a418-0aac-4541-82b2-b3142c89da77","roleDefinitionId":"86695298-2eb9-48a7-9ec3-2fdb38b6878b"},{"applicationId":"ca7f3f0b-7d91-482c-8e09-c5d840d0eac5","roleDefinitionId":"5d5a2e56-9835-44aa-93db-d2f19e155438"}],"resourceTypes":[{"resourceType":"workspaces","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2021-06-01","2021-03-01-privatepreview","2020-10-01","2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"querypacks","locations":["West - Central US","East US","South Central US","North Europe","West Europe","Southeast - Asia","West US 2","UK South","Canada Central","Central India","Japan East","Australia - East","Korea Central","France Central","Central US","East US 2","East Asia","West - US","South Africa North","North Central US","Brazil South","Switzerland North","Norway - East","Australia Southeast","Australia Central 2","Germany West Central","Switzerland - West","UAE Central","UK West","Brazil Southeast","Japan West","UAE North","Australia - Central","France South","South India","Jio India Central","Jio India West","Canada - East","West US 3","Sweden Central","Korea South"],"apiVersions":["2019-09-01-preview","2019-09-01"],"capabilities":"SupportsTags, - SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"locations/operationStatuses","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/scopedPrivateLinkProxies","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-03-01-preview","capabilities":"None"},{"resourceType":"workspaces/query","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/metadata","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/dataSources","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/linkedStorageAccounts","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/tables","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia East","Australia - Central","France Central","Korea Central","North Europe","Central US","East - Asia","East US 2","South Central US","North Central US","West US","UK West","South - Africa North","Brazil South","Switzerland North","Switzerland West","Germany - West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2020-08-01","2020-03-01-preview","2017-04-26-preview"],"capabilities":"None"},{"resourceType":"workspaces/storageInsightConfigs","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia East","Australia - Central","France Central","Korea Central","North Europe","Central US","East - Asia","East US 2","South Central US","North Central US","West US","UK West","South - Africa North","Brazil South","Switzerland North","Switzerland West","Germany - West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"storageInsightConfigs","locations":[],"apiVersions":["2020-08-01","2020-03-01-preview","2014-10-10"],"capabilities":"SupportsExtension"},{"resourceType":"workspaces/linkedServices","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"linkTargets","locations":["East - US"],"apiVersions":["2020-03-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"deletedWorkspaces","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"operations","locations":[],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview","2015-11-01-preview","2014-11-10"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"clusters","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Switzerland North","Switzerland West","Germany West Central","Australia - Central 2","UAE Central","Brazil South","UAE North","Japan West","Brazil Southeast","Norway - East","Norway West","France South","South India","Jio India Central","Jio - India West","Canada East","West US 3","Sweden Central","Korea South"],"apiVersions":["2021-06-01","2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2021-06-01","capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SystemAssignedResourceIdentity, SupportsTags, - SupportsLocation"},{"resourceType":"workspaces/dataExports","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '12146' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:10:43 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.OperationalInsights","namespace":"Microsoft.OperationalInsights","authorizations":[{"applicationId":"d2a0a418-0aac-4541-82b2-b3142c89da77","roleDefinitionId":"86695298-2eb9-48a7-9ec3-2fdb38b6878b"},{"applicationId":"ca7f3f0b-7d91-482c-8e09-c5d840d0eac5","roleDefinitionId":"5d5a2e56-9835-44aa-93db-d2f19e155438"}],"resourceTypes":[{"resourceType":"workspaces","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2021-06-01","2021-03-01-privatepreview","2020-10-01","2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"querypacks","locations":["West - Central US","East US","South Central US","North Europe","West Europe","Southeast - Asia","West US 2","UK South","Canada Central","Central India","Japan East","Australia - East","Korea Central","France Central","Central US","East US 2","East Asia","West - US","South Africa North","North Central US","Brazil South","Switzerland North","Norway - East","Australia Southeast","Australia Central 2","Germany West Central","Switzerland - West","UAE Central","UK West","Brazil Southeast","Japan West","UAE North","Australia - Central","France South","South India","Jio India Central","Jio India West","Canada - East","West US 3","Sweden Central","Korea South"],"apiVersions":["2019-09-01-preview","2019-09-01"],"capabilities":"SupportsTags, - SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"locations/operationStatuses","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/scopedPrivateLinkProxies","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-03-01-preview","capabilities":"None"},{"resourceType":"workspaces/query","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/metadata","locations":[],"apiVersions":["2017-10-01"],"capabilities":"None"},{"resourceType":"workspaces/dataSources","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/linkedStorageAccounts","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"workspaces/tables","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia East","Australia - Central","France Central","Korea Central","North Europe","Central US","East - Asia","East US 2","South Central US","North Central US","West US","UK West","South - Africa North","Brazil South","Switzerland North","Switzerland West","Germany - West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2020-08-01","2020-03-01-preview","2017-04-26-preview"],"capabilities":"None"},{"resourceType":"workspaces/storageInsightConfigs","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia East","Australia - Central","France Central","Korea Central","North Europe","Central US","East - Asia","East US 2","South Central US","North Central US","West US","UK West","South - Africa North","Brazil South","Switzerland North","Switzerland West","Germany - West Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2017-04-26-preview","2017-03-15-preview","2017-03-03-preview","2017-01-01-preview","2015-11-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"storageInsightConfigs","locations":[],"apiVersions":["2020-08-01","2020-03-01-preview","2014-10-10"],"capabilities":"SupportsExtension"},{"resourceType":"workspaces/linkedServices","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview","2015-11-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"linkTargets","locations":["East - US"],"apiVersions":["2020-03-01-preview","2015-03-20"],"capabilities":"None"},{"resourceType":"deletedWorkspaces","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"operations","locations":[],"apiVersions":["2021-12-01-preview","2020-10-01","2020-08-01","2020-03-01-preview","2015-11-01-preview","2014-11-10"],"defaultApiVersion":"2020-08-01","capabilities":"None"},{"resourceType":"clusters","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Switzerland North","Switzerland West","Germany West Central","Australia - Central 2","UAE Central","Brazil South","UAE North","Japan West","Brazil Southeast","Norway - East","Norway West","France South","South India","Jio India Central","Jio - India West","Canada East","West US 3","Sweden Central","Korea South"],"apiVersions":["2021-06-01","2020-10-01","2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2021-06-01","capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SystemAssignedResourceIdentity, SupportsTags, - SupportsLocation"},{"resourceType":"workspaces/dataExports","locations":["East - US","West Europe","Southeast Asia","Australia Southeast","West Central US","Japan - East","UK South","Central India","Canada Central","West US 2","Australia Central","Australia - East","France Central","Korea Central","North Europe","Central US","East Asia","East - US 2","South Central US","North Central US","West US","UK West","South Africa - North","Brazil South","Switzerland North","Switzerland West","Germany West - Central","Australia Central 2","UAE Central","UAE North","Japan West","Brazil - Southeast","Norway East","Norway West","France South","South India","Jio India - Central","Jio India West","Canada East","West US 3","Sweden Central","Korea - South"],"apiVersions":["2020-08-01","2020-03-01-preview","2019-08-01-preview"],"defaultApiVersion":"2020-08-01","capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '12146' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:10:44 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: '{"location": "eastus", "properties": {"publicNetworkAccessForIngestion": - "Enabled", "publicNetworkAccessForQuery": "Enabled"}}' - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - Content-Length: - - '126' - Content-Type: - - application/json - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-mgmt-loganalytics/12.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: PUT - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/Microsoft.OperationalInsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf?api-version=2021-06-01 - response: - body: - string: "{\r\n \"properties\": {\r\n \"source\": \"Azure\",\r\n \"customerId\": - \"ebb686fa-c4ff-41b4-837a-f4998cd19621\",\r\n \"provisioningState\": \"Creating\",\r\n - \ \"sku\": {\r\n \"name\": \"pergb2018\",\r\n \"lastSkuUpdate\": - \"Thu, 24 Mar 2022 21:10:50 GMT\"\r\n },\r\n \"retentionInDays\": 30,\r\n - \ \"features\": {\r\n \"legacy\": 0,\r\n \"searchVersion\": 1,\r\n - \ \"enableLogAccessUsingOnlyResourcePermissions\": true\r\n },\r\n - \ \"workspaceCapping\": {\r\n \"dailyQuotaGb\": -1.0,\r\n \"quotaNextResetTime\": - \"Fri, 25 Mar 2022 06:00:00 GMT\",\r\n \"dataIngestionStatus\": \"RespectQuota\"\r\n - \ },\r\n \"publicNetworkAccessForIngestion\": \"Enabled\",\r\n \"publicNetworkAccessForQuery\": - \"Enabled\",\r\n \"createdDate\": \"Thu, 24 Mar 2022 21:10:50 GMT\",\r\n - \ \"modifiedDate\": \"Thu, 24 Mar 2022 21:10:50 GMT\"\r\n },\r\n \"id\": - \"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/microsoft.operationalinsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n - \ \"name\": \"workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n \"type\": \"Microsoft.OperationalInsights/workspaces\",\r\n - \ \"location\": \"eastus\"\r\n}" - headers: - cache-control: - - no-cache - content-length: - - '1114' - content-type: - - application/json - date: - - Thu, 24 Mar 2022 21:10:51 GMT - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - x-content-type-options: - - nosniff - x-ms-ratelimit-remaining-subscription-writes: - - '1199' - x-powered-by: - - ASP.NET - - ASP.NET - status: - code: 201 - message: Created -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-mgmt-loganalytics/12.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/Microsoft.OperationalInsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf?api-version=2021-06-01 - response: - body: - string: "{\r\n \"properties\": {\r\n \"source\": \"Azure\",\r\n \"customerId\": - \"ebb686fa-c4ff-41b4-837a-f4998cd19621\",\r\n \"provisioningState\": \"Succeeded\",\r\n - \ \"sku\": {\r\n \"name\": \"pergb2018\",\r\n \"lastSkuUpdate\": - \"Thu, 24 Mar 2022 21:10:50 GMT\"\r\n },\r\n \"retentionInDays\": 30,\r\n - \ \"features\": {\r\n \"legacy\": 0,\r\n \"searchVersion\": 1,\r\n - \ \"enableLogAccessUsingOnlyResourcePermissions\": true\r\n },\r\n - \ \"workspaceCapping\": {\r\n \"dailyQuotaGb\": -1.0,\r\n \"quotaNextResetTime\": - \"Fri, 25 Mar 2022 06:00:00 GMT\",\r\n \"dataIngestionStatus\": \"RespectQuota\"\r\n - \ },\r\n \"publicNetworkAccessForIngestion\": \"Enabled\",\r\n \"publicNetworkAccessForQuery\": - \"Enabled\",\r\n \"createdDate\": \"Thu, 24 Mar 2022 21:10:50 GMT\",\r\n - \ \"modifiedDate\": \"Thu, 24 Mar 2022 21:10:51 GMT\"\r\n },\r\n \"id\": - \"/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/microsoft.operationalinsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n - \ \"name\": \"workspace-clitestrglxn4btvw3nvjgtj4uyfjf\",\r\n \"type\": \"Microsoft.OperationalInsights/workspaces\",\r\n - \ \"location\": \"eastus\"\r\n}" - headers: - cache-control: - - no-cache - content-length: - - '1115' - content-type: - - application/json - date: - - Thu, 24 Mar 2022 21:11:21 GMT - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - Content-Length: - - '0' - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-mgmt-loganalytics/12.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: POST - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/clitest.rg000001/providers/Microsoft.OperationalInsights/workspaces/workspace-clitestrglxn4btvw3nvjgtj4uyfjf/sharedKeys?api-version=2020-08-01 - response: - body: - string: "{\r\n \"primarySharedKey\": \"9tkPsi59o6/57v7/XCFxhMQIa22hPh1ZnTGUNVc4VbQHElDzKjPyVAiP+RvOTbvhie1nfDarykeIW2GyoeYO5A==\",\r\n - \ \"secondarySharedKey\": \"nOM5I4SOclSgHHRmHxZCO0nYMaTZiiN9/5S0If4qIjQmO8dRZsX/3NN6ue+UDMr6nNL3LcZhtKimnexYkIJmzg==\"\r\n}" - headers: - cache-control: - - no-cache - cachecontrol: - - no-cache - content-length: - - '235' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:11:22 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding - x-ams-apiversion: - - WebAPI1.0 - x-content-type-options: - - nosniff - x-ms-ratelimit-remaining-subscription-writes: - - '1199' - x-powered-by: - - ASP.NET - - ASP.NET - status: - code: 200 - message: OK -- request: - body: '{"location": "centraluseuap", "tags": null, "properties": {"daprAIInstrumentationKey": - null, "vnetConfiguration": null, "internalLoadBalancerEnabled": false, "appLogsConfiguration": - {"destination": "log-analytics", "logAnalyticsConfiguration": {"customerId": - "ebb686fa-c4ff-41b4-837a-f4998cd19621", "sharedKey": "9tkPsi59o6/57v7/XCFxhMQIa22hPh1ZnTGUNVc4VbQHElDzKjPyVAiP+RvOTbvhie1nfDarykeIW2GyoeYO5A=="}}}}' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - Content-Length: - - '406' - Content-Type: - - application/json - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: PUT - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719Z","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719Z"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - azure-asyncoperation: - - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App/locations/centraluseuap/managedEnvironmentOperationStatuses/6bb42c28-e754-4541-9479-fcfe8efa5a3f?api-version=2022-01-01-preview&azureAsyncOperation=true - cache-control: - - no-cache - content-length: - - '782' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:11:24 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - x-content-type-options: - - nosniff - x-ms-async-operation-timeout: - - PT15M - x-ms-ratelimit-remaining-subscription-writes: - - '1199' - x-powered-by: - - ASP.NET - status: - code: 201 - message: Created -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:11:24 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:11:52 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:11:55 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:11:57 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:12:00 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:12:03 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Waiting","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '780' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:12:05 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env create - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Succeeded","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '782' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:12:09 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env list - Connection: - - keep-alive - ParameterSetName: - - -g - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North - Central US (Stage)","Central US EUAP","Canada Central","West Europe","North - Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '2714' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:08 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp env list - Connection: - - keep-alive - ParameterSetName: - - -g - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments?api-version=2022-01-01-preview - response: - body: - string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Succeeded","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}]}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '794' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:10 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North - Central US (Stage)","Central US EUAP","Canada Central","West Europe","North - Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '2714' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:10 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","name":"containerapp-e2e-env000003","type":"Microsoft.App/managedenvironments","location":"centraluseuap","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:11:24.3295719","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:11:24.3295719"},"properties":{"provisioningState":"Succeeded","defaultDomain":"bluebush-2c6a1643.centraluseuap.azurecontainerapps.io","staticIp":"20.45.253.238","appLogsConfiguration":{"destination":"log-analytics","logAnalyticsConfiguration":{"customerId":"ebb686fa-c4ff-41b4-837a-f4998cd19621"}}}}' - headers: - api-supported-versions: - - 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '782' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:10 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North - Central US (Stage)","Central US EUAP","Canada Central","West Europe","North - Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '2714' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:11 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: '{"location": "centraluseuap", "identity": {"type": "None", "userAssignedIdentities": - null}, "properties": {"managedEnvironmentId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedEnvironments/containerapp-e2e-env000003", - "configuration": {"secrets": null, "activeRevisionsMode": "single", "ingress": - null, "dapr": null, "registries": null}, "template": {"revisionSuffix": null, - "containers": [{"image": "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest", - "name": "containerapp-e2e000002", "command": null, "args": null, "env": null, - "resources": null, "volumeMounts": null}], "scale": null, "volumes": null}}, - "tags": null}' - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - Content-Length: - - '702' - Content-Type: - - application/json - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: PUT - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896Z","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896Z"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - azure-asyncoperation: - - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App/locations/centraluseuap/containerappOperationStatuses/263df507-52e1-466f-8d32-7b11626cceb5?api-version=2022-01-01-preview&azureAsyncOperation=true - cache-control: - - no-cache - content-length: - - '1171' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:15 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - x-content-type-options: - - nosniff - x-ms-async-operation-timeout: - - PT15M - x-ms-ratelimit-remaining-subscription-writes: - - '1199' - x-powered-by: - - ASP.NET - status: - code: 201 - message: Created -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:16 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:20 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:23 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:25 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:28 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:30 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:33 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:36 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:39 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:41 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:44 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:46 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:49 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"InProgress","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1224' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:52 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp create - Connection: - - keep-alive - ParameterSetName: - - -g -n --environment - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"Succeeded","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1223' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:13:55 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - application/json - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp show - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - AZURECLI/2.33.0 azsdk-python-azure-mgmt-resource/20.0.0 Python/3.6.7 (Windows-10-10.0.22000-SP0) - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App?api-version=2021-04-01 - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.App","namespace":"Microsoft.App","authorizations":[{"applicationId":"7e3bc4fd-85a3-4192-b177-5b8bfc87f42c","roleDefinitionId":"39a74f72-b40f-4bdc-b639-562fe2260bf0"},{"applicationId":"3734c1a4-2bed-4998-a37a-ff1a9e7bf019","roleDefinitionId":"5c779a4f-5cb2-4547-8c41-478d9be8ba90"}],"resourceTypes":[{"resourceType":"managedEnvironments","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"managedEnvironments/certificates","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"containerApps","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"CrossResourceGroupResourceMove, - CrossSubscriptionResourceMove, SupportsTags, SupportsLocation"},{"resourceType":"locations","locations":[],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/managedEnvironmentOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationResults","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"locations/containerappOperationStatuses","locations":["North - Central US (Stage)","Canada Central","West Europe","North Europe","East US","East - US 2","Central US EUAP"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"},{"resourceType":"operations","locations":["North - Central US (Stage)","Central US EUAP","Canada Central","West Europe","North - Europe","East US","East US 2"],"apiVersions":["2022-01-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' - headers: - cache-control: - - no-cache - content-length: - - '2714' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:14:55 GMT - expires: - - '-1' - pragma: - - no-cache - strict-transport-security: - - max-age=31536000; includeSubDomains - vary: - - Accept-Encoding - x-content-type-options: - - nosniff - status: - code: 200 - message: OK -- request: - body: null - headers: - Accept: - - '*/*' - Accept-Encoding: - - gzip, deflate - CommandName: - - containerapp show - Connection: - - keep-alive - ParameterSetName: - - -g -n - User-Agent: - - python/3.6.7 (Windows-10-10.0.22000-SP0) AZURECLI/2.33.0 - method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002?api-version=2022-01-01-preview - response: - body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/containerApps/containerapp-e2e000002","name":"containerapp-e2e000002","type":"Microsoft.App/containerApps","location":"Central - US EUAP","systemData":{"createdBy":"calcha@microsoft.com","createdByType":"User","createdAt":"2022-03-24T21:13:14.097896","lastModifiedBy":"calcha@microsoft.com","lastModifiedByType":"User","lastModifiedAt":"2022-03-24T21:13:14.097896"},"properties":{"provisioningState":"Succeeded","managedEnvironmentId":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/clitest.rg000001/providers/Microsoft.App/managedenvironments/containerapp-e2e-env000003","outboundIpAddresses":["20.51.24.126","20.51.25.94","20.51.25.111"],"latestRevisionName":"containerapp-e2e000002--j33jrw5","latestRevisionFqdn":"","customDomainVerificationId":"A5B12E93C19D795D34DF79C449C2C3BAF58329ACBE54CAAF49EDCD93822ACCE8","configuration":{"activeRevisionsMode":"Single"},"template":{"containers":[{"image":"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest","name":"containerapp-e2e000002","resources":{"cpu":0.5,"memory":"1Gi"}}],"scale":{"maxReplicas":10}}},"identity":{"type":"None"}}' - headers: - api-supported-versions: - - 2021-03-01, 2022-01-01-preview, 2022-03-01 - cache-control: - - no-cache - content-length: - - '1223' - content-type: - - application/json; charset=utf-8 - date: - - Thu, 24 Mar 2022 21:14:56 GMT - expires: - - '-1' - pragma: - - no-cache - server: - - Microsoft-IIS/10.0 - strict-transport-security: - - max-age=31536000; includeSubDomains - transfer-encoding: - - chunked - vary: - - Accept-Encoding,Accept-Encoding - x-content-type-options: - - nosniff - x-powered-by: - - ASP.NET - status: - code: 200 - message: OK -version: 1 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 f0fa3042f8f..9a89dcc55c9 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -14,6 +14,7 @@ TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) +@unittest.skip("Managed environment flaky") class ContainerappScenarioTest(ScenarioTest): @AllowLargeResponse(8192) @ResourceGroupPreparer(location="centraluseuap") From 5d0bef3d63cf9460416a95d326fd3a108dd3967a Mon Sep 17 00:00:00 2001 From: Sisira Panchagnula Date: Fri, 25 Mar 2022 00:18:11 -0700 Subject: [PATCH 094/158] Update _help.py removing duplicate help --- src/containerapp/azext_containerapp/_help.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index baf70ae24b0..5e75c334d82 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -162,14 +162,6 @@ """ -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'] = """ From 03ed09a3c741c754d440cd73786403149b07c7ae Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 29 Mar 2022 14:45:50 -0400 Subject: [PATCH 095/158] Added prototype of container up. --- .../azext_containerapp/_params.py | 6 + .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 131 ++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 6e0ee6918d4..c4f6505379b 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -176,3 +176,9 @@ def load_arguments(self, _): with self.argument_context('containerapp revision list') as c: c.argument('name', id_part=None) + + with self.argument_context('containerapp up') as c: + c.argument('resource_group_name', configured_default='resource_group_name') + c.argument('location', configured_default='location') + c.argument('name', configured_default='name') + c.argument('managed_env', configured_default='managed_env') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index c5b924287f8..0d7b6090d0f 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -49,6 +49,7 @@ def load_command_table(self, _): 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()) + g.custom_command('up', 'containerapp_up', supports_no_wait=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 15cea81f0ff..ba9294710f6 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1932,3 +1932,134 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ return r except Exception as e: handle_raw_exception(e) + +def containerapp_up(cmd, + name=None, + resource_group_name=None, + managed_env=None, + location=None, + registry=None, + image=None, + source=None, + dockerfile=None, + # compose=None, + ingress=None, + port=None, + registry_username=None, + registry_password=None, + env_vars=None, + dryrun=False, + no_wait=False): + import os + src_dir = os.getcwd() + _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) + + if not name: + if image: + name = image.split('/')[-1].split(':')[0].lower() # .azurecr.io/: + if source: + name = source.replace('.', '').replace('/', '').lower() + + if not resource_group_name: + try: + rg_found = False + containerapps = list_containerapp(cmd) + for containerapp in containerapps: + if containerapp["name"].lower() == name.lower(): + if rg_found: + raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) + # could also just do resource_group_name = None here and create a new one, ask Anthony + # break + if containerapp["id"][0] != '/': + containerapp["id"] = '/' + containerapp["id"] + rg_found = True + resource_group_name = containerapp["id"].split('/')[4] + except: + # error handle maybe + pass + + if "azurecr.io" in image: + if registry_username is None or registry_password is None: + # If registry is Azure Container Registry, we can try inferring credentials + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(image) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + + try: + registry_username, registry_password = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + if not containerapp_def: + if not location: + location = "canadacentral" # check user's default location? + if not resource_group_name: + user = get_profile_username() + rg_name = get_randomized_name(user, resource_group_name) + create_resource_group(cmd, rg_name, location) + resource_group_name = rg_name + if not managed_env: + env_name = "{}-env".format(name).replace("_","-") + managed_env = create_managed_environment(cmd, env_name, location = "canadacentral", resource_group_name=resource_group_name)["id"] + _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env) + return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, no_wait=no_wait) + else: + return + + + +def get_randomized_name(prefix, name=None): + from random import randint + default = "{}_rg_{:04}".format(prefix, randint(0, 9999)) + if name is not None: + return name + return default + + +def _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env): + from azure.cli.core.util import ConfiguredDefaultSetter + with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): + logger.warning("Setting 'az containerapp up' default arguments for current directory. " + "Manage defaults with 'az configure --scope local'") + + + cmd.cli_ctx.config.set_value('defaults', 'resource_group_name', resource_group_name) + logger.warning("--resource-group/-g default: %s", resource_group_name) + + + cmd.cli_ctx.config.set_value('defaults', 'location', location) + logger.warning("--location/-l default: %s", location) + + + cmd.cli_ctx.config.set_value('defaults', 'name', name) + logger.warning("--name/-n default: %s", name) + + cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) + logger.warning("--environment default: %s", managed_env) + + +def get_profile_username(): + from azure.cli.core._profile import Profile + user = Profile().get_current_account_user() + user = user.split('@', 1)[0] + if len(user.split('#', 1)) > 1: # on cloudShell user is in format live.com#user@domain.com + user = user.split('#', 1)[1] + return user + +def create_resource_group(cmd, rg_name, location): + from azure.cli.core.profiles import ResourceType, get_sdk + rcf = _resource_client_factory(cmd.cli_ctx) + resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') + rg_params = resource_group(location=location) + return rcf.resource_groups.create_or_update(rg_name, rg_params) + +def _resource_client_factory(cli_ctx, **_): + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.cli.core.profiles import ResourceType + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) \ No newline at end of file From 5797a89dfbea7e55d405099376d0b9ac1a5e4e5b Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 29 Mar 2022 14:52:48 -0400 Subject: [PATCH 096/158] Fixed deploy from acr registry image infer credentials issue. --- src/containerapp/azext_containerapp/custom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ba9294710f6..ce315d88827 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1938,7 +1938,7 @@ def containerapp_up(cmd, resource_group_name=None, managed_env=None, location=None, - registry=None, + registry_server=None, image=None, source=None, dockerfile=None, @@ -1982,6 +1982,7 @@ def containerapp_up(cmd, if registry_username is None or registry_password is None: # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + registry_server=image.split('/')[0] parsed = urlparse(image) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] @@ -2008,7 +2009,7 @@ def containerapp_up(cmd, env_name = "{}-env".format(name).replace("_","-") managed_env = create_managed_environment(cmd, env_name, location = "canadacentral", resource_group_name=resource_group_name)["id"] _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env) - return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, no_wait=no_wait) + return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, no_wait=no_wait) else: return From 991036676cee07c88697a0db683e389cd37c5ed1 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 30 Mar 2022 16:22:34 -0400 Subject: [PATCH 097/158] Tried to add source. --- src/containerapp/azext_containerapp/_utils.py | 6 +- src/containerapp/azext_containerapp/custom.py | 154 +++++++++++++++--- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 7376167f592..13f862c9580 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -162,7 +162,7 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ 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 + # 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 @@ -234,7 +234,7 @@ def _generate_log_analytics_workspace_name(resource_group_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.") + # logger.warning("No Log Analytics workspace provided.") try: _validate_subscription_registered(cmd, "Microsoft.OperationalInsights") log_analytics_client = log_analytics_client_factory(cmd.cli_ctx) @@ -251,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)) # pylint: disable=logging-format-interpolation + # 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) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ce315d88827..35b005e45e4 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -292,6 +292,7 @@ def create_containerapp(cmd, startup_command=None, args=None, tags=None, + disable_warnings=False, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") @@ -300,7 +301,7 @@ def create_containerapp(cmd, 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') + not disable_warnings and logger.warning('Additional flags were passed along with --yaml. These flags will be ignored, and the configuration defined in the yaml will be used instead') return create_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait) if not image: @@ -417,12 +418,12 @@ def create_containerapp(cmd, 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)) + not disable_warnings and 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"])) + not disable_warnings and 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") + not disable_warnings and logger.warning("\nContainer app created. To access it over HTTPS, enable ingress: az containerapp ingress enable --help\n") return r except Exception as e: @@ -447,6 +448,7 @@ def update_containerapp(cmd, startup_command=None, args=None, tags=None, + disable_warnings=False, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") @@ -501,6 +503,9 @@ def update_containerapp(cmd, if image is not None: c["image"] = image + if remove_all_env_vars: + c["env"] = [] + if set_env_vars is not None: if "env" not in c or not c["env"]: c["env"] = [] @@ -519,9 +524,6 @@ def update_containerapp(cmd, # 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 @@ -606,7 +608,7 @@ def update_containerapp(cmd, 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)) + not disable_warnings and 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: @@ -659,6 +661,7 @@ def create_managed_environment(cmd, platform_reserved_dns_ip=None, internal_only=False, tags=None, + disable_warnings=False, no_wait=False): location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) @@ -713,9 +716,9 @@ 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 env show -n {} -g {}`'.format(name, resource_group_name)) + not disable_warnings and 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") + not disable_warnings and logger.warning("\nContainer Apps environment created. To deploy a container app, use: az containerapp create --help\n") return r except Exception as e: @@ -1410,7 +1413,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, allow_insecure=False, disable_warnings=False, no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1444,7 +1447,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"])) + not disable_warnings and 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) @@ -1566,7 +1569,7 @@ def list_registry(cmd, name, resource_group_name): 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): +def set_registry(cmd, name, resource_group_name, server, username=None, password=None, disable_warnings=False, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1592,7 +1595,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, 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...') + not disable_warnings and 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] @@ -1605,7 +1608,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password updating_existing_registry = False for r in registries_def: if r['server'].lower() == server.lower(): - logger.warning("Updating existing registry.") + not disable_warnings and logger.warning("Updating existing registry.") updating_existing_registry = True if username: r["username"] = username @@ -1933,6 +1936,7 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ except Exception as e: handle_raw_exception(e) + def containerapp_up(cmd, name=None, resource_group_name=None, @@ -1949,6 +1953,8 @@ def containerapp_up(cmd, registry_password=None, env_vars=None, dryrun=False, + logs_customer_id=None, + logs_key=None, no_wait=False): import os src_dir = os.getcwd() @@ -1959,6 +1965,10 @@ def containerapp_up(cmd, name = image.split('/')[-1].split(':')[0].lower() # .azurecr.io/: if source: name = source.replace('.', '').replace('/', '').lower() + if not image: + image = name # not sure if both allowed + print(image) + print(name) if not resource_group_name: try: @@ -1997,27 +2007,37 @@ def containerapp_up(cmd, except: pass + if source: + queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", image, source) + if not containerapp_def: if not location: - location = "canadacentral" # check user's default location? + location = "eastus2" # check user's default location? find least populated server? if not resource_group_name: user = get_profile_username() rg_name = get_randomized_name(user, resource_group_name) + logger.warning("Creating new resource group {}".format(rg_name)) create_resource_group(cmd, rg_name, location) resource_group_name = rg_name if not managed_env: env_name = "{}-env".format(name).replace("_","-") - managed_env = create_managed_environment(cmd, env_name, location = "canadacentral", resource_group_name=resource_group_name)["id"] + logger.warning("Creating new managed environment {}".format(env_name)) + managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] + else: + location = containerapp_def["location"] + managed_env = containerapp_def["properties"]["managedEnvironmentId"] + env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] + if logs_customer_id and logs_key: + managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] + if source: _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env) - return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, no_wait=no_wait) - else: - return + return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, disable_warnings=True, no_wait=no_wait) -def get_randomized_name(prefix, name=None): +def get_randomized_name(prefix, name=None, initial="rg"): from random import randint - default = "{}_rg_{:04}".format(prefix, randint(0, 9999)) + default = "{}_{}_{:04}".format(prefix, initial, randint(0, 9999)) if name is not None: return name return default @@ -2053,6 +2073,7 @@ def get_profile_username(): user = user.split('#', 1)[1] return user + def create_resource_group(cmd, rg_name, location): from azure.cli.core.profiles import ResourceType, get_sdk rcf = _resource_client_factory(cmd.cli_ctx) @@ -2060,7 +2081,94 @@ def create_resource_group(cmd, rg_name, location): rg_params = resource_group(location=location) return rcf.resource_groups.create_or_update(rg_name, rg_params) + def _resource_client_factory(cli_ctx, **_): from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.profiles import ResourceType - return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) \ No newline at end of file + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) + + +def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir): + import os, uuid, tempfile + from azure.cli.command_modules.acr._archive_utils import upload_source_code + from azure.cli.command_modules.acr._stream_utils import stream_logs + from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks + + from azure.cli.core.commands import LongRunningOperation + + # client_registries = get_acr_service_client(cmd.cli_ctx).registries + client_registries = cf_acr_registries_tasks(cmd.cli_ctx) + + + if not os.path.isdir(src_dir): + raise CLIError("Source directory should be a local directory path.") + + + docker_file_path = os.path.join(src_dir, "Dockerfile") + if not os.path.isfile(docker_file_path): + raise CLIError("Unable to find '{}'.".format(docker_file_path)) + + + # NOTE: os.path.basename is unable to parse "\" in the file path + original_docker_file_name = os.path.basename(docker_file_path.replace("\\", "/")) + docker_file_in_tar = '{}_{}'.format(uuid.uuid4().hex, original_docker_file_name) + tar_file_path = os.path.join(tempfile.gettempdir(), 'build_archive_{}.tar.gz'.format(uuid.uuid4().hex)) + + + source_location = upload_source_code(cmd, client_registries, registry_name, registry_rg, src_dir, tar_file_path, docker_file_path, docker_file_in_tar) + + # For local source, the docker file is added separately into tar as the new file name (docker_file_in_tar) + # So we need to update the docker_file_path + docker_file_path = docker_file_in_tar + + + from azure.cli.core.profiles import ResourceType + OS, Architecture = cmd.get_models('OS', 'Architecture', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') + # Default platform values + platform_os = OS.linux.value + platform_arch = Architecture.amd64.value + platform_variant = None + + + DockerBuildRequest, PlatformProperties = cmd.get_models('DockerBuildRequest', 'PlatformProperties', + resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') + docker_build_request = DockerBuildRequest( + image_names=[img_name], + is_push_enabled=True, + source_location=source_location, + platform=PlatformProperties( + os=platform_os, + architecture=platform_arch, + variant=platform_variant + ), + docker_file_path=docker_file_path, + timeout=None, + arguments=[]) + + + queued_build = LongRunningOperation(cmd.cli_ctx)(client_registries.begin_schedule_run( + resource_group_name=registry_rg, + registry_name=registry_name, + run_request=docker_build_request)) + + + run_id = queued_build.run_id + logger.warning("Queued a build with ID: %s", run_id) + logger.warning("Waiting for agent...") + + from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) + # client_runs = get_acr_service_client(cmd.cli_ctx).runs + client_runs = cf_acr_runs + + + + return stream_logs(client_runs, run_id, registry_name, registry_rg, False, True) + + +def get_acr_service_client(cli_ctx, api_version=None): + """Returns the client for managing container registries. """ + from azure.mgmt.containerregistry import ContainerRegistryManagementClient + from azure.cli.core.profiles import ResourceType + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_CONTAINERREGISTRY, api_version="2019-06-01-preview") \ No newline at end of file From c8e460e03b8e000268b0ba8544cbf1ad5e81a981 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 31 Mar 2022 14:10:36 -0400 Subject: [PATCH 098/158] Added acr build. --- src/containerapp/azext_containerapp/custom.py | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 35b005e45e4..10742fb85bd 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1964,11 +1964,10 @@ def containerapp_up(cmd, if image: name = image.split('/')[-1].split(':')[0].lower() # .azurecr.io/: if source: - name = source.replace('.', '').replace('/', '').lower() - if not image: - image = name # not sure if both allowed - print(image) - print(name) + temp = source[1:] if source[0] == '.' else source # replace first . if it exists + name = temp.split('/')[-1].lower() # replace first . if it exists + if len(name) == 0: + name = _src_path_escaped.split('\\')[-1] if not resource_group_name: try: @@ -1988,7 +1987,7 @@ def containerapp_up(cmd, # error handle maybe pass - if "azurecr.io" in image: + if image is not None and "azurecr.io" in image: if registry_username is None or registry_password is None: # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') @@ -2001,15 +2000,32 @@ def containerapp_up(cmd, except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + if source is not None: + if registry_server: + if registry_username is None or registry_password is None: + if "azurecr.io" in registry_server: + # If registry is Azure Container Registry, we can try inferring credentials + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + registry_username, registry_password = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + else: + raise RequiredArgumentMissingError("Registry usename and password are required if using non-Azure registry.") + else: + # create ACR here + pass + image = registry_server + '/' + name + queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", name, source) + containerapp_def = None try: containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) except: pass - if source: - queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", image, source) - if not containerapp_def: if not location: location = "eastus2" # check user's default location? find least populated server? @@ -2029,7 +2045,8 @@ def containerapp_up(cmd, env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] if logs_customer_id and logs_key: managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] - if source: + + if source is not None: _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env) return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, disable_warnings=True, no_wait=no_wait) @@ -2157,18 +2174,8 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir): logger.warning("Waiting for agent...") from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) - # client_runs = get_acr_service_client(cmd.cli_ctx).runs - client_runs = cf_acr_runs + client_runs = cf_acr_runs(cmd.cli_ctx) + return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) - return stream_logs(client_runs, run_id, registry_name, registry_rg, False, True) - - -def get_acr_service_client(cli_ctx, api_version=None): - """Returns the client for managing container registries. """ - from azure.mgmt.containerregistry import ContainerRegistryManagementClient - from azure.cli.core.profiles import ResourceType - from azure.cli.core.commands.client_factory import get_mgmt_service_client - - return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_CONTAINERREGISTRY, api_version="2019-06-01-preview") \ No newline at end of file From 52abab912eb97652213766c37b56fa8dab11096d Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 31 Mar 2022 14:24:01 -0400 Subject: [PATCH 099/158] Finished acr build functionality. --- src/containerapp/azext_containerapp/custom.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 10742fb85bd..0bef38f8419 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1960,6 +1960,9 @@ def containerapp_up(cmd, src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) + if source is None and image is None: + raise RequiredArgumentMissingError("You must specify either --source or --image.") + if not name: if image: name = image.split('/')[-1].split(':')[0].lower() # .azurecr.io/: @@ -2016,9 +2019,15 @@ def containerapp_up(cmd, raise RequiredArgumentMissingError("Registry usename and password are required if using non-Azure registry.") else: # create ACR here + # set registry_server to created acr pass - image = registry_server + '/' + name - queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", name, source) + if image is None: + image = registry_server + '/' + name + queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", name, source) + else: + image = registry_server + '/' + image + queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", image, source) + containerapp_def = None try: From 4a71bb809298ececf04f1a15391b9631ce3a5df5 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 31 Mar 2022 16:08:17 -0400 Subject: [PATCH 100/158] Added acr create functionality and pull registry from existing containerapp if it exists. --- .../azext_containerapp/_params.py | 3 +- src/containerapp/azext_containerapp/custom.py | 94 ++++++++++++++----- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index c4f6505379b..24fece9d6a6 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -181,4 +181,5 @@ def load_arguments(self, _): c.argument('resource_group_name', configured_default='resource_group_name') c.argument('location', configured_default='location') c.argument('name', configured_default='name') - c.argument('managed_env', configured_default='managed_env') \ No newline at end of file + c.argument('managed_env', configured_default='managed_env') + c.argument('registry_server', configured_default='registry_server') \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 0bef38f8419..89614e14d4f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -5,7 +5,7 @@ # 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.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( RequiredArgumentMissingError, ValidationError, @@ -1972,6 +1972,9 @@ def containerapp_up(cmd, if len(name) == 0: name = _src_path_escaped.split('\\')[-1] + if not location: + location = "eastus2" # check user's default location? find least populated server? + if not resource_group_name: try: rg_found = False @@ -1990,6 +1993,12 @@ def containerapp_up(cmd, # error handle maybe pass + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + if image is not None and "azurecr.io" in image: if registry_username is None or registry_password is None: # If registry is Azure Container Registry, we can try inferring credentials @@ -1997,13 +2006,17 @@ def containerapp_up(cmd, registry_server=image.split('/')[0] parsed = urlparse(image) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: registry_username, registry_password = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex if source is not None: + if containerapp_def: + if "registries" in containerapp_def["properties"]["configuration"] and len(containerapp_def["properties"]["configuration"]["registries"]) == 1: + registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] + registry_name = "" + registry_rg = "" if registry_server: if registry_username is None or registry_password is None: if "azurecr.io" in registry_server: @@ -2012,32 +2025,25 @@ def containerapp_up(cmd, parsed = urlparse(registry_server) 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) + registry_username, registry_password, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex else: raise RequiredArgumentMissingError("Registry usename and password are required if using non-Azure registry.") else: - # create ACR here - # set registry_server to created acr - pass - if image is None: - image = registry_server + '/' + name - queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", name, source) - else: - image = registry_server + '/' + image - queue_acr_build(cmd, "my-container-apps", "haroonftstregistry", image, source) + registry_rg = resource_group_name + user = get_profile_username() + registry_name = "{}acr".format(name) + registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-","") + logger.warning("Creating new acr {}".format(registry_name)) + registry_def = create_new_acr(cmd, registry_name, registry_rg, location) + registry_server = registry_def.login_server - - containerapp_def = None - try: - containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except: - pass + image_name = image if image is not None else name + image = registry_server + '/' + image_name + queue_acr_build(cmd, registry_rg, registry_name, image_name, source) if not containerapp_def: - if not location: - location = "eastus2" # check user's default location? find least populated server? if not resource_group_name: user = get_profile_username() rg_name = get_randomized_name(user, resource_group_name) @@ -2056,7 +2062,7 @@ def containerapp_up(cmd, managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] if source is not None: - _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env) + _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env, registry_server) return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, disable_warnings=True, no_wait=no_wait) @@ -2069,7 +2075,7 @@ def get_randomized_name(prefix, name=None, initial="rg"): return default -def _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env): +def _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env, registry_server): from azure.cli.core.util import ConfiguredDefaultSetter with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): logger.warning("Setting 'az containerapp up' default arguments for current directory. " @@ -2090,6 +2096,10 @@ def _set_webapp_up_default_args(cmd, resource_group_name, location, name, manage cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) logger.warning("--environment default: %s", managed_env) + cmd.cli_ctx.config.set_value('defaults', 'registry_server', registry_server) + logger.warning("--registry-server default: %s", registry_server) + + def get_profile_username(): from azure.cli.core._profile import Profile @@ -2188,3 +2198,43 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir): return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) +def _get_acr_cred(cli_ctx, registry_name): + from azure.mgmt.containerregistry import ContainerRegistryManagementClient + from azure.cli.core.commands.parameters import get_resources_in_subscription + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + client = get_mgmt_service_client(cli_ctx, ContainerRegistryManagementClient).registries + + result = get_resources_in_subscription(cli_ctx, 'Microsoft.ContainerRegistry/registries') + result = [item for item in result if item.name.lower() == registry_name] + if not result or len(result) > 1: + raise ResourceNotFoundError("No resource or more than one were found with name '{}'.".format(registry_name)) + resource_group_name = parse_resource_id(result[0].id)['resource_group'] + + registry = client.get(resource_group_name, registry_name) + + if registry.admin_user_enabled: # pylint: disable=no-member + cred = client.list_credentials(resource_group_name, registry_name) + return cred.username, cred.passwords[0].value, resource_group_name + raise ResourceNotFoundError("Failed to retrieve container registry credentials. Please either provide the " + "credentials or run 'az acr update -n {} --admin-enabled true' to enable " + "admin first.".format(registry_name)) + + +def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku="Basic"): + # from azure.cli.command_modules.acr.custom import acr_create + from azure.cli.command_modules.acr._client_factory import cf_acr_registries + from azure.cli.core.profiles import ResourceType + + + client = cf_acr_registries(cmd.cli_ctx) + # return acr_create(cmd, client, registry_name, resource_group_name, sku, location) + + Registry, Sku, NetworkRuleSet = cmd.get_models('Registry', 'Sku', 'NetworkRuleSet', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group="registries") + registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=True, + zone_redundancy=None, tags=None) + + # lro_poller = client.begin_create(resource_group_name, registry_name, registry, polling=False) + lro_poller = client._create_initial(resource_group_name, registry_name, registry) + + return lro_poller From bcb1954d3bcba06f753dfdb7a0f6d3855fb9826d Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 1 Apr 2022 14:33:18 -0400 Subject: [PATCH 101/158] Fixed bugs. --- src/containerapp/azext_containerapp/_utils.py | 4 +- src/containerapp/azext_containerapp/custom.py | 111 +++++++++++------- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 13f862c9580..57e0fd44108 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -562,11 +562,11 @@ def _get_app_from_revision(revision): return revision -def _infer_acr_credentials(cmd, registry_server): +def _infer_acr_credentials(cmd, registry_server, disable_warnings=False): # If registry is Azure Container Registry, we can try inferring credentials if '.azurecr.io' not in registry_server: raise RequiredArgumentMissingError('Registry username and password are required if not using Azure Container Registry.') - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up credentials...') + not disable_warnings and 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] diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 89614e14d4f..34214a94dcf 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -351,7 +351,7 @@ def create_containerapp(cmd, # Infer credentials if not supplied and its azurecr if registry_user is None or registry_pass is None: - registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server, disable_warnings) registries_def["server"] = registry_server registries_def["username"] = registry_user @@ -1945,7 +1945,7 @@ def containerapp_up(cmd, registry_server=None, image=None, source=None, - dockerfile=None, + dockerfile="Dockerfile", # compose=None, ingress=None, port=None, @@ -1956,7 +1956,7 @@ def containerapp_up(cmd, logs_customer_id=None, logs_key=None, no_wait=False): - import os + import os, json src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) @@ -1965,13 +1965,16 @@ def containerapp_up(cmd, if not name: if image: - name = image.split('/')[-1].split(':')[0].lower() # .azurecr.io/: + name = image.split('/')[-1].split(':')[0].lower() if source: temp = source[1:] if source[0] == '.' else source # replace first . if it exists name = temp.split('/')[-1].lower() # replace first . if it exists if len(name) == 0: name = _src_path_escaped.split('\\')[-1] + if source and image: + image = image.replace(':', '') + if not location: location = "eastus2" # check user's default location? find least populated server? @@ -1990,7 +1993,6 @@ def containerapp_up(cmd, rg_found = True resource_group_name = containerapp["id"].split('/')[4] except: - # error handle maybe pass containerapp_def = None @@ -1999,7 +2001,30 @@ def containerapp_up(cmd, except: pass - if image is not None and "azurecr.io" in image: + env_name = "" if not managed_env else managed_env.split('/')[8] + if not containerapp_def: + if not resource_group_name: + user = get_profile_username() + rg_name = get_randomized_name(user, resource_group_name) + not dryrun and logger.warning("Creating new resource group {}".format(rg_name)) + not dryrun and create_resource_group(cmd, rg_name, location) + resource_group_name = rg_name + if not managed_env: + env_name = "{}-env".format(name).replace("_","-") + if not dryrun: + logger.warning("Creating new managed environment {}".format(env_name)) + managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] + else: + managed_env = env_name + else: + location = containerapp_def["location"] + managed_env = containerapp_def["properties"]["managedEnvironmentId"] + env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] + if logs_customer_id and logs_key: + if not dryrun: + managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] + + if image is not None and "azurecr.io" in image and not dryrun: if registry_username is None or registry_password is None: # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') @@ -2018,7 +2043,7 @@ def containerapp_up(cmd, registry_name = "" registry_rg = "" if registry_server: - if registry_username is None or registry_password is None: + if not dryrun and (registry_username is None or registry_password is None): if "azurecr.io" in registry_server: # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') @@ -2035,36 +2060,38 @@ def containerapp_up(cmd, user = get_profile_username() registry_name = "{}acr".format(name) registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-","") - logger.warning("Creating new acr {}".format(registry_name)) - registry_def = create_new_acr(cmd, registry_name, registry_rg, location) - registry_server = registry_def.login_server + not dryrun and logger.warning("Creating new acr {}".format(registry_name)) + if not dryrun: + registry_def = create_new_acr(cmd, registry_name, registry_rg, location) + registry_server = registry_def.login_server + else: + registry_server = registry_name + "azurecr.io" image_name = image if image is not None else name + from datetime import datetime + now = datetime.now() + image_name += ":{}".format(str(now).replace(' ', '').replace('-','').replace('.','').replace(':','')) image = registry_server + '/' + image_name - queue_acr_build(cmd, registry_rg, registry_name, image_name, source) - - if not containerapp_def: - if not resource_group_name: - user = get_profile_username() - rg_name = get_randomized_name(user, resource_group_name) - logger.warning("Creating new resource group {}".format(rg_name)) - create_resource_group(cmd, rg_name, location) - resource_group_name = rg_name - if not managed_env: - env_name = "{}-env".format(name).replace("_","-") - logger.warning("Creating new managed environment {}".format(env_name)) - managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] - else: - location = containerapp_def["location"] - managed_env = containerapp_def["properties"]["managedEnvironmentId"] - env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] - if logs_customer_id and logs_key: - managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] - - if source is not None: - _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env, registry_server) - return create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, disable_warnings=True, no_wait=no_wait) - + not dryrun and queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile) + _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) + + dry_run_str = r""" { + "name" : "%s", + "resourcegroup" : "%s", + "location" : "%s", + "environment" : "%s", + "registry": "%s", + "image" : "%s", + "src_path" : "%s" + } + """ % (name, resource_group_name, location, env_name, registry_server, image, _src_path_escaped) + + not dryrun and create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, disable_warnings=True, no_wait=no_wait) + + if dryrun: + logger.warning("Containerapp will be created with the below configuration, re-run command " + "without the --dryrun flag to create & deploy a new containerapp.") + return json.loads(dry_run_str) def get_randomized_name(prefix, name=None, initial="rg"): @@ -2075,7 +2102,7 @@ def get_randomized_name(prefix, name=None, initial="rg"): return default -def _set_webapp_up_default_args(cmd, resource_group_name, location, name, managed_env, registry_server): +def _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server): from azure.cli.core.util import ConfiguredDefaultSetter with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): logger.warning("Setting 'az containerapp up' default arguments for current directory. " @@ -2093,8 +2120,8 @@ def _set_webapp_up_default_args(cmd, resource_group_name, location, name, manage cmd.cli_ctx.config.set_value('defaults', 'name', name) logger.warning("--name/-n default: %s", name) - cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) - logger.warning("--environment default: %s", managed_env) + # cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) + # logger.warning("--environment default: %s", managed_env) cmd.cli_ctx.config.set_value('defaults', 'registry_server', registry_server) logger.warning("--registry-server default: %s", registry_server) @@ -2124,7 +2151,7 @@ def _resource_client_factory(cli_ctx, **_): return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) -def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir): +def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile"): import os, uuid, tempfile from azure.cli.command_modules.acr._archive_utils import upload_source_code from azure.cli.command_modules.acr._stream_utils import stream_logs @@ -2140,7 +2167,7 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir): raise CLIError("Source directory should be a local directory path.") - docker_file_path = os.path.join(src_dir, "Dockerfile") + docker_file_path = os.path.join(src_dir, dockerfile) if not os.path.isfile(docker_file_path): raise CLIError("Unable to find '{}'.".format(docker_file_path)) @@ -2197,6 +2224,8 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir): return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) + # return client_runs.get(registry_rg, registry_name, run_id) # returns only the response object + def _get_acr_cred(cli_ctx, registry_name): from azure.mgmt.containerregistry import ContainerRegistryManagementClient @@ -2235,6 +2264,4 @@ def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku=" zone_redundancy=None, tags=None) # lro_poller = client.begin_create(resource_group_name, registry_name, registry, polling=False) - lro_poller = client._create_initial(resource_group_name, registry_name, registry) - - return lro_poller + return client._create_initial(resource_group_name, registry_name, registry) From 7dae662f92672d723e58ac610502660fb038a5f9 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 1 Apr 2022 16:48:47 -0400 Subject: [PATCH 102/158] Check if rg exists and create one with name if it doesn't. --- src/containerapp/azext_containerapp/custom.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 34214a94dcf..c65cd7aa0a9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1978,7 +1978,17 @@ def containerapp_up(cmd, if not location: location = "eastus2" # check user's default location? find least populated server? - if not resource_group_name: + custom_rg_name = None + # user passes bad resource group name, we create it for them + if resource_group_name: + try: + get_resource_group(cmd, resource_group_name) + except: + custom_rg_name = resource_group_name + resource_group_name = None + + # if custom_rg_name, that means rg doesn't exist no need to look for CA + if not resource_group_name and not custom_rg_name: try: rg_found = False containerapps = list_containerapp(cmd) @@ -2005,7 +2015,7 @@ def containerapp_up(cmd, if not containerapp_def: if not resource_group_name: user = get_profile_username() - rg_name = get_randomized_name(user, resource_group_name) + rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name not dryrun and logger.warning("Creating new resource group {}".format(rg_name)) not dryrun and create_resource_group(cmd, rg_name, location) resource_group_name = rg_name @@ -2065,7 +2075,7 @@ def containerapp_up(cmd, registry_def = create_new_acr(cmd, registry_name, registry_rg, location) registry_server = registry_def.login_server else: - registry_server = registry_name + "azurecr.io" + registry_server = registry_name + ".azurecr.io" image_name = image if image is not None else name from datetime import datetime @@ -2145,6 +2155,13 @@ def create_resource_group(cmd, rg_name, location): return rcf.resource_groups.create_or_update(rg_name, rg_params) +def get_resource_group(cmd, rg_name): + from azure.cli.core.profiles import ResourceType, get_sdk + rcf = _resource_client_factory(cmd.cli_ctx) + resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') + return rcf.resource_groups.get(rg_name) + + def _resource_client_factory(cli_ctx, **_): from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.cli.core.profiles import ResourceType From cddad94d08a7f76dc22314adf4de9d63d041a86c Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Mon, 4 Apr 2022 10:29:40 -0700 Subject: [PATCH 103/158] initial containerapp ssh implementation --- .../azext_containerapp/_clients.py | 34 ++++++ .../azext_containerapp/_constants.py | 21 ++++ .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 102 ++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 src/containerapp/azext_containerapp/_constants.py diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 77cf596c8bf..c9fed310dc3 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -353,6 +353,40 @@ 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() + # TODO support pagination + # TODO expose via a command + @classmethod + def list_replicas(cls, cmd, resource_group_name, container_app_name, revision_name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}/replicas?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + container_app_name, + revision_name, + NEW_API_VERSION) + + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + return r.json() + + + @classmethod + def get_auth_token(cls, cmd, resource_group_name, name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/authtoken?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + name, + NEW_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/_constants.py b/src/containerapp/azext_containerapp/_constants.py new file mode 100644 index 00000000000..f2f52565afe --- /dev/null +++ b/src/containerapp/azext_containerapp/_constants.py @@ -0,0 +1,21 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +# SSH control byte values for container app proxy +SSH_PROXY_FORWARD = 0 +SSH_PROXY_INFO = 1 +SSH_PROXY_ERROR = 2 + +# SSH control byte values for container app cluster +SSH_CLUSTER_STDIN = 0 +SSH_CLUSTER_STDOUT = 1 +SSH_CLUSTER_STDERR = 2 + +# forward byte + stdin byte +SSH_INPUT_PREFIX = b"\x00\x00" + +SSH_DEFAULT_ENCODING = "utf-8" +SSH_BACKUP_ENCODING = "latin_1" diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index c5b924287f8..c162d0c1f24 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -49,6 +49,7 @@ def load_command_table(self, _): 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()) + g.custom_command('ssh', 'containerapp_ssh') 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 15cea81f0ff..ea814016205 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,7 +4,11 @@ # -------------------------------------------------------------------------------------------- # 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 +import websocket +import threading +import sys from urllib.parse import urlparse + from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( RequiredArgumentMissingError, @@ -48,6 +52,9 @@ _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) +from ._constants import (SSH_PROXY_FORWARD, SSH_PROXY_ERROR, SSH_PROXY_INFO, SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR, + SSH_BACKUP_ENCODING, SSH_DEFAULT_ENCODING, SSH_INPUT_PREFIX) + logger = get_logger(__name__) @@ -1932,3 +1939,98 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ return r except Exception as e: handle_raw_exception(e) + + +class Connection: + def __init__(self, is_connected, socket): + self.is_connected = is_connected + self.socket = socket + + def disconnect(self): + logger.warn("Disconnecting socket") + self.is_connected = False + self.socket.close() + + +def _read_ssh(connection, encodings): + while connection.is_connected: + response = connection.socket.recv() + if response is None: # close message ? + print("response was None") + connection.disconnect() + else: + proxy_status = response[0] + if proxy_status == SSH_PROXY_INFO: + print(f"INFO: {response[1:].decode(encodings[0])}") + elif proxy_status == SSH_PROXY_ERROR: + print(f"ERROR: {response[1:].decode(encodings[0])}") + elif proxy_status == SSH_PROXY_FORWARD: + control_byte = response[1] + if control_byte == SSH_CLUSTER_STDOUT or control_byte == SSH_CLUSTER_STDERR: + for i, encoding in enumerate(encodings): + try: + print(response[2:].decode(encoding), end="", flush=True) + break + except UnicodeDecodeError as e: + if i == len(encodings) - 1: # ran out of encodings to try + connection.disconnect() + logger.info("Proxy Control Byte: ", response[0]) + logger.info("Cluster Control Byte: ", response[1]) + logger.info("Hexdump: %s", response[2:].hex()) + raise CLIInternalError("Failed to decode server data") from e + else: + logger.info("Failed to encode with encoding %s", encoding) + else: + connection.disconnect() + raise CLIInternalError("Unexpected message received") + + +# TODO implement timeout if needed +# FYI currently only works against Jeff's app +def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, timeout=None): + app = ContainerAppClient.show(cmd, resource_group_name, name) + if not revision: + revision = app["properties"]["latestRevisionName"] + if not replica: + import requests + requests.get(f'https://{app["properties"]["configuration"]["ingress"]["fqdn"]}') # needed to get an alive replica + replicas = ContainerAppClient.list_replicas(cmd=cmd, + resource_group_name=resource_group_name, + container_app_name=name, + revision_name=revision) + replica = replicas["value"][0]["name"] + if not container: + container = app["properties"]["template"]["containers"][0]["name"] + # TODO validate that this container is in the current replica or make the user specify it -- or pick a container differently + + sub_id = get_subscription_id(cmd.cli_ctx) + command = "bash" + + token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) + token = token_response["properties"]["token"] + logstream_endpoint = token_response["properties"]["logStreamEndpoint"] + + proxy_api_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")].replace("https://", "") + + url = f"wss://{proxy_api_url}/subscriptions/{sub_id}/resourceGroups/{resource_group_name}/containerApps/{name}/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{command}?token={token}" + + # websocket.enableTrace(True) TODO enable on debug maybe + # TODO catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found + from websocket._exceptions import WebSocketBadStatusException + socket = websocket.WebSocket(enable_multithread=True) + encodings = [SSH_DEFAULT_ENCODING, SSH_BACKUP_ENCODING] + + logger.warn("Attempting to connect to %s", url) + socket.connect(url) + + conn = Connection(is_connected=True, socket=socket) + + reader = threading.Thread(target=_read_ssh, args=(conn, encodings)) + reader.daemon = True + reader.start() + + CTRL_C_MSG = b"\x00\x00\x03" # TODO may need to use this + + while conn.is_connected: + ch = sys.stdin.read(1).encode(encodings[0]) # TODO test on windows + socket.send(b"".join([SSH_INPUT_PREFIX, ch])) From 3d9c67ab416e4f9e4509a9512e83f0c0dc0dc63c Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Mon, 4 Apr 2022 15:54:16 -0700 Subject: [PATCH 104/158] fix interactive commands (vim); handle ctrl + c instead of exiting --- .../azext_containerapp/_constants.py | 2 + src/containerapp/azext_containerapp/custom.py | 59 +++++++++++++------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/containerapp/azext_containerapp/_constants.py b/src/containerapp/azext_containerapp/_constants.py index f2f52565afe..bd3c0445898 100644 --- a/src/containerapp/azext_containerapp/_constants.py +++ b/src/containerapp/azext_containerapp/_constants.py @@ -19,3 +19,5 @@ SSH_DEFAULT_ENCODING = "utf-8" SSH_BACKUP_ENCODING = "latin_1" + +SSH_CTRL_C_MSG = b"\x00\x00\x03" diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ea814016205..cca072253f7 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,9 +4,7 @@ # -------------------------------------------------------------------------------------------- # 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 -import websocket -import threading -import sys +import websocket, threading, sys, requests, time, tty from urllib.parse import urlparse from azure.cli.command_modules.appservice.custom import (_get_acr_cred) @@ -18,7 +16,7 @@ CLIInternalError, InvalidArgumentValueError) from azure.cli.core.commands.client_factory import get_subscription_id -from knack.log import get_logger +from knack.log import get_logger, CliLogLevel from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError @@ -53,7 +51,7 @@ _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) from ._constants import (SSH_PROXY_FORWARD, SSH_PROXY_ERROR, SSH_PROXY_INFO, SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR, - SSH_BACKUP_ENCODING, SSH_DEFAULT_ENCODING, SSH_INPUT_PREFIX) + SSH_BACKUP_ENCODING, SSH_DEFAULT_ENCODING, SSH_INPUT_PREFIX, SSH_CTRL_C_MSG) logger = get_logger(__name__) @@ -1941,13 +1939,13 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ handle_raw_exception(e) -class Connection: +class _WebSocketConnection: def __init__(self, is_connected, socket): self.is_connected = is_connected self.socket = socket def disconnect(self): - logger.warn("Disconnecting socket") + logger.warning("Disconnecting...") self.is_connected = False self.socket.close() @@ -1955,8 +1953,7 @@ def disconnect(self): def _read_ssh(connection, encodings): while connection.is_connected: response = connection.socket.recv() - if response is None: # close message ? - print("response was None") + if not response: connection.disconnect() else: proxy_status = response[0] @@ -1985,14 +1982,30 @@ def _read_ssh(connection, encodings): raise CLIInternalError("Unexpected message received") -# TODO implement timeout if needed +def _send_stdin(connection, encoding): + while connection.is_connected: + # TODO this will try and read one more character after the connections is closed + try: + ch = sys.stdin.read(1).encode(encoding) # TODO test on windows + if connection.is_connected: + connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) + except KeyboardInterrupt: + print("Got keyboard interrupt") + + # FYI currently only works against Jeff's app +# TODO get working on windows +# TODO manage terminal size +# TODO implement timeout if needed def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, timeout=None): + if cmd.cli_ctx.logging.log_level == CliLogLevel.DEBUG: + websocket.enableTrace(True) + app = ContainerAppClient.show(cmd, resource_group_name, name) if not revision: revision = app["properties"]["latestRevisionName"] if not replica: - import requests + # VVV this may not be necessary according to Anthony Chu requests.get(f'https://{app["properties"]["configuration"]["ingress"]["fqdn"]}') # needed to get an alive replica replicas = ContainerAppClient.list_replicas(cmd=cmd, resource_group_name=resource_group_name, @@ -2012,25 +2025,33 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No proxy_api_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")].replace("https://", "") - url = f"wss://{proxy_api_url}/subscriptions/{sub_id}/resourceGroups/{resource_group_name}/containerApps/{name}/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{command}?token={token}" + url = (f"wss://{proxy_api_url}/subscriptions/{sub_id}/resourceGroups/{resource_group_name}/containerApps/{name}" + f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{command}?token={token}") - # websocket.enableTrace(True) TODO enable on debug maybe - # TODO catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found - from websocket._exceptions import WebSocketBadStatusException + # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found socket = websocket.WebSocket(enable_multithread=True) encodings = [SSH_DEFAULT_ENCODING, SSH_BACKUP_ENCODING] logger.warn("Attempting to connect to %s", url) socket.connect(url) - conn = Connection(is_connected=True, socket=socket) + conn = _WebSocketConnection(is_connected=True, socket=socket) reader = threading.Thread(target=_read_ssh, args=(conn, encodings)) reader.daemon = True reader.start() - CTRL_C_MSG = b"\x00\x00\x03" # TODO may need to use this + tty.setcbreak(sys.stdin.fileno()) # needed to prevent printing arrow key characters + writer = threading.Thread(target=_send_stdin, args=(conn, SSH_DEFAULT_ENCODING)) + writer.daemon = True + writer.start() + logger.warning("Use ctrl + D to exit.") while conn.is_connected: - ch = sys.stdin.read(1).encode(encodings[0]) # TODO test on windows - socket.send(b"".join([SSH_INPUT_PREFIX, ch])) + try: + time.sleep(0.1) + except KeyboardInterrupt: + if conn.is_connected: + logger.info("Caught KeyboardInterrupt. Sending ctrl+c to server") + socket.send(SSH_CTRL_C_MSG) + From c8130a41b1c0cebc911607df936053441edb5f85 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 5 Apr 2022 09:07:10 -0700 Subject: [PATCH 105/158] fix style and linter issues --- .../azext_containerapp/_clients.py | 1 - src/containerapp/azext_containerapp/_help.py | 12 +++++ .../azext_containerapp/_params.py | 8 ++- .../azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 49 +++++++++---------- 5 files changed, 43 insertions(+), 29 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index c9fed310dc3..c072cc058a2 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -371,7 +371,6 @@ def list_replicas(cls, cmd, resource_group_name, container_app_name, revision_na r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() - @classmethod def get_auth_token(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 5e75c334d82..feb84ae6509 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -86,6 +86,18 @@ az containerapp list -g MyResourceGroup """ +helps['containerapp ssh'] = """ + type: command + short-summary: Open an interactive shell within a container app replica via SSH + examples: + - name: ssh into a container app + text: | + az containerapp ssh -n MyContainerapp -g MyResourceGroup + - name: ssh into a particular container app replica and revision + text: | + az containerapp ssh -n MyContainerapp -g MyResourceGroup +""" + # Revision Commands helps['containerapp revision'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 6e0ee6918d4..f28d9380439 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, option-length-too-long +# pylint: disable=line-too-long, too-many-statements, consider-using-f-string from knack.arguments import CLIArgumentType @@ -30,6 +30,12 @@ def load_arguments(self, _): 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') + with self.argument_context('containerapp ssh') as c: + c.argument('container', help="The name of the container to ssh into") + c.argument('replica', help="The name of the replica to ssh into") + c.argument('revision', help="The name of the container app revision to ssh into") + # c.argument('timeout') + # Container with self.argument_context('containerapp', arg_group='Container') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index c162d0c1f24..b5d44a77cd4 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -49,7 +49,7 @@ def load_command_table(self, _): 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()) - g.custom_command('ssh', 'containerapp_ssh') + g.custom_command('ssh', 'containerapp_ssh', is_preview=True) 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 cca072253f7..efd81db2ced 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,8 +4,13 @@ # -------------------------------------------------------------------------------------------- # 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 -import websocket, threading, sys, requests, time, tty +import threading +import sys +import time +import tty from urllib.parse import urlparse +import websocket +import requests from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( @@ -16,7 +21,7 @@ CLIInternalError, InvalidArgumentValueError) from azure.cli.core.commands.client_factory import get_subscription_id -from knack.log import get_logger, CliLogLevel +from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id from msrest.exceptions import DeserializationError @@ -41,15 +46,14 @@ GitHubActionConfiguration, RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, - SourceControl as SourceControlModel, - ManagedServiceIdentity as ManagedServiceIdentityModel) + 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_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) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _remove_env_vars, _update_revision_env_secretrefs) from ._constants import (SSH_PROXY_FORWARD, SSH_PROXY_ERROR, SSH_PROXY_INFO, SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR, SSH_BACKUP_ENCODING, SSH_DEFAULT_ENCODING, SSH_INPUT_PREFIX, SSH_CTRL_C_MSG) @@ -62,7 +66,8 @@ 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"] + nested_properties = ["provisioningState", "managedEnvironmentId", "latestRevisionName", "latestRevisionFqdn", + "customDomainVerificationId", "configuration", "template", "outboundIPAddresses"] for nested_property in nested_properties: tmp = yaml_containerapp.get(nested_property) if tmp: @@ -90,7 +95,6 @@ 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 sdkClasses = inspect.getmembers(sys.modules["azext_containerapp._sdk_models"]) @@ -1939,6 +1943,7 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ handle_raw_exception(e) +# pylint: disable=too-few-public-methods class _WebSocketConnection: def __init__(self, is_connected, socket): self.is_connected = is_connected @@ -1956,6 +1961,7 @@ def _read_ssh(connection, encodings): if not response: connection.disconnect() else: + logger.info("Received raw response %s", response.hex()) proxy_status = response[0] if proxy_status == SSH_PROXY_INFO: print(f"INFO: {response[1:].decode(encodings[0])}") @@ -1963,7 +1969,7 @@ def _read_ssh(connection, encodings): print(f"ERROR: {response[1:].decode(encodings[0])}") elif proxy_status == SSH_PROXY_FORWARD: control_byte = response[1] - if control_byte == SSH_CLUSTER_STDOUT or control_byte == SSH_CLUSTER_STDERR: + if control_byte in (SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR): for i, encoding in enumerate(encodings): try: print(response[2:].decode(encoding), end="", flush=True) @@ -1971,12 +1977,11 @@ def _read_ssh(connection, encodings): except UnicodeDecodeError as e: if i == len(encodings) - 1: # ran out of encodings to try connection.disconnect() - logger.info("Proxy Control Byte: ", response[0]) - logger.info("Cluster Control Byte: ", response[1]) + logger.info("Proxy Control Byte: %s", response[0]) + logger.info("Cluster Control Byte: %s", response[1]) logger.info("Hexdump: %s", response[2:].hex()) raise CLIInternalError("Failed to decode server data") from e - else: - logger.info("Failed to encode with encoding %s", encoding) + logger.info("Failed to encode with encoding %s", encoding) else: connection.disconnect() raise CLIInternalError("Unexpected message received") @@ -1984,23 +1989,16 @@ def _read_ssh(connection, encodings): def _send_stdin(connection, encoding): while connection.is_connected: - # TODO this will try and read one more character after the connections is closed - try: - ch = sys.stdin.read(1).encode(encoding) # TODO test on windows - if connection.is_connected: - connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) - except KeyboardInterrupt: - print("Got keyboard interrupt") + ch = sys.stdin.read(1).encode(encoding) + if connection.is_connected: + connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) # FYI currently only works against Jeff's app # TODO get working on windows # TODO manage terminal size # TODO implement timeout if needed -def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, timeout=None): - if cmd.cli_ctx.logging.log_level == CliLogLevel.DEBUG: - websocket.enableTrace(True) - +def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None): app = ContainerAppClient.show(cmd, resource_group_name, name) if not revision: revision = app["properties"]["latestRevisionName"] @@ -2017,7 +2015,7 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No # TODO validate that this container is in the current replica or make the user specify it -- or pick a container differently sub_id = get_subscription_id(cmd.cli_ctx) - command = "bash" + command = "bash" # TODO check if there are other shells that can be used token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) token = token_response["properties"]["token"] @@ -2032,7 +2030,7 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No socket = websocket.WebSocket(enable_multithread=True) encodings = [SSH_DEFAULT_ENCODING, SSH_BACKUP_ENCODING] - logger.warn("Attempting to connect to %s", url) + logger.warning("Attempting to connect to %s", url) socket.connect(url) conn = _WebSocketConnection(is_connected=True, socket=socket) @@ -2054,4 +2052,3 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No if conn.is_connected: logger.info("Caught KeyboardInterrupt. Sending ctrl+c to server") socket.send(SSH_CTRL_C_MSG) - From d94a36dd54c01318c696f0486abde4a54fbeaf65 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 5 Apr 2022 15:56:36 -0400 Subject: [PATCH 106/158] Added disable verbose. Moved utils into utils.py. --- src/containerapp/azext_containerapp/_utils.py | 195 +++++++++++++++++- src/containerapp/azext_containerapp/custom.py | 191 +---------------- 2 files changed, 201 insertions(+), 185 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 57e0fd44108..91fb092ad40 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,7 +5,6 @@ # pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument from urllib.parse import urlparse -from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger @@ -585,3 +584,197 @@ def _registry_exists(containerapp_def, registry_server): exists = True break return exists + + +def get_randomized_name(prefix, name=None, initial="rg"): + from random import randint + default = "{}_{}_{:04}".format(prefix, initial, randint(0, 9999)) + if name is not None: + return name + return default + + +def _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server): + from azure.cli.core.util import ConfiguredDefaultSetter + with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): + logger.warning("Setting 'az containerapp up' default arguments for current directory. " + "Manage defaults with 'az configure --scope local'") + + + cmd.cli_ctx.config.set_value('defaults', 'resource_group_name', resource_group_name) + logger.warning("--resource-group/-g default: %s", resource_group_name) + + + cmd.cli_ctx.config.set_value('defaults', 'location', location) + logger.warning("--location/-l default: %s", location) + + + cmd.cli_ctx.config.set_value('defaults', 'name', name) + logger.warning("--name/-n default: %s", name) + + # cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) + # logger.warning("--environment default: %s", managed_env) + + cmd.cli_ctx.config.set_value('defaults', 'registry_server', registry_server) + logger.warning("--registry-server default: %s", registry_server) + + +def get_profile_username(): + from azure.cli.core._profile import Profile + user = Profile().get_current_account_user() + user = user.split('@', 1)[0] + if len(user.split('#', 1)) > 1: # on cloudShell user is in format live.com#user@domain.com + user = user.split('#', 1)[1] + return user + + +def create_resource_group(cmd, rg_name, location): + from azure.cli.core.profiles import ResourceType, get_sdk + rcf = _resource_client_factory(cmd.cli_ctx) + resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') + rg_params = resource_group(location=location) + return rcf.resource_groups.create_or_update(rg_name, rg_params) + + +def get_resource_group(cmd, rg_name): + from azure.cli.core.profiles import ResourceType, get_sdk + rcf = _resource_client_factory(cmd.cli_ctx) + resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') + return rcf.resource_groups.get(rg_name) + + +def _resource_client_factory(cli_ctx, **_): + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.cli.core.profiles import ResourceType + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) + + +def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile", disable_verbose=False): + import os, uuid, tempfile + from azure.cli.command_modules.acr._archive_utils import upload_source_code + from azure.cli.command_modules.acr._stream_utils import stream_logs + from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks + from azure.cli.core.commands import LongRunningOperation + + # client_registries = get_acr_service_client(cmd.cli_ctx).registries + client_registries = cf_acr_registries_tasks(cmd.cli_ctx) + + + if not os.path.isdir(src_dir): + raise CLIError("Source directory should be a local directory path.") + + + docker_file_path = os.path.join(src_dir, dockerfile) + if not os.path.isfile(docker_file_path): + raise CLIError("Unable to find '{}'.".format(docker_file_path)) + + + # NOTE: os.path.basename is unable to parse "\" in the file path + original_docker_file_name = os.path.basename(docker_file_path.replace("\\", "/")) + docker_file_in_tar = '{}_{}'.format(uuid.uuid4().hex, original_docker_file_name) + tar_file_path = os.path.join(tempfile.gettempdir(), 'build_archive_{}.tar.gz'.format(uuid.uuid4().hex)) + + + source_location = upload_source_code(cmd, client_registries, registry_name, registry_rg, src_dir, tar_file_path, docker_file_path, docker_file_in_tar) + + # For local source, the docker file is added separately into tar as the new file name (docker_file_in_tar) + # So we need to update the docker_file_path + docker_file_path = docker_file_in_tar + + + from azure.cli.core.profiles import ResourceType + OS, Architecture = cmd.get_models('OS', 'Architecture', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') + # Default platform values + platform_os = OS.linux.value + platform_arch = Architecture.amd64.value + platform_variant = None + + + DockerBuildRequest, PlatformProperties = cmd.get_models('DockerBuildRequest', 'PlatformProperties', + resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') + docker_build_request = DockerBuildRequest( + image_names=[img_name], + is_push_enabled=True, + source_location=source_location, + platform=PlatformProperties( + os=platform_os, + architecture=platform_arch, + variant=platform_variant + ), + docker_file_path=docker_file_path, + timeout=None, + arguments=[]) + + + queued_build = LongRunningOperation(cmd.cli_ctx)(client_registries.begin_schedule_run( + resource_group_name=registry_rg, + registry_name=registry_name, + run_request=docker_build_request)) + + + run_id = queued_build.run_id + logger.warning("Queued a build with ID: %s", run_id) + not disable_verbose and logger.warning("Waiting for agent...") + + from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) + client_runs = cf_acr_runs(cmd.cli_ctx) + + if disable_verbose: + logger.warning("Waiting for build to finish.") + finished = False + while not finished: + try: + res = client_runs.get(registry_rg, registry_name, run_id) + except: + pass + if res.status != "Running": + if res.status != "Succeeded": + logger.warning("Build failed.") + return res.status + finished = True # doesn't matter can just do while True + logger.warning("Build succeeded.") + return res.status + + return client_runs.get(registry_rg, registry_name, run_id) # returns only the response object + + return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) + + +def _get_acr_cred(cli_ctx, registry_name): + from azure.mgmt.containerregistry import ContainerRegistryManagementClient + from azure.cli.core.commands.parameters import get_resources_in_subscription + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + client = get_mgmt_service_client(cli_ctx, ContainerRegistryManagementClient).registries + + result = get_resources_in_subscription(cli_ctx, 'Microsoft.ContainerRegistry/registries') + result = [item for item in result if item.name.lower() == registry_name] + if not result or len(result) > 1: + raise ResourceNotFoundError("No resource or more than one were found with name '{}'.".format(registry_name)) + resource_group_name = parse_resource_id(result[0].id)['resource_group'] + + registry = client.get(resource_group_name, registry_name) + + if registry.admin_user_enabled: # pylint: disable=no-member + cred = client.list_credentials(resource_group_name, registry_name) + return cred.username, cred.passwords[0].value, resource_group_name + raise ResourceNotFoundError("Failed to retrieve container registry credentials. Please either provide the " + "credentials or run 'az acr update -n {} --admin-enabled true' to enable " + "admin first.".format(registry_name)) + + +def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku="Basic"): + # from azure.cli.command_modules.acr.custom import acr_create + from azure.cli.command_modules.acr._client_factory import cf_acr_registries + from azure.cli.core.profiles import ResourceType + + + client = cf_acr_registries(cmd.cli_ctx) + # return acr_create(cmd, client, registry_name, resource_group_name, sku, location) + + Registry, Sku, NetworkRuleSet = cmd.get_models('Registry', 'Sku', 'NetworkRuleSet', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group="registries") + registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=True, + zone_redundancy=None, tags=None) + + # lro_poller = client.begin_create(resource_group_name, registry_name, registry, polling=False) + return client._create_initial(resource_group_name, registry_name, registry) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c65cd7aa0a9..edb8d0ce0ee 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -47,7 +47,9 @@ _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, _registry_exists, _remove_env_vars, + _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, + get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr) logger = get_logger(__name__) @@ -1955,6 +1957,7 @@ def containerapp_up(cmd, dryrun=False, logs_customer_id=None, logs_key=None, + disable_verbose=False, no_wait=False): import os, json src_dir = os.getcwd() @@ -2011,7 +2014,7 @@ def containerapp_up(cmd, except: pass - env_name = "" if not managed_env else managed_env.split('/')[8] + env_name = "" if not managed_env else managed_env.split('/')[6] if not containerapp_def: if not resource_group_name: user = get_profile_username() @@ -2082,9 +2085,9 @@ def containerapp_up(cmd, now = datetime.now() image_name += ":{}".format(str(now).replace(' ', '').replace('-','').replace('.','').replace(':','')) image = registry_server + '/' + image_name - not dryrun and queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile) + not dryrun and queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, disable_verbose) _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) - + dry_run_str = r""" { "name" : "%s", "resourcegroup" : "%s", @@ -2102,183 +2105,3 @@ def containerapp_up(cmd, logger.warning("Containerapp will be created with the below configuration, re-run command " "without the --dryrun flag to create & deploy a new containerapp.") return json.loads(dry_run_str) - - -def get_randomized_name(prefix, name=None, initial="rg"): - from random import randint - default = "{}_{}_{:04}".format(prefix, initial, randint(0, 9999)) - if name is not None: - return name - return default - - -def _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server): - from azure.cli.core.util import ConfiguredDefaultSetter - with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): - logger.warning("Setting 'az containerapp up' default arguments for current directory. " - "Manage defaults with 'az configure --scope local'") - - - cmd.cli_ctx.config.set_value('defaults', 'resource_group_name', resource_group_name) - logger.warning("--resource-group/-g default: %s", resource_group_name) - - - cmd.cli_ctx.config.set_value('defaults', 'location', location) - logger.warning("--location/-l default: %s", location) - - - cmd.cli_ctx.config.set_value('defaults', 'name', name) - logger.warning("--name/-n default: %s", name) - - # cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) - # logger.warning("--environment default: %s", managed_env) - - cmd.cli_ctx.config.set_value('defaults', 'registry_server', registry_server) - logger.warning("--registry-server default: %s", registry_server) - - - -def get_profile_username(): - from azure.cli.core._profile import Profile - user = Profile().get_current_account_user() - user = user.split('@', 1)[0] - if len(user.split('#', 1)) > 1: # on cloudShell user is in format live.com#user@domain.com - user = user.split('#', 1)[1] - return user - - -def create_resource_group(cmd, rg_name, location): - from azure.cli.core.profiles import ResourceType, get_sdk - rcf = _resource_client_factory(cmd.cli_ctx) - resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') - rg_params = resource_group(location=location) - return rcf.resource_groups.create_or_update(rg_name, rg_params) - - -def get_resource_group(cmd, rg_name): - from azure.cli.core.profiles import ResourceType, get_sdk - rcf = _resource_client_factory(cmd.cli_ctx) - resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') - return rcf.resource_groups.get(rg_name) - - -def _resource_client_factory(cli_ctx, **_): - from azure.cli.core.commands.client_factory import get_mgmt_service_client - from azure.cli.core.profiles import ResourceType - return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) - - -def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile"): - import os, uuid, tempfile - from azure.cli.command_modules.acr._archive_utils import upload_source_code - from azure.cli.command_modules.acr._stream_utils import stream_logs - from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks - - from azure.cli.core.commands import LongRunningOperation - - # client_registries = get_acr_service_client(cmd.cli_ctx).registries - client_registries = cf_acr_registries_tasks(cmd.cli_ctx) - - - if not os.path.isdir(src_dir): - raise CLIError("Source directory should be a local directory path.") - - - docker_file_path = os.path.join(src_dir, dockerfile) - if not os.path.isfile(docker_file_path): - raise CLIError("Unable to find '{}'.".format(docker_file_path)) - - - # NOTE: os.path.basename is unable to parse "\" in the file path - original_docker_file_name = os.path.basename(docker_file_path.replace("\\", "/")) - docker_file_in_tar = '{}_{}'.format(uuid.uuid4().hex, original_docker_file_name) - tar_file_path = os.path.join(tempfile.gettempdir(), 'build_archive_{}.tar.gz'.format(uuid.uuid4().hex)) - - - source_location = upload_source_code(cmd, client_registries, registry_name, registry_rg, src_dir, tar_file_path, docker_file_path, docker_file_in_tar) - - # For local source, the docker file is added separately into tar as the new file name (docker_file_in_tar) - # So we need to update the docker_file_path - docker_file_path = docker_file_in_tar - - - from azure.cli.core.profiles import ResourceType - OS, Architecture = cmd.get_models('OS', 'Architecture', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') - # Default platform values - platform_os = OS.linux.value - platform_arch = Architecture.amd64.value - platform_variant = None - - - DockerBuildRequest, PlatformProperties = cmd.get_models('DockerBuildRequest', 'PlatformProperties', - resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') - docker_build_request = DockerBuildRequest( - image_names=[img_name], - is_push_enabled=True, - source_location=source_location, - platform=PlatformProperties( - os=platform_os, - architecture=platform_arch, - variant=platform_variant - ), - docker_file_path=docker_file_path, - timeout=None, - arguments=[]) - - - queued_build = LongRunningOperation(cmd.cli_ctx)(client_registries.begin_schedule_run( - resource_group_name=registry_rg, - registry_name=registry_name, - run_request=docker_build_request)) - - - run_id = queued_build.run_id - logger.warning("Queued a build with ID: %s", run_id) - logger.warning("Waiting for agent...") - - from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) - client_runs = cf_acr_runs(cmd.cli_ctx) - - return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) - - # return client_runs.get(registry_rg, registry_name, run_id) # returns only the response object - - -def _get_acr_cred(cli_ctx, registry_name): - from azure.mgmt.containerregistry import ContainerRegistryManagementClient - from azure.cli.core.commands.parameters import get_resources_in_subscription - from azure.cli.core.commands.client_factory import get_mgmt_service_client - - client = get_mgmt_service_client(cli_ctx, ContainerRegistryManagementClient).registries - - result = get_resources_in_subscription(cli_ctx, 'Microsoft.ContainerRegistry/registries') - result = [item for item in result if item.name.lower() == registry_name] - if not result or len(result) > 1: - raise ResourceNotFoundError("No resource or more than one were found with name '{}'.".format(registry_name)) - resource_group_name = parse_resource_id(result[0].id)['resource_group'] - - registry = client.get(resource_group_name, registry_name) - - if registry.admin_user_enabled: # pylint: disable=no-member - cred = client.list_credentials(resource_group_name, registry_name) - return cred.username, cred.passwords[0].value, resource_group_name - raise ResourceNotFoundError("Failed to retrieve container registry credentials. Please either provide the " - "credentials or run 'az acr update -n {} --admin-enabled true' to enable " - "admin first.".format(registry_name)) - - -def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku="Basic"): - # from azure.cli.command_modules.acr.custom import acr_create - from azure.cli.command_modules.acr._client_factory import cf_acr_registries - from azure.cli.core.profiles import ResourceType - - - client = cf_acr_registries(cmd.cli_ctx) - # return acr_create(cmd, client, registry_name, resource_group_name, sku, location) - - Registry, Sku, NetworkRuleSet = cmd.get_models('Registry', 'Sku', 'NetworkRuleSet', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group="registries") - registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=True, - zone_redundancy=None, tags=None) - - # lro_poller = client.begin_create(resource_group_name, registry_name, registry, polling=False) - return client._create_initial(resource_group_name, registry_name, registry) From 581d629027e64587c7b12a0e0155eb030c45b108 Mon Sep 17 00:00:00 2001 From: StrawnSC Date: Tue, 5 Apr 2022 13:14:53 -0700 Subject: [PATCH 107/158] fix for ssh for windows clients --- src/containerapp/azext_containerapp/custom.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index efd81db2ced..23c0d0d80f5 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -6,13 +6,16 @@ import threading import sys +import platform import time -import tty from urllib.parse import urlparse +import msvcrt import websocket import requests + from azure.cli.command_modules.appservice.custom import (_get_acr_cred) +from azure.cli.command_modules.container._vt_helper import enable_vt_mode from azure.cli.core.azclierror import ( RequiredArgumentMissingError, ValidationError, @@ -1987,15 +1990,24 @@ def _read_ssh(connection, encodings): raise CLIInternalError("Unexpected message received") -def _send_stdin(connection, encoding): +def _send_stdin(connection, getch_fn): while connection.is_connected: - ch = sys.stdin.read(1).encode(encoding) + ch = getch_fn() if connection.is_connected: connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) +def _getch_unix(): + return sys.stdin.read(1).encode(SSH_DEFAULT_ENCODING) + + +def _getch_windows(): + while not msvcrt.kbhit(): + time.sleep(0.01) + return msvcrt.getch() + + # FYI currently only works against Jeff's app -# TODO get working on windows # TODO manage terminal size # TODO implement timeout if needed def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None): @@ -2039,8 +2051,14 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No reader.daemon = True reader.start() - tty.setcbreak(sys.stdin.fileno()) # needed to prevent printing arrow key characters - writer = threading.Thread(target=_send_stdin, args=(conn, SSH_DEFAULT_ENCODING)) + if platform.system() != "Windows": + import tty + tty.setcbreak(sys.stdin.fileno()) # needed to prevent printing arrow key characters + writer = threading.Thread(target=_send_stdin, args=(conn, _getch_unix)) + else: + enable_vt_mode() # needed for interactive commands (ie vim) + writer = threading.Thread(target=_send_stdin, args=(conn, _getch_windows)) + writer.daemon = True writer.start() From 2ca69cdbf8255faf763383dfb83a676b02c1f046 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 5 Apr 2022 13:39:48 -0700 Subject: [PATCH 108/158] fix for unix --- src/containerapp/azext_containerapp/custom.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 23c0d0d80f5..c0b8463bef9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -9,13 +9,11 @@ import platform import time from urllib.parse import urlparse -import msvcrt import websocket import requests from azure.cli.command_modules.appservice.custom import (_get_acr_cred) -from azure.cli.command_modules.container._vt_helper import enable_vt_mode from azure.cli.core.azclierror import ( RequiredArgumentMissingError, ValidationError, @@ -60,6 +58,9 @@ from ._constants import (SSH_PROXY_FORWARD, SSH_PROXY_ERROR, SSH_PROXY_INFO, SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR, SSH_BACKUP_ENCODING, SSH_DEFAULT_ENCODING, SSH_INPUT_PREFIX, SSH_CTRL_C_MSG) +if platform.system() == "Windows": + import msvcrt # pylint: disable=import-error + from azure.cli.command_modules.container._vt_helper import enable_vt_mode # pylint: disable=ungrouped-imports logger = get_logger(__name__) From dadc70261e3f401297207062a30c79359fa209fa Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 5 Apr 2022 17:08:53 -0400 Subject: [PATCH 109/158] Disable verbose now uses sdk poller so it gives running animation. --- .../azext_containerapp/_acr_run_polling.py | 111 ++++++++++++++++++ .../azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 42 +++---- 3 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_acr_run_polling.py diff --git a/src/containerapp/azext_containerapp/_acr_run_polling.py b/src/containerapp/azext_containerapp/_acr_run_polling.py new file mode 100644 index 00000000000..1ea21228617 --- /dev/null +++ b/src/containerapp/azext_containerapp/_acr_run_polling.py @@ -0,0 +1,111 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import time + +from azure.core.polling import PollingMethod, LROPoller +from msrest import Deserializer +from msrestazure.azure_exceptions import CloudError +from azure.cli.core.profiles import ResourceType +from azure.cli.command_modules.acr._constants import get_acr_task_models + + +def get_run_with_polling(cmd, + client, + run_id, + registry_name, + resource_group_name): + deserializer = Deserializer( + {k: v for k, v in get_acr_task_models(cmd).__dict__.items() if isinstance(v, type)}) + + def deserialize_run(response): + return deserializer('Run', response) + + return LROPoller( + client=client, + initial_response=client.get( + resource_group_name, registry_name, run_id, cls=lambda x, y, z: x), + deserialization_callback=deserialize_run, + polling_method=RunPolling( + cmd=cmd, + registry_name=registry_name, + run_id=run_id + )) + + +class RunPolling(PollingMethod): # pylint: disable=too-many-instance-attributes + + def __init__(self, cmd, registry_name, run_id, timeout=30): + self._cmd = cmd + self._registry_name = registry_name + self._run_id = run_id + self._timeout = timeout + self._client = None + self._response = None # Will hold latest received response + self._url = None # The URL used to get the run + self._deserialize = None # The deserializer for Run + self.operation_status = "" + self.operation_result = None + + def initialize(self, client, initial_response, deserialization_callback): + self._client = client._client # pylint: disable=protected-access + self._response = initial_response + self._url = initial_response.http_request.url + self._deserialize = deserialization_callback + + self._set_operation_status(initial_response) + + def run(self): + while not self.finished(): + time.sleep(self._timeout) + self._update_status() + + if self.operation_status not in get_succeeded_run_status(self._cmd): + from knack.util import CLIError + raise CLIError("The run with ID '{}' finished with unsuccessful status '{}'. " + "Show run details by 'az acr task show-run -r {} --run-id {}'. " + "Show run logs by 'az acr task logs -r {} --run-id {}'.".format( + self._run_id, + self.operation_status, + self._registry_name, + self._run_id, + self._registry_name, + self._run_id + )) + + def status(self): + return self.operation_status + + def finished(self): + return self.operation_status in get_finished_run_status(self._cmd) + + def resource(self): + return self.operation_result + + def _set_operation_status(self, response): + if response.http_response.status_code == 200: + self.operation_result = self._deserialize(response) + self.operation_status = self.operation_result.status + return + raise CloudError(response) + + def _update_status(self): + self._response = self._client._pipeline.run( # pylint: disable=protected-access + self._client.get(self._url), stream=False) + self._set_operation_status(self._response) + + +def get_succeeded_run_status(cmd): + RunStatus = cmd.get_models('RunStatus', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='task_runs') + return [RunStatus.succeeded.value] + + +def get_finished_run_status(cmd): + RunStatus = cmd.get_models('RunStatus', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='task_runs') + return [RunStatus.succeeded.value, + RunStatus.failed.value, + RunStatus.canceled.value, + RunStatus.error.value, + RunStatus.timeout.value] diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 24fece9d6a6..26d7638228a 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -182,4 +182,4 @@ def load_arguments(self, _): c.argument('location', configured_default='location') c.argument('name', configured_default='name') c.argument('managed_env', configured_default='managed_env') - c.argument('registry_server', configured_default='registry_server') \ No newline at end of file + c.argument('registry_server', configured_default='registry_server') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 91fb092ad40..c65584b0527 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -637,9 +637,7 @@ def create_resource_group(cmd, rg_name, location): def get_resource_group(cmd, rg_name): - from azure.cli.core.profiles import ResourceType, get_sdk rcf = _resource_client_factory(cmd.cli_ctx) - resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') return rcf.resource_groups.get(rg_name) @@ -650,7 +648,9 @@ def _resource_client_factory(cli_ctx, **_): def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile", disable_verbose=False): - import os, uuid, tempfile + import os + import uuid + import tempfile from azure.cli.command_modules.acr._archive_utils import upload_source_code from azure.cli.command_modules.acr._stream_utils import stream_logs from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks @@ -661,12 +661,12 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi if not os.path.isdir(src_dir): - raise CLIError("Source directory should be a local directory path.") + raise ValidationError("Source directory should be a local directory path.") docker_file_path = os.path.join(src_dir, dockerfile) if not os.path.isfile(docker_file_path): - raise CLIError("Unable to find '{}'.".format(docker_file_path)) + raise ValidationError("Unable to find '{}'.".format(docker_file_path)) # NOTE: os.path.basename is unable to parse "\" in the file path @@ -717,25 +717,14 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi not disable_verbose and logger.warning("Waiting for agent...") from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) + from ._acr_run_polling import get_run_with_polling client_runs = cf_acr_runs(cmd.cli_ctx) if disable_verbose: - logger.warning("Waiting for build to finish.") - finished = False - while not finished: - try: - res = client_runs.get(registry_rg, registry_name, run_id) - except: - pass - if res.status != "Running": - if res.status != "Succeeded": - logger.warning("Build failed.") - return res.status - finished = True # doesn't matter can just do while True - logger.warning("Build succeeded.") - return res.status - - return client_runs.get(registry_rg, registry_name, run_id) # returns only the response object + lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) + acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) + logger.warning("Build {}.".format(acr.status.lower())) + return acr return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) @@ -767,14 +756,15 @@ def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku=" # from azure.cli.command_modules.acr.custom import acr_create from azure.cli.command_modules.acr._client_factory import cf_acr_registries from azure.cli.core.profiles import ResourceType - + from azure.cli.core.commands import LongRunningOperation client = cf_acr_registries(cmd.cli_ctx) # return acr_create(cmd, client, registry_name, resource_group_name, sku, location) - - Registry, Sku, NetworkRuleSet = cmd.get_models('Registry', 'Sku', 'NetworkRuleSet', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group="registries") + + Registry, Sku = cmd.get_models('Registry', 'Sku', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group="registries") registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=True, zone_redundancy=None, tags=None) - # lro_poller = client.begin_create(resource_group_name, registry_name, registry, polling=False) - return client._create_initial(resource_group_name, registry_name, registry) + lro_poller = client.begin_create(resource_group_name, registry_name, registry) + acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) + return acr From a7c880a4158b125145506d4a6388e7e1e85a3f64 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 6 Apr 2022 15:24:55 -0400 Subject: [PATCH 110/158] Added helps for params. Added error handling for non acr registry passed with source. Ignore param disable_warnings for create and no_wait for up. --- .../azext_containerapp/_params.py | 14 ++++- src/containerapp/azext_containerapp/_utils.py | 16 +++-- src/containerapp/azext_containerapp/custom.py | 58 +++++++++---------- 3 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 26d7638228a..81fe6d0d39a 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -76,6 +76,7 @@ def load_arguments(self, _): with self.argument_context('containerapp create') as c: c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") + c.ignore('disable_warnings') with self.argument_context('containerapp scale') as c: c.argument('min_replicas', type=int, help="The minimum number of replicas.") @@ -180,6 +181,17 @@ def load_arguments(self, _): with self.argument_context('containerapp up') as c: c.argument('resource_group_name', configured_default='resource_group_name') c.argument('location', configured_default='location') - c.argument('name', configured_default='name') + c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') c.argument('registry_server', configured_default='registry_server') + c.argument('disable_verbose', help="Disable verbose output from ACR build when using --source.") + c.argument('dockerfile', help="Name of the dockerfile.") + c.argument('dryrun', help="Show summary of the operation instead of executing it.") + + with self.argument_context('containerapp up', arg_group='Log Analytics (Environment)') as c: + c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Name or resource ID of the Log Analytics workspace to send diagnostics logs to. You can use \"az monitor log-analytics workspace create\" to create one. Extra billing may apply.') + c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log Analytics workspace key to configure your Log Analytics workspace. You can use \"az monitor log-analytics workspace get-shared-keys\" to retrieve the key.') + c.ignore('no_wait') + + with self.argument_context('containerapp', arg_group='Container') as c: + c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index c65584b0527..a04ebce9f4d 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -5,7 +5,7 @@ # pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument from urllib.parse import urlparse -from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from msrestazure.tools import parse_resource_id @@ -131,7 +131,7 @@ def _update_revision_env_secretrefs(containers, name): 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): +def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass, update_existing_secret=False, disable_warnings=False): if registry_pass.startswith("secretref:"): # If user passed in registry password using a secret @@ -161,7 +161,8 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ 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 + if not disable_warnings: + 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 @@ -233,7 +234,7 @@ def _generate_log_analytics_workspace_name(resource_group_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.") + logger.warning("No Log Analytics workspace provided.") try: _validate_subscription_registered(cmd, "Microsoft.OperationalInsights") log_analytics_client = log_analytics_client_factory(cmd.cli_ctx) @@ -250,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)) # pylint: disable=logging-format-interpolation + 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) @@ -288,7 +289,7 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc logs_key = shared_keys.primary_shared_key - return logs_customer_id, logs_key + return logs_customer_id, logs_key, workspace_name def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): @@ -565,6 +566,7 @@ def _infer_acr_credentials(cmd, registry_server, disable_warnings=False): # If registry is Azure Container Registry, we can try inferring credentials if '.azurecr.io' not in registry_server: raise RequiredArgumentMissingError('Registry username and password are required if not using Azure Container Registry.') + logger.warning("Infer acr credentials") not disable_warnings and 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] @@ -724,6 +726,8 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) logger.warning("Build {}.".format(acr.status.lower())) + if acr.status.lower() != "succeeded": + raise CLIInternalError("ACR build {}.".format(acr.status.lower())) return acr return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index edb8d0ce0ee..ddb501951e9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -360,7 +360,7 @@ def create_containerapp(cmd, if secrets_def is None: secrets_def = [] - registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass, disable_warnings=disable_warnings) dapr_def = None if dapr_enabled: @@ -1950,15 +1950,14 @@ def containerapp_up(cmd, dockerfile="Dockerfile", # compose=None, ingress=None, - port=None, - registry_username=None, - registry_password=None, + target_port=None, + registry_user=None, + registry_pass=None, env_vars=None, dryrun=False, logs_customer_id=None, logs_key=None, - disable_verbose=False, - no_wait=False): + disable_verbose=False): import os, json src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) @@ -2019,14 +2018,15 @@ def containerapp_up(cmd, if not resource_group_name: user = get_profile_username() rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name - not dryrun and logger.warning("Creating new resource group {}".format(rg_name)) - not dryrun and create_resource_group(cmd, rg_name, location) + if not dryrun: + logger.warning("Creating new resource group {}".format(rg_name)) + create_resource_group(cmd, rg_name, location) resource_group_name = rg_name if not managed_env: env_name = "{}-env".format(name).replace("_","-") if not dryrun: logger.warning("Creating new managed environment {}".format(env_name)) - managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] + managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] else: managed_env = env_name else: @@ -2035,17 +2035,17 @@ def containerapp_up(cmd, env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] if logs_customer_id and logs_key: if not dryrun: - managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True, no_wait=no_wait)["id"] + managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] if image is not None and "azurecr.io" in image and not dryrun: - if registry_username is None or registry_password is None: + if registry_user is None or registry_pass is None: # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') registry_server=image.split('/')[0] parsed = urlparse(image) 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) + registry_user, registry_pass = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex @@ -2056,25 +2056,24 @@ def containerapp_up(cmd, registry_name = "" registry_rg = "" if registry_server: - if not dryrun and (registry_username is None or registry_password is None): - if "azurecr.io" in registry_server: - # If registry is Azure Container Registry, we can try inferring credentials - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') - parsed = urlparse(registry_server) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: - registry_username, registry_password, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) - except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex - else: - raise RequiredArgumentMissingError("Registry usename and password are required if using non-Azure registry.") + if "azurecr.io" not in registry_server: + raise ValidationError("Cannot supply non-Azure registry when using --source.") + elif not dryrun and (registry_user is None or registry_pass is None): + # If registry is Azure Container Registry, we can try inferring credentials + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex else: registry_rg = resource_group_name user = get_profile_username() registry_name = "{}acr".format(name) registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-","") - not dryrun and logger.warning("Creating new acr {}".format(registry_name)) if not dryrun: + logger.warning("Creating new acr {}".format(registry_name)) registry_def = create_new_acr(cmd, registry_name, registry_rg, location) registry_server = registry_def.login_server else: @@ -2085,7 +2084,8 @@ def containerapp_up(cmd, now = datetime.now() image_name += ":{}".format(str(now).replace(' ', '').replace('-','').replace('.','').replace(':','')) image = registry_server + '/' + image_name - not dryrun and queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, disable_verbose) + if not dryrun: + queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, disable_verbose) _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) dry_run_str = r""" { @@ -2098,10 +2098,10 @@ def containerapp_up(cmd, "src_path" : "%s" } """ % (name, resource_group_name, location, env_name, registry_server, image, _src_path_escaped) - - not dryrun and create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=port, registry_server=registry_server, registry_pass=registry_password, registry_user=registry_username, env_vars=env_vars, ingress=ingress, disable_warnings=True, no_wait=no_wait) - if dryrun: logger.warning("Containerapp will be created with the below configuration, re-run command " "without the --dryrun flag to create & deploy a new containerapp.") + else: + create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) + return json.loads(dry_run_str) From 9eea137ae0d9783659907c9d3083e48305d354f5 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 6 Apr 2022 15:28:08 -0400 Subject: [PATCH 111/158] Updated disable_warnings ignore. Removed disable_warnings from update_containerapp. --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/custom.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 81fe6d0d39a..d85f8c42cf6 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -24,6 +24,7 @@ def load_arguments(self, _): 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)) + c.ignore('disable_warnings') with self.argument_context('containerapp') as c: c.argument('tags', arg_type=tags_type) @@ -76,7 +77,6 @@ def load_arguments(self, _): with self.argument_context('containerapp create') as c: c.argument('traffic_weights', nargs='*', options_list=['--traffic-weight'], help="A list of revision weight(s) for the container app. Space-separated values in 'revision_name=weight' format. For latest revision, use 'latest=weight'") - c.ignore('disable_warnings') with self.argument_context('containerapp scale') as c: c.argument('min_replicas', type=int, help="The minimum number of replicas.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ddb501951e9..f444f4a5e58 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -450,7 +450,6 @@ def update_containerapp(cmd, startup_command=None, args=None, tags=None, - disable_warnings=False, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") @@ -610,7 +609,7 @@ def update_containerapp(cmd, 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: - not disable_warnings and logger.warning('Containerapp update in progress. Please monitor the update using `az containerapp show -n {} -g {}`'.format(name, resource_group_name)) + 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: From 5bc365d8ff178eadef2290b99dcb3f3f99362e44 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 6 Apr 2022 14:00:57 -0700 Subject: [PATCH 112/158] add terminal resizing --- src/containerapp/azext_containerapp/custom.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c0b8463bef9..2da7e3b9c10 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- # 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 +import os import threading import sys import platform @@ -12,7 +13,6 @@ import websocket import requests - from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( RequiredArgumentMissingError, @@ -1993,11 +1993,21 @@ def _read_ssh(connection, encodings): def _send_stdin(connection, getch_fn): while connection.is_connected: + _resize_terminal(connection) ch = getch_fn() + _resize_terminal(connection) if connection.is_connected: connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) +def _resize_terminal(connection): + size = os.get_terminal_size() + if connection.is_connected: + connection.socket.send(b"".join([b"\x00\x04", + f'{{"Width": {size.columns}, ' + f'"Height": {size.lines}}}'.encode(SSH_DEFAULT_ENCODING)])) + + def _getch_unix(): return sys.stdin.read(1).encode(SSH_DEFAULT_ENCODING) @@ -2011,6 +2021,8 @@ def _getch_windows(): # FYI currently only works against Jeff's app # TODO manage terminal size # TODO implement timeout if needed +# TODO token will be read from header at some point +# TODO validate argument values (+ defaults) def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None): app = ContainerAppClient.show(cmd, resource_group_name, name) if not revision: @@ -2022,7 +2034,7 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No resource_group_name=resource_group_name, container_app_name=name, revision_name=revision) - replica = replicas["value"][0]["name"] + replica = replicas["value"][0]["name"] # TODO validate that a replica exists if not container: container = app["properties"]["template"]["containers"][0]["name"] # TODO validate that this container is in the current replica or make the user specify it -- or pick a container differently From d0271f7224fa2cf763e53fc7587f5975f310e960 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 6 Apr 2022 16:11:07 -0700 Subject: [PATCH 113/158] reorganize code; implement terminal resizing, add startup command param, etc --- .../azext_containerapp/_constants.py | 23 --- .../azext_containerapp/_params.py | 6 +- .../azext_containerapp/_ssh_utils.py | 137 ++++++++++++++++++ src/containerapp/azext_containerapp/custom.py | 107 ++------------ 4 files changed, 150 insertions(+), 123 deletions(-) delete mode 100644 src/containerapp/azext_containerapp/_constants.py create mode 100644 src/containerapp/azext_containerapp/_ssh_utils.py diff --git a/src/containerapp/azext_containerapp/_constants.py b/src/containerapp/azext_containerapp/_constants.py deleted file mode 100644 index bd3c0445898..00000000000 --- a/src/containerapp/azext_containerapp/_constants.py +++ /dev/null @@ -1,23 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - - -# SSH control byte values for container app proxy -SSH_PROXY_FORWARD = 0 -SSH_PROXY_INFO = 1 -SSH_PROXY_ERROR = 2 - -# SSH control byte values for container app cluster -SSH_CLUSTER_STDIN = 0 -SSH_CLUSTER_STDOUT = 1 -SSH_CLUSTER_STDERR = 2 - -# forward byte + stdin byte -SSH_INPUT_PREFIX = b"\x00\x00" - -SSH_DEFAULT_ENCODING = "utf-8" -SSH_BACKUP_ENCODING = "latin_1" - -SSH_CTRL_C_MSG = b"\x00\x00\x03" diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index f28d9380439..8933396a68e 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -32,9 +32,11 @@ def load_arguments(self, _): with self.argument_context('containerapp ssh') as c: c.argument('container', help="The name of the container to ssh into") - c.argument('replica', help="The name of the replica to ssh into") + c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") c.argument('revision', help="The name of the container app revision to ssh into") - # c.argument('timeout') + c.argument('startup_command', options_list=["--command"], default="sh", help="The startup command (bash, zsh, sh, etc.).") + c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") + c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) # Container with self.argument_context('containerapp', arg_group='Container') as c: diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py new file mode 100644 index 00000000000..43f5c077d44 --- /dev/null +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -0,0 +1,137 @@ +# -------------------------------------------------------------------------------------------- +# 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 sys +import time +import platform +import threading +import requests + +from knack.log import get_logger + +from azure.cli.core.azclierror import CLIInternalError + +if platform.system() == "Windows": + import msvcrt # pylint: disable=import-error + from azure.cli.command_modules.container._vt_helper import enable_vt_mode # pylint: disable=ungrouped-imports + +logger = get_logger(__name__) + +# SSH control byte values for container app proxy +SSH_PROXY_FORWARD = 0 +SSH_PROXY_INFO = 1 +SSH_PROXY_ERROR = 2 + +# SSH control byte values for container app cluster +SSH_CLUSTER_STDIN = 0 +SSH_CLUSTER_STDOUT = 1 +SSH_CLUSTER_STDERR = 2 + +# forward byte + stdin byte +SSH_INPUT_PREFIX = b"\x00\x00" + +SSH_DEFAULT_ENCODING = "utf-8" +SSH_BACKUP_ENCODING = "latin_1" + +SSH_CTRL_C_MSG = b"\x00\x00\x03" + + +# pylint: disable=too-few-public-methods +class WebSocketConnection: + def __init__(self, is_connected, socket): + self.is_connected = is_connected + self.socket = socket + + def disconnect(self): + logger.warning("Disconnecting...") + self.is_connected = False + self.socket.close() + + +def _decode_and_output_to_terminal(connection: WebSocketConnection, response, encodings): + for i, encoding in enumerate(encodings): + try: + print(response[2:].decode(encoding), end="", flush=True) + break + except UnicodeDecodeError as e: + if i == len(encodings) - 1: # ran out of encodings to try + connection.disconnect() + logger.info("Proxy Control Byte: %s", response[0]) + logger.info("Cluster Control Byte: %s", response[1]) + logger.info("Hexdump: %s", response[2:].hex()) + raise CLIInternalError("Failed to decode server data") from e + logger.info("Failed to encode with encoding %s", encoding) + + +def read_ssh(connection: WebSocketConnection, response_encodings): + # response_encodings is the ordered list of Unicode encodings to try to decode with before raising an exception + while connection.is_connected: + response = connection.socket.recv() + if not response: + connection.disconnect() + else: + logger.info("Received raw response %s", response.hex()) + proxy_status = response[0] + if proxy_status == SSH_PROXY_INFO: + print(f"INFO: {response[1:].decode(SSH_DEFAULT_ENCODING)}") + elif proxy_status == SSH_PROXY_ERROR: + print(f"ERROR: {response[1:].decode(SSH_DEFAULT_ENCODING)}") + elif proxy_status == SSH_PROXY_FORWARD: + control_byte = response[1] + if control_byte in (SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR): + _decode_and_output_to_terminal(connection, response, response_encodings) + else: + connection.disconnect() + raise CLIInternalError("Unexpected message received") + + +def _send_stdin(connection: WebSocketConnection, getch_fn): + while connection.is_connected: + _resize_terminal(connection) + ch = getch_fn() + _resize_terminal(connection) + if connection.is_connected: + connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) + + +def _resize_terminal(connection: WebSocketConnection): + size = os.get_terminal_size() + if connection.is_connected: + connection.socket.send(b"".join([b"\x00\x04", + f'{{"Width": {size.columns}, ' + f'"Height": {size.lines}}}'.encode(SSH_DEFAULT_ENCODING)])) + + +def _getch_unix(): + return sys.stdin.read(1).encode(SSH_DEFAULT_ENCODING) + + +def _getch_windows(): + while not msvcrt.kbhit(): + time.sleep(0.01) + return msvcrt.getch() + + +def ping_container_app(app): + site = app.get("properties", {}).get("configuration", {}).get("ingress", {}).get("fqdn") + if site: + resp = requests.get(f'https://{site}') + if not resp.ok: + logger.info("Got bad status pinging app: {resp.status_code}") + else: + logger.info("Could not fetch site external URL") + + +def get_stdin_writer(connection: WebSocketConnection): + if platform.system() != "Windows": + import tty + tty.setcbreak(sys.stdin.fileno()) # needed to prevent printing arrow key characters + writer = threading.Thread(target=_send_stdin, args=(connection, _getch_unix)) + else: + enable_vt_mode() # needed for interactive commands (ie vim) + writer = threading.Thread(target=_send_stdin, args=(connection, _getch_windows)) + + return writer diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 2da7e3b9c10..22b337e51cf 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -4,14 +4,11 @@ # -------------------------------------------------------------------------------------------- # 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 -import os import threading import sys -import platform import time from urllib.parse import urlparse import websocket -import requests from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( @@ -55,12 +52,8 @@ _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) -from ._constants import (SSH_PROXY_FORWARD, SSH_PROXY_ERROR, SSH_PROXY_INFO, SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR, - SSH_BACKUP_ENCODING, SSH_DEFAULT_ENCODING, SSH_INPUT_PREFIX, SSH_CTRL_C_MSG) - -if platform.system() == "Windows": - import msvcrt # pylint: disable=import-error - from azure.cli.command_modules.container._vt_helper import enable_vt_mode # pylint: disable=ungrouped-imports +from ._ssh_utils import (ping_container_app, SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, + SSH_CTRL_C_MSG) logger = get_logger(__name__) @@ -1947,89 +1940,15 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ handle_raw_exception(e) -# pylint: disable=too-few-public-methods -class _WebSocketConnection: - def __init__(self, is_connected, socket): - self.is_connected = is_connected - self.socket = socket - - def disconnect(self): - logger.warning("Disconnecting...") - self.is_connected = False - self.socket.close() - - -def _read_ssh(connection, encodings): - while connection.is_connected: - response = connection.socket.recv() - if not response: - connection.disconnect() - else: - logger.info("Received raw response %s", response.hex()) - proxy_status = response[0] - if proxy_status == SSH_PROXY_INFO: - print(f"INFO: {response[1:].decode(encodings[0])}") - elif proxy_status == SSH_PROXY_ERROR: - print(f"ERROR: {response[1:].decode(encodings[0])}") - elif proxy_status == SSH_PROXY_FORWARD: - control_byte = response[1] - if control_byte in (SSH_CLUSTER_STDOUT, SSH_CLUSTER_STDERR): - for i, encoding in enumerate(encodings): - try: - print(response[2:].decode(encoding), end="", flush=True) - break - except UnicodeDecodeError as e: - if i == len(encodings) - 1: # ran out of encodings to try - connection.disconnect() - logger.info("Proxy Control Byte: %s", response[0]) - logger.info("Cluster Control Byte: %s", response[1]) - logger.info("Hexdump: %s", response[2:].hex()) - raise CLIInternalError("Failed to decode server data") from e - logger.info("Failed to encode with encoding %s", encoding) - else: - connection.disconnect() - raise CLIInternalError("Unexpected message received") - - -def _send_stdin(connection, getch_fn): - while connection.is_connected: - _resize_terminal(connection) - ch = getch_fn() - _resize_terminal(connection) - if connection.is_connected: - connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) - - -def _resize_terminal(connection): - size = os.get_terminal_size() - if connection.is_connected: - connection.socket.send(b"".join([b"\x00\x04", - f'{{"Width": {size.columns}, ' - f'"Height": {size.lines}}}'.encode(SSH_DEFAULT_ENCODING)])) - - -def _getch_unix(): - return sys.stdin.read(1).encode(SSH_DEFAULT_ENCODING) - - -def _getch_windows(): - while not msvcrt.kbhit(): - time.sleep(0.01) - return msvcrt.getch() - - -# FYI currently only works against Jeff's app -# TODO manage terminal size -# TODO implement timeout if needed # TODO token will be read from header at some point # TODO validate argument values (+ defaults) -def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None): +def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command=None): app = ContainerAppClient.show(cmd, resource_group_name, name) if not revision: revision = app["properties"]["latestRevisionName"] if not replica: # VVV this may not be necessary according to Anthony Chu - requests.get(f'https://{app["properties"]["configuration"]["ingress"]["fqdn"]}') # needed to get an alive replica + ping_container_app(app) # needed to get an alive replica replicas = ContainerAppClient.list_replicas(cmd=cmd, resource_group_name=resource_group_name, container_app_name=name, @@ -2040,7 +1959,6 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No # TODO validate that this container is in the current replica or make the user specify it -- or pick a container differently sub_id = get_subscription_id(cmd.cli_ctx) - command = "bash" # TODO check if there are other shells that can be used token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) token = token_response["properties"]["token"] @@ -2049,29 +1967,22 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No proxy_api_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")].replace("https://", "") url = (f"wss://{proxy_api_url}/subscriptions/{sub_id}/resourceGroups/{resource_group_name}/containerApps/{name}" - f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{command}?token={token}") + f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}?token={token}") # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found socket = websocket.WebSocket(enable_multithread=True) - encodings = [SSH_DEFAULT_ENCODING, SSH_BACKUP_ENCODING] + encodings = [SSH_DEFAULT_ENCODING, SSH_DEFAULT_ENCODING] logger.warning("Attempting to connect to %s", url) socket.connect(url) - conn = _WebSocketConnection(is_connected=True, socket=socket) + conn = WebSocketConnection(is_connected=True, socket=socket) - reader = threading.Thread(target=_read_ssh, args=(conn, encodings)) + reader = threading.Thread(target=read_ssh, args=(conn, encodings)) reader.daemon = True reader.start() - if platform.system() != "Windows": - import tty - tty.setcbreak(sys.stdin.fileno()) # needed to prevent printing arrow key characters - writer = threading.Thread(target=_send_stdin, args=(conn, _getch_unix)) - else: - enable_vt_mode() # needed for interactive commands (ie vim) - writer = threading.Thread(target=_send_stdin, args=(conn, _getch_windows)) - + writer = get_stdin_writer(conn) writer.daemon = True writer.start() From 19d91bb11013afaf338126bfc0e64af1e38bd26b Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 6 Apr 2022 16:45:27 -0700 Subject: [PATCH 114/158] organize code, remove token from warning output --- src/containerapp/azext_containerapp/_help.py | 2 +- .../azext_containerapp/_ssh_utils.py | 56 +++++++++++++++---- src/containerapp/azext_containerapp/custom.py | 27 ++------- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index feb84ae6509..90c3216d280 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -88,7 +88,7 @@ helps['containerapp ssh'] = """ type: command - short-summary: Open an interactive shell within a container app replica via SSH + short-summary: Open an SSH-like interactive shell within a container app pod examples: - name: ssh into a container app text: | diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index 43f5c077d44..9399db8a28d 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -9,10 +9,13 @@ import platform import threading import requests +import websocket from knack.log import get_logger - from azure.cli.core.azclierror import CLIInternalError +from azure.cli.core.commands.client_factory import get_subscription_id + +from ._clients import ContainerAppClient if platform.system() == "Windows": import msvcrt # pylint: disable=import-error @@ -33,6 +36,9 @@ # forward byte + stdin byte SSH_INPUT_PREFIX = b"\x00\x00" +# forward byte + terminal resize byte +SSH_TERM_RESIZE_PREFIX = b"\x00\x04" + SSH_DEFAULT_ENCODING = "utf-8" SSH_BACKUP_ENCODING = "latin_1" @@ -41,14 +47,44 @@ # pylint: disable=too-few-public-methods class WebSocketConnection: - def __init__(self, is_connected, socket): - self.is_connected = is_connected - self.socket = socket + def __init__(self, cmd, resource_group_name, name, revision, replica, container, startup_command): + self._url = self._get_url(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, + replica=replica, container=container, startup_command=startup_command) + self._socket = websocket.WebSocket(enable_multithread=True) + logger.warning("Attempting to connect to %s", self._remove_token(self._url)) + + # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found + self._socket.connect(self._url) + self.is_connected = True + + @classmethod + def _remove_token(cls, url): + if "?token=" in url: + return url[:url.index("?token=")] + return url + + + @classmethod + def _get_url(cls, cmd, resource_group_name, name, revision, replica, container, startup_command): + sub = get_subscription_id(cmd.cli_ctx) + token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) + token = token_response["properties"]["token"] + logstream_endpoint = token_response["properties"]["logStreamEndpoint"] + proxy_api_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")].replace("https://", "") + + return (f"wss://{proxy_api_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" + f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}?token={token}") def disconnect(self): logger.warning("Disconnecting...") self.is_connected = False - self.socket.close() + self._socket.close() + + def send(self, *args, **kwargs): + return self._socket.send(*args, **kwargs) + + def recv(self, *args, **kwargs): + return self._socket.recv(*args, **kwargs) def _decode_and_output_to_terminal(connection: WebSocketConnection, response, encodings): @@ -69,7 +105,7 @@ def _decode_and_output_to_terminal(connection: WebSocketConnection, response, en def read_ssh(connection: WebSocketConnection, response_encodings): # response_encodings is the ordered list of Unicode encodings to try to decode with before raising an exception while connection.is_connected: - response = connection.socket.recv() + response = connection.recv() if not response: connection.disconnect() else: @@ -94,15 +130,15 @@ def _send_stdin(connection: WebSocketConnection, getch_fn): ch = getch_fn() _resize_terminal(connection) if connection.is_connected: - connection.socket.send(b"".join([SSH_INPUT_PREFIX, ch])) + connection.send(b"".join([SSH_INPUT_PREFIX, ch])) def _resize_terminal(connection: WebSocketConnection): size = os.get_terminal_size() if connection.is_connected: - connection.socket.send(b"".join([b"\x00\x04", - f'{{"Width": {size.columns}, ' - f'"Height": {size.lines}}}'.encode(SSH_DEFAULT_ENCODING)])) + connection.send(b"".join([SSH_TERM_RESIZE_PREFIX, + f'{{"Width": {size.columns}, ' + f'"Height": {size.lines}}}'.encode(SSH_DEFAULT_ENCODING)])) def _getch_unix(): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 22b337e51cf..6b27add99ff 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -8,7 +8,6 @@ import sys import time from urllib.parse import urlparse -import websocket from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( @@ -53,7 +52,7 @@ _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) from ._ssh_utils import (ping_container_app, SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, - SSH_CTRL_C_MSG) + SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) logger = get_logger(__name__) @@ -1958,26 +1957,10 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No container = app["properties"]["template"]["containers"][0]["name"] # TODO validate that this container is in the current replica or make the user specify it -- or pick a container differently - sub_id = get_subscription_id(cmd.cli_ctx) - - token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) - token = token_response["properties"]["token"] - logstream_endpoint = token_response["properties"]["logStreamEndpoint"] - - proxy_api_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")].replace("https://", "") - - url = (f"wss://{proxy_api_url}/subscriptions/{sub_id}/resourceGroups/{resource_group_name}/containerApps/{name}" - f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}?token={token}") - - # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found - socket = websocket.WebSocket(enable_multithread=True) - encodings = [SSH_DEFAULT_ENCODING, SSH_DEFAULT_ENCODING] - - logger.warning("Attempting to connect to %s", url) - socket.connect(url) - - conn = WebSocketConnection(is_connected=True, socket=socket) + conn = WebSocketConnection(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, + replica=replica, container=container, startup_command=startup_command) + encodings = [SSH_DEFAULT_ENCODING, SSH_BACKUP_ENCODING] reader = threading.Thread(target=read_ssh, args=(conn, encodings)) reader.daemon = True reader.start() @@ -1993,4 +1976,4 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No except KeyboardInterrupt: if conn.is_connected: logger.info("Caught KeyboardInterrupt. Sending ctrl+c to server") - socket.send(SSH_CTRL_C_MSG) + conn.send(SSH_CTRL_C_MSG) From 06088cd38e766036fdb0abccda6476f725e6c81a Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 6 Apr 2022 18:20:04 -0700 Subject: [PATCH 115/158] add validations, add replica commands --- .../azext_containerapp/_clients.py | 32 ++++++++- src/containerapp/azext_containerapp/_help.py | 45 ++++++++++-- .../azext_containerapp/_params.py | 9 ++- .../azext_containerapp/_ssh_utils.py | 2 - .../azext_containerapp/_validators.py | 70 ++++++++++++++++++- .../azext_containerapp/commands.py | 7 +- src/containerapp/azext_containerapp/custom.py | 37 +++++----- 7 files changed, 173 insertions(+), 29 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index c072cc058a2..cd1debfc0d2 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -353,10 +353,10 @@ 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() - # TODO support pagination - # TODO expose via a command @classmethod def list_replicas(cls, cmd, resource_group_name, container_app_name, revision_name): + replica_list = [] + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager sub_id = get_subscription_id(cmd.cli_ctx) url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}/replicas?api-version={}" @@ -368,6 +368,34 @@ def list_replicas(cls, cmd, resource_group_name, container_app_name, revision_na revision_name, NEW_API_VERSION) + r = send_raw_request(cmd.cli_ctx, "GET", request_url) + j = r.json() + for replica in j["value"]: + replica_list.append(replica) + + 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 replica in j["value"]: + replica_list.append(replica) + + return replica_list + + @classmethod + def get_replica(cls, cmd, resource_group_name, container_app_name, revision_name, replica_name): + management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + sub_id = get_subscription_id(cmd.cli_ctx) + url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}/revisions/{}/replicas/{}/?api-version={}" + request_url = url_fmt.format( + management_hostname.strip('/'), + sub_id, + resource_group_name, + container_app_name, + revision_name, + replica_name, + NEW_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 90c3216d280..a32e90b9ff1 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -86,16 +86,49 @@ az containerapp list -g MyResourceGroup """ -helps['containerapp ssh'] = """ +helps['containerapp exec'] = """ type: command - short-summary: Open an SSH-like interactive shell within a container app pod + short-summary: Open an SSH-like interactive shell within a container app replica (pod) examples: - - name: ssh into a container app + - name: exec into a container app text: | - az containerapp ssh -n MyContainerapp -g MyResourceGroup - - name: ssh into a particular container app replica and revision + az containerapp exec -n MyContainerapp -g MyResourceGroup + - name: exec into a particular container app replica and revision text: | - az containerapp ssh -n MyContainerapp -g MyResourceGroup + az containerapp exec -n MyContainerapp -g MyResourceGroup --replica MyReplica --revision MyRevision + - name: open a bash shell in a containerapp + text: | + az containerapp exec -n MyContainerapp -g MyResourceGroup --command bash +""" + +# Replica Commands +helps['containerapp replica'] = """ + type: group + short-summary: Manage container app replicas (pods) +""" + +helps['containerapp replica list'] = """ + type: command + short-summary: List a container app revision's replicas (pods) + examples: + - name: List a container app's replicas in the latest revision + text: | + az containerapp replica list -n MyContainerapp -g MyResourceGroup + - name: List a container app's replicas in a particular revision + text: | + az containerapp replica list -n MyContainerapp -g MyResourceGroup --revision MyRevision +""" + +helps['containerapp replica show'] = """ + type: command + short-summary: Show a container app replica (pod) + examples: + - name: Show a replica from the latest revision + text: | + az containerapp replica show -n MyContainerapp -g MyResourceGroup --replica MyReplica + - name: Show a replica from the a particular revision + text: | + az containerapp replica show -n MyContainerapp -g MyResourceGroup --replica MyReplica --revision MyRevision """ # Revision Commands diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 8933396a68e..46154481c8f 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('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') - with self.argument_context('containerapp ssh') as c: + with self.argument_context('containerapp exec') as c: c.argument('container', help="The name of the container to ssh into") c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") c.argument('revision', help="The name of the container app revision to ssh into") @@ -38,6 +38,13 @@ def load_arguments(self, _): c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) + # Replica + with self.argument_context('containerapp replica') as c: + c.argument('replica', help="The name of the replica (pod). ") + c.argument('revision', help="The name of the container app revision. Defaults to the latest revision.") + c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") + c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) + # Container with self.argument_context('containerapp', arg_group='Container') as c: c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index 9399db8a28d..21fbbea4eb2 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -45,7 +45,6 @@ SSH_CTRL_C_MSG = b"\x00\x00\x03" -# pylint: disable=too-few-public-methods class WebSocketConnection: def __init__(self, cmd, resource_group_name, name, revision, replica, container, startup_command): self._url = self._get_url(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, @@ -63,7 +62,6 @@ def _remove_token(cls, url): return url[:url.index("?token=")] return url - @classmethod def _get_url(cls, cmd, resource_group_name, name, revision, replica, container, startup_command): sub = get_subscription_id(cmd.cli_ctx) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index e7fe0435a11..4f5158dfc0b 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -4,7 +4,10 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long -from azure.cli.core.azclierror import (ValidationError) +from azure.cli.core.azclierror import (ValidationError, ResourceNotFoundError) + +from ._clients import ContainerAppClient +from ._ssh_utils import ping_container_app def _is_number(s): @@ -86,3 +89,68 @@ def validate_ingress(namespace): if namespace.ingress: if not namespace.target_port: raise ValidationError("Usage error: must specify --target-port with --ingress") + + +def _set_ssh_defaults(cmd, namespace): + app = ContainerAppClient.show(namespace.cmd, namespace.resource_group_name, namespace.name) + if not app: + raise ResourceNotFoundError("Could not find a container app") + replicas = [] + if not namespace.revision: + namespace.revision = app.get("properties", {}).get("latestRevisionName") + if not namespace.revision: + raise ResourceNotFoundError("Could not find a revision") + if not namespace.replica: + # VVV this may not be necessary according to Anthony Chu + ping_container_app(app) # needed to get an alive replica + replicas = ContainerAppClient.list_replicas(cmd=cmd, + resource_group_name=namespace.resource_group_name, + container_app_name=namespace.name, + revision_name=namespace.revision) + if not replicas: + raise ResourceNotFoundError("Could not find a replica for this app") + namespace.replica = replicas[0]["name"] + if not namespace.container: + replica_containers = ContainerAppClient.get_replica(cmd=cmd, + resource_group_name=namespace.resource_group_name, + container_app_name=namespace.name, + revision_name=namespace.revision, + replica_name=namespace.replica)["properties"]["containers"] + # sadly this may be a system container, but the API will stop exposing system containers soon + namespace.container = replica_containers[0]["name"] + # container = app["properties"]["template"]["containers"][0]["name"] + + +def _validate_revision_exists(cmd, namespace): + revision = ContainerAppClient.show_revision(cmd, resource_group_name=namespace.resource_group_name, + container_app_name=namespace.name, name=namespace.revision) + if not revision: + raise ResourceNotFoundError("Could not find revision") + + +def _validate_replica_exists(cmd, namespace): + replica = ContainerAppClient.get_replica(cmd=cmd, + resource_group_name=namespace.resource_group_name, + container_app_name=namespace.name, + revision_name=namespace.revision, + replica_name=namespace.replica) + if not replica: + raise ResourceNotFoundError("Could not find replica") + + +def _validate_container_exists(cmd, namespace): + replica_containers = ContainerAppClient.get_replica(cmd=cmd, + resource_group_name=namespace.resource_group_name, + container_app_name=namespace.name, + revision_name=namespace.revision, + replica_name=namespace.replica)["properties"]["containers"] + matches = [r for r in replica_containers if r["name"].lower() == namespace.container.lower()] + if not matches: + raise ResourceNotFoundError("Could not find container") + + +def validate_ssh(cmd, namespace): + _set_ssh_defaults(cmd, namespace) + _validate_revision_exists(cmd, namespace) + _validate_replica_exists(cmd, namespace) + _validate_container_exists(cmd, namespace) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index b5d44a77cd4..2939a6f124c 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -7,6 +7,7 @@ # 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 +from ._validators import validate_ssh def transform_containerapp_output(app): @@ -49,7 +50,11 @@ def load_command_table(self, _): 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()) - g.custom_command('ssh', 'containerapp_ssh', is_preview=True) + g.custom_command('exec', 'containerapp_ssh', is_preview=True, validator=validate_ssh) + + with self.command_group('containerapp replica', is_preview=True) as g: + g.custom_show_command('show', 'get_replica') # TODO implement the table transformer + g.custom_command('list', 'list_replicas') 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 6b27add99ff..c671620f55e 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -51,8 +51,8 @@ _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) -from ._ssh_utils import (ping_container_app, SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, - SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) +from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, + SSH_BACKUP_ENCODING) logger = get_logger(__name__) @@ -1939,24 +1939,29 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ handle_raw_exception(e) -# TODO token will be read from header at some point -# TODO validate argument values (+ defaults) -def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command=None): +def list_replicas(cmd, resource_group_name, name, revision=None): + app = ContainerAppClient.show(cmd, resource_group_name, name) + if not revision: + revision = app["properties"]["latestRevisionName"] + return ContainerAppClient.list_replicas(cmd=cmd, + resource_group_name=resource_group_name, + container_app_name=name, + revision_name=revision) + + +def get_replica(cmd, resource_group_name, name, replica, revision=None): app = ContainerAppClient.show(cmd, resource_group_name, name) if not revision: revision = app["properties"]["latestRevisionName"] - if not replica: - # VVV this may not be necessary according to Anthony Chu - ping_container_app(app) # needed to get an alive replica - replicas = ContainerAppClient.list_replicas(cmd=cmd, - resource_group_name=resource_group_name, - container_app_name=name, - revision_name=revision) - replica = replicas["value"][0]["name"] # TODO validate that a replica exists - if not container: - container = app["properties"]["template"]["containers"][0]["name"] - # TODO validate that this container is in the current replica or make the user specify it -- or pick a container differently + return ContainerAppClient.get_replica(cmd=cmd, + resource_group_name=resource_group_name, + container_app_name=name, + revision_name=revision, + replica_name=replica) + +# TODO token will be read from header at some point +def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command=None): conn = WebSocketConnection(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, replica=replica, container=container, startup_command=startup_command) From 668082b10f62e5328046d1d39a304268ba43689b Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 7 Apr 2022 10:17:38 -0700 Subject: [PATCH 116/158] use the correct API for fetching default container; remove is_preview --- src/containerapp/azext_containerapp/_ssh_utils.py | 3 ++- src/containerapp/azext_containerapp/_utils.py | 8 ++++++++ .../azext_containerapp/_validators.py | 15 +++++++-------- src/containerapp/azext_containerapp/commands.py | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index 21fbbea4eb2..3085ebf5400 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -16,6 +16,7 @@ from azure.cli.core.commands.client_factory import get_subscription_id from ._clients import ContainerAppClient +from ._utils import safe_get if platform.system() == "Windows": import msvcrt # pylint: disable=import-error @@ -150,7 +151,7 @@ def _getch_windows(): def ping_container_app(app): - site = app.get("properties", {}).get("configuration", {}).get("ingress", {}).get("fqdn") + site = safe_get(app, "properties", "configuration", "ingress", "fqdn") if site: resp = requests.get(f'https://{site}') if not resp.ok: diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 39ccef52633..be6a1b56036 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -585,3 +585,11 @@ def _registry_exists(containerapp_def, registry_server): exists = True break return exists + + +# get a value from nested dict without getting IndexError (returns None instead) +# for example, model["key1"]["key2"]["key3"] would become safe_get(model, "key1", "key2", "key3") +def safe_get(model, *keys): + for k in keys[:-1]: + model = model.get(k, {}) + return model.get(keys[-1]) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index 4f5158dfc0b..b1a507ec9df 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -8,6 +8,7 @@ from ._clients import ContainerAppClient from ._ssh_utils import ping_container_app +from ._utils import safe_get def _is_number(s): @@ -111,14 +112,12 @@ def _set_ssh_defaults(cmd, namespace): raise ResourceNotFoundError("Could not find a replica for this app") namespace.replica = replicas[0]["name"] if not namespace.container: - replica_containers = ContainerAppClient.get_replica(cmd=cmd, - resource_group_name=namespace.resource_group_name, - container_app_name=namespace.name, - revision_name=namespace.revision, - replica_name=namespace.replica)["properties"]["containers"] - # sadly this may be a system container, but the API will stop exposing system containers soon - namespace.container = replica_containers[0]["name"] - # container = app["properties"]["template"]["containers"][0]["name"] + revision = ContainerAppClient.show_revision(cmd, resource_group_name=namespace.resource_group_name, + container_app_name=namespace.name, + name=namespace.revision) + revision_containers = safe_get(revision, "properties", "template", "containers") + if revision_containers: + namespace.container = revision_containers[0]["name"] def _validate_revision_exists(cmd, namespace): diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 2939a6f124c..eb1fd3c7959 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -50,7 +50,7 @@ def load_command_table(self, _): 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()) - g.custom_command('exec', 'containerapp_ssh', is_preview=True, validator=validate_ssh) + g.custom_command('exec', 'containerapp_ssh', validator=validate_ssh) with self.command_group('containerapp replica', is_preview=True) as g: g.custom_show_command('show', 'get_replica') # TODO implement the table transformer From 5b2b3c692884c852601e63f611f16aee740cf4ab Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 7 Apr 2022 14:47:53 -0400 Subject: [PATCH 117/158] Renamed silent to quiet. --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_utils.py | 8 ++++---- src/containerapp/azext_containerapp/custom.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index d85f8c42cf6..ce35f2f2535 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -184,7 +184,7 @@ def load_arguments(self, _): c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') c.argument('registry_server', configured_default='registry_server') - c.argument('disable_verbose', help="Disable verbose output from ACR build when using --source.") + c.argument('quiet', help="Disable logs output from ACR build when using --source.") c.argument('dockerfile', help="Name of the dockerfile.") c.argument('dryrun', help="Show summary of the operation instead of executing it.") diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index a04ebce9f4d..3d8ee9bf9eb 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -289,7 +289,7 @@ def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, loc logs_key = shared_keys.primary_shared_key - return logs_customer_id, logs_key, workspace_name + return logs_customer_id, logs_key def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): @@ -649,7 +649,7 @@ def _resource_client_factory(cli_ctx, **_): return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) -def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile", disable_verbose=False): +def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile", quiet=False): import os import uuid import tempfile @@ -716,13 +716,13 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi run_id = queued_build.run_id logger.warning("Queued a build with ID: %s", run_id) - not disable_verbose and logger.warning("Waiting for agent...") + not quiet and logger.warning("Waiting for agent...") from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) from ._acr_run_polling import get_run_with_polling client_runs = cf_acr_runs(cmd.cli_ctx) - if disable_verbose: + if quiet: lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) logger.warning("Build {}.".format(acr.status.lower())) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f444f4a5e58..0012e79bf44 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1956,7 +1956,7 @@ def containerapp_up(cmd, dryrun=False, logs_customer_id=None, logs_key=None, - disable_verbose=False): + quiet=False): import os, json src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) @@ -2084,7 +2084,7 @@ def containerapp_up(cmd, image_name += ":{}".format(str(now).replace(' ', '').replace('-','').replace('.','').replace(':','')) image = registry_server + '/' + image_name if not dryrun: - queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, disable_verbose) + queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) dry_run_str = r""" { From bba0599ec20d6e9d25d755cef77cc60cd12b1ad9 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 7 Apr 2022 17:38:20 -0400 Subject: [PATCH 118/158] Fixed style issues. --- .../azext_containerapp/_acr_run_polling.py | 3 +- src/containerapp/azext_containerapp/_utils.py | 19 ++------ src/containerapp/azext_containerapp/custom.py | 45 ++++++++++--------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/src/containerapp/azext_containerapp/_acr_run_polling.py b/src/containerapp/azext_containerapp/_acr_run_polling.py index 1ea21228617..1a71a87c99a 100644 --- a/src/containerapp/azext_containerapp/_acr_run_polling.py +++ b/src/containerapp/azext_containerapp/_acr_run_polling.py @@ -2,14 +2,15 @@ # 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 import time -from azure.core.polling import PollingMethod, LROPoller from msrest import Deserializer from msrestazure.azure_exceptions import CloudError from azure.cli.core.profiles import ResourceType from azure.cli.command_modules.acr._constants import get_acr_task_models +from azure.core.polling import PollingMethod, LROPoller def get_run_with_polling(cmd, diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 3d8ee9bf9eb..0bb5284bc0b 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -2,10 +2,10 @@ # 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 +# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals from urllib.parse import urlparse -from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError) +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError, ResourceNotFoundError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from msrestazure.tools import parse_resource_id @@ -572,7 +572,7 @@ def _infer_acr_credentials(cmd, registry_server, disable_warnings=False): 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) + registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) # pylint: disable=unused-variable return (registry_user, registry_pass) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry {}. Please provide the registry username and password'.format(registry_name)) from ex @@ -602,15 +602,12 @@ def _set_webapp_up_default_args(cmd, resource_group_name, location, name, regist logger.warning("Setting 'az containerapp up' default arguments for current directory. " "Manage defaults with 'az configure --scope local'") - cmd.cli_ctx.config.set_value('defaults', 'resource_group_name', resource_group_name) logger.warning("--resource-group/-g default: %s", resource_group_name) - cmd.cli_ctx.config.set_value('defaults', 'location', location) logger.warning("--location/-l default: %s", location) - cmd.cli_ctx.config.set_value('defaults', 'name', name) logger.warning("--name/-n default: %s", name) @@ -661,29 +658,24 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi # client_registries = get_acr_service_client(cmd.cli_ctx).registries client_registries = cf_acr_registries_tasks(cmd.cli_ctx) - if not os.path.isdir(src_dir): raise ValidationError("Source directory should be a local directory path.") - docker_file_path = os.path.join(src_dir, dockerfile) if not os.path.isfile(docker_file_path): raise ValidationError("Unable to find '{}'.".format(docker_file_path)) - # NOTE: os.path.basename is unable to parse "\" in the file path original_docker_file_name = os.path.basename(docker_file_path.replace("\\", "/")) docker_file_in_tar = '{}_{}'.format(uuid.uuid4().hex, original_docker_file_name) tar_file_path = os.path.join(tempfile.gettempdir(), 'build_archive_{}.tar.gz'.format(uuid.uuid4().hex)) - source_location = upload_source_code(cmd, client_registries, registry_name, registry_rg, src_dir, tar_file_path, docker_file_path, docker_file_in_tar) # For local source, the docker file is added separately into tar as the new file name (docker_file_in_tar) # So we need to update the docker_file_path docker_file_path = docker_file_in_tar - from azure.cli.core.profiles import ResourceType OS, Architecture = cmd.get_models('OS', 'Architecture', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') # Default platform values @@ -691,7 +683,6 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi platform_arch = Architecture.amd64.value platform_variant = None - DockerBuildRequest, PlatformProperties = cmd.get_models('DockerBuildRequest', 'PlatformProperties', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') docker_build_request = DockerBuildRequest( @@ -707,13 +698,11 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi timeout=None, arguments=[]) - queued_build = LongRunningOperation(cmd.cli_ctx)(client_registries.begin_schedule_run( resource_group_name=registry_rg, registry_name=registry_name, run_request=docker_build_request)) - run_id = queued_build.run_id logger.warning("Queued a build with ID: %s", run_id) not quiet and logger.warning("Waiting for agent...") @@ -725,7 +714,7 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi if quiet: lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) - logger.warning("Build {}.".format(acr.status.lower())) + logger.warning("Build {}.".format(acr.status.lower())) # pylint: disable=logging-format-interpolation if acr.status.lower() != "succeeded": raise CLIInternalError("ACR build {}.".format(acr.status.lower())) return acr diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 0012e79bf44..d86982970a6 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.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, 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 +# 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, expression-not-assigned, unbalanced-tuple-unpacking from urllib.parse import urlparse # from azure.cli.command_modules.appservice.custom import (_get_acr_cred) @@ -47,8 +47,8 @@ _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, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, + _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr) logger = get_logger(__name__) @@ -1938,13 +1938,13 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ handle_raw_exception(e) -def containerapp_up(cmd, - name=None, - resource_group_name=None, +def containerapp_up(cmd, + name=None, + resource_group_name=None, managed_env=None, - location=None, - registry_server=None, - image=None, + location=None, + registry_server=None, + image=None, source=None, dockerfile="Dockerfile", # compose=None, @@ -1957,7 +1957,8 @@ def containerapp_up(cmd, logs_customer_id=None, logs_key=None, quiet=False): - import os, json + import os + import json src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) @@ -1968,15 +1969,15 @@ def containerapp_up(cmd, if image: name = image.split('/')[-1].split(':')[0].lower() if source: - temp = source[1:] if source[0] == '.' else source # replace first . if it exists - name = temp.split('/')[-1].lower() # replace first . if it exists + temp = source[1:] if source[0] == '.' else source # replace first . if it exists + name = temp.split('/')[-1].lower() # isolate folder name if len(name) == 0: - name = _src_path_escaped.split('\\')[-1] + name = _src_path_escaped.rsplit('\\', maxsplit=1)[-1] if source and image: image = image.replace(':', '') - if not location: + if not location: location = "eastus2" # check user's default location? find least populated server? custom_rg_name = None @@ -2022,10 +2023,10 @@ def containerapp_up(cmd, create_resource_group(cmd, rg_name, location) resource_group_name = rg_name if not managed_env: - env_name = "{}-env".format(name).replace("_","-") + env_name = "{}-env".format(name).replace("_", "-") if not dryrun: logger.warning("Creating new managed environment {}".format(env_name)) - managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] else: managed_env = env_name else: @@ -2034,13 +2035,13 @@ def containerapp_up(cmd, env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] if logs_customer_id and logs_key: if not dryrun: - managed_env = create_managed_environment(cmd, env_name, location = location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] if image is not None and "azurecr.io" in image and not dryrun: if registry_user is None or registry_pass is None: # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') - registry_server=image.split('/')[0] + registry_server = image.split('/')[0] parsed = urlparse(image) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: @@ -2057,7 +2058,7 @@ def containerapp_up(cmd, if registry_server: if "azurecr.io" not in registry_server: raise ValidationError("Cannot supply non-Azure registry when using --source.") - elif not dryrun and (registry_user is None or registry_pass is None): + if not dryrun and (registry_user is None or registry_pass is None): # If registry is Azure Container Registry, we can try inferring credentials logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') parsed = urlparse(registry_server) @@ -2070,7 +2071,7 @@ def containerapp_up(cmd, registry_rg = resource_group_name user = get_profile_username() registry_name = "{}acr".format(name) - registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-","") + registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") if not dryrun: logger.warning("Creating new acr {}".format(registry_name)) registry_def = create_new_acr(cmd, registry_name, registry_rg, location) @@ -2081,7 +2082,7 @@ def containerapp_up(cmd, image_name = image if image is not None else name from datetime import datetime now = datetime.now() - image_name += ":{}".format(str(now).replace(' ', '').replace('-','').replace('.','').replace(':','')) + image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) image = registry_server + '/' + image_name if not dryrun: queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) @@ -2103,4 +2104,4 @@ def containerapp_up(cmd, else: create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) - return json.loads(dry_run_str) + return json.loads(dry_run_str) From 99fb00fc060bcbf0b40dc258ae2c39220b3db00b Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 7 Apr 2022 16:19:05 -0700 Subject: [PATCH 119/158] add log streaming, bump version number and add to HISTORY.rst --- src/containerapp/HISTORY.rst | 6 ++++ src/containerapp/azext_containerapp/_help.py | 20 +++++++++++ .../azext_containerapp/_params.py | 12 ++++++- .../azext_containerapp/_validators.py | 1 + .../azext_containerapp/commands.py | 3 ++ src/containerapp/azext_containerapp/custom.py | 34 ++++++++++++++++++- src/containerapp/setup.py | 2 +- 7 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index b66465a832a..206d9015a8a 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.2.0 +++++++ +* Open an ssh-like shell in a Container App with 'az containerapp exec' +* Support for log streaming with 'az containerapp log tail' +* Replica show and list commands + 0.1.0 ++++++ * Initial release for Container App support with Microsoft.App RP. diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index a32e90b9ff1..dff213d5f86 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -101,6 +101,26 @@ az containerapp exec -n MyContainerapp -g MyResourceGroup --command bash """ +helps['containerapp log'] = """ + type: group + short-summary: Show container app logs +""" + +helps['containerapp log tail'] = """ + type: command + short-summary: Show past logs and/or print logs in real time (with the --follow parameter). Note that the logs are only taken from one revision, replica (pod), and container. + examples: + - name: Fetch the past 10 lines of logs from an app and return + text: | + az containerapp log tail -n MyContainerapp -g MyResourceGroup + - name: Fetch 20 lines of past logs logs from an app and print logs as they come in + text: | + az containerapp log tail -n MyContainerapp -g MyResourceGroup --follow + - name: Fetch logs for a particular revision, replica, and container + text: | + az containerapp log tail -n MyContainerapp -g MyResourceGroup --replica MyReplica --revision MyRevision --container MyContainer +""" + # Replica Commands helps['containerapp replica'] = """ type: group diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 46154481c8f..91464547fbc 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -33,11 +33,21 @@ def load_arguments(self, _): with self.argument_context('containerapp exec') as c: c.argument('container', help="The name of the container to ssh into") c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") - c.argument('revision', help="The name of the container app revision to ssh into") + c.argument('revision', help="The name of the container app revision to ssh into. Defaults to the latest revision.") c.argument('startup_command', options_list=["--command"], default="sh", help="The startup command (bash, zsh, sh, etc.).") c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) + with self.argument_context('containerapp log tail') as c: + c.argument('follow', help="Print logs in real time if present.", arg_type=get_three_state_flag()) + c.argument('tail', help="The number of past logs to print (0-300)", type=int, default=10) + c.argument('container', help="The name of the container") + c.argument('output_format', options_list=["--format"], help="Log output format", arg_type=get_enum_type(["json", "text"]), default="json") + c.argument('replica', help="The name of the replica (pod). List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") + c.argument('revision', help="The name of the container app revision. Defaults to the latest revision.") + c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") + c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) + # Replica with self.argument_context('containerapp replica') as c: c.argument('replica', help="The name of the replica (pod). ") diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index b1a507ec9df..d1abe031092 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -148,6 +148,7 @@ def _validate_container_exists(cmd, namespace): raise ResourceNotFoundError("Could not find container") +# also used to validate logstream def validate_ssh(cmd, namespace): _set_ssh_defaults(cmd, namespace) _validate_revision_exists(cmd, namespace) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index eb1fd3c7959..21bbd6f9252 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -56,6 +56,9 @@ def load_command_table(self, _): g.custom_show_command('show', 'get_replica') # TODO implement the table transformer g.custom_command('list', 'list_replicas') + with self.command_group('containerapp log', is_preview=True) as g: + g.custom_command('tail', 'stream_containerapp_logs', validator=validate_ssh) + with self.command_group('containerapp env') as g: g.custom_show_command('show', 'show_managed_environment') g.custom_command('list', 'list_managed_environments') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index c671620f55e..6920f06642f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -8,6 +8,7 @@ import sys import time from urllib.parse import urlparse +import requests from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( @@ -1960,7 +1961,7 @@ def get_replica(cmd, resource_group_name, name, replica, revision=None): replica_name=replica) -# TODO token will be read from header at some point +# TODO token will be read from header at some point -- a PR has apparently been opened for this def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command=None): conn = WebSocketConnection(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, replica=replica, container=container, startup_command=startup_command) @@ -1982,3 +1983,34 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No if conn.is_connected: logger.info("Caught KeyboardInterrupt. Sending ctrl+c to server") conn.send(SSH_CTRL_C_MSG) + + +# TODO token will be in header soon (same as SSH) +def stream_containerapp_logs(cmd, resource_group_name, name, container=None, revision=None, replica=None, follow=False, + tail=None, output_format=None): + if tail: + if tail < 0 or tail > 300: + raise ValidationError("--tail must be between 0 and 300.") + + sub = get_subscription_id(cmd.cli_ctx) + token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) + token = token_response["properties"]["token"] + logstream_endpoint = token_response["properties"]["logStreamEndpoint"] + base_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")] + + url = (f"{base_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" + f"/revisions/{revision}/replicas/{replica}/containers/{container}/logstream?token={token}") + + logger.warning("connecting to : %s", url) + request_params = {"follow": str(follow).lower(), "output": output_format, "tailLines": tail} + resp = requests.get(url, timeout=None, stream=True, params=request_params) + + if not resp.ok: + ValidationError(f"Got bad status from the logstream API: {resp.status_code}") + + for line in resp.iter_lines(): + if line: + logger.info("received raw log line: %s", line) + # these .replaces are needed to display color/quotations properly + # for some reason the API returns garbled unicode special characters (may need to add more in the future) + print(line.decode("utf-8").replace("\\u0022", "\u0022").replace("\\u001B", "\u001B")) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index e23b0011367..22613cb6a10 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.2.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From 5a6e7392608a38d70c24e0be68fcef8c46f02ff9 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Fri, 8 Apr 2022 13:42:34 -0700 Subject: [PATCH 120/158] add basic ssh test --- .../azext_containerapp/_validators.py | 2 +- .../latest/test_containerapp_scenario.py | 79 ++++++++++++++++--- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/containerapp/azext_containerapp/_validators.py b/src/containerapp/azext_containerapp/_validators.py index d1abe031092..f4d25ed5f20 100644 --- a/src/containerapp/azext_containerapp/_validators.py +++ b/src/containerapp/azext_containerapp/_validators.py @@ -93,7 +93,7 @@ def validate_ingress(namespace): def _set_ssh_defaults(cmd, namespace): - app = ContainerAppClient.show(namespace.cmd, namespace.resource_group_name, namespace.name) + app = ContainerAppClient.show(cmd, namespace.resource_group_name, namespace.name) if not app: raise ResourceNotFoundError("Could not find a container app") replicas = [] 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 9a89dcc55c9..2692ccfc4ef 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -4,17 +4,16 @@ # -------------------------------------------------------------------------------------------- import os -import time -import unittest +from unittest import mock +from azure.cli.testsdk.reverse_dependency import get_dummy_cli from azure.cli.testsdk.scenario_tests import AllowLargeResponse -from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, JMESPathCheck) +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, JMESPathCheck, live_only) TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) -@unittest.skip("Managed environment flaky") class ContainerappScenarioTest(ScenarioTest): @AllowLargeResponse(8192) @ResourceGroupPreparer(location="centraluseuap") @@ -22,21 +21,77 @@ def test_containerapp_e2e(self, resource_group): containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) env_name = self.create_random_name(prefix='containerapp-e2e-env', length=24) - self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + self.cmd(f'containerapp env create -g {resource_group} -n {env_name}') - # Sleep in case env create takes a while - time.sleep(60) - self.cmd('containerapp env list -g {}'.format(resource_group), checks=[ + self.cmd(f'containerapp env list -g {resource_group}', checks=[ JMESPathCheck('length(@)', 1), JMESPathCheck('[0].name', env_name), ]) - self.cmd('containerapp create -g {} -n {} --environment {}'.format(resource_group, containerapp_name, env_name), checks=[ + self.cmd(f'containerapp create -g {resource_group} -n {containerapp_name} --environment {env_name}', checks=[ JMESPathCheck('name', containerapp_name) ]) - # Sleep in case containerapp create takes a while - time.sleep(60) - self.cmd('containerapp show -g {} -n {}'.format(resource_group, containerapp_name), checks=[ + self.cmd(f'containerapp show -g {resource_group} -n {containerapp_name}', checks=[ JMESPathCheck('name', containerapp_name) ]) + + @live_only() # VCR.py can't seem to handle websockets (only --live works) + # @ResourceGroupPreparer(location="centraluseuap") + @mock.patch("azext_containerapp._ssh_utils._resize_terminal") + # @mock.patch("azext_containerapp._ssh_utils.enable_vt_mode") -- TODO handle this on windows + @mock.patch("tty.setcbreak") # TODO don't do this on windows + @mock.patch("sys.stdin") + def test_containerapp_ssh(self, resource_group=None, *args): + # containerapp_name = self.create_random_name(prefix='capp', length=24) + # env_name = self.create_random_name(prefix='env', length=24) + + # self.cmd(f'containerapp env create -g {resource_group} -n {env_name}') + # self.cmd(f'containerapp create -g {resource_group} -n {containerapp_name} --environment {env_name} --min-replicas 1 --ingress external') + + # TODO remove hardcoded app info (currently the SSH feature is only enabled in stage) + # these are only in my sub so they won't work on the CI / other people's machines + containerapp_name = "stage" + resource_group = "sca" + + stdout_buff = [] + + def mock_print(*args, end="\n", **kwargs): + out = " ".join([str(a) for a in args]) + if not stdout_buff: + stdout_buff.append(out) + elif end != "\n": + stdout_buff[-1] = f"{stdout_buff[-1]}{out}" + else: + stdout_buff.append(out) + + commands = "\n".join(["whoami", "pwd", "ls -l | grep index.js", "exit\n"]) + expected_output = ["root", "/usr/src/app", "-rw-r--r-- 1 root root 267 Oct 15 00:21 index.js"] + + idx = [0] + def mock_getch(): + ch = commands[idx[0]].encode("utf-8") + idx[0] = (idx[0] + 1) % len(commands) + return ch + + cmd = mock.MagicMock() + cmd.cli_ctx = get_dummy_cli() + from azext_containerapp._validators import validate_ssh + from azext_containerapp.custom import containerapp_ssh + + class Namespace: pass + namespace = Namespace() + setattr(namespace, "name", containerapp_name) + setattr(namespace, "resource_group_name", resource_group) + setattr(namespace, "revision", None) + setattr(namespace, "replica", None) + setattr(namespace, "container", None) + + validate_ssh(cmd=cmd, namespace=namespace) # needed to set values for container, replica, revision + + with mock.patch("builtins.print", side_effect=mock_print): + with mock.patch("azext_containerapp._ssh_utils._getch_unix", side_effect=mock_getch), mock.patch("azext_containerapp._ssh_utils._getch_windows", side_effect=mock_getch): + containerapp_ssh(cmd=cmd, resource_group_name=namespace.resource_group_name, name=namespace.name, + container=namespace.container, revision=namespace.revision, replica=namespace.replica, startup_command="sh") + for line in expected_output: + self.assertIn(line, expected_output) From b8315bccb25652af565513b7efd9ee57dab1641b Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 8 Apr 2022 17:00:11 -0400 Subject: [PATCH 121/158] Added workspace name and fqdn to dry_run_str. Added indicators of if the resources are new or existing to dry_run_str. --- src/containerapp/azext_containerapp/_utils.py | 10 ++- src/containerapp/azext_containerapp/custom.py | 61 +++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 0bb5284bc0b..203f88bdfd8 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -232,6 +232,15 @@ def _generate_log_analytics_workspace_name(resource_group_name): return name +def _get_log_analytics_workspace_name(cmd, logs_customer_id, resource_group_name): + log_analytics_client = log_analytics_client_factory(cmd.cli_ctx) + logs_list = log_analytics_client.list_by_resource_group(resource_group_name) + for log in logs_list: + if log.customer_id.lower() == logs_customer_id.lower(): + return log.name + return ResourceNotFoundError("Cannot find Log Analytics workspace with customer ID {}".format(logs_customer_id)) + + 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.") @@ -566,7 +575,6 @@ def _infer_acr_credentials(cmd, registry_server, disable_warnings=False): # If registry is Azure Container Registry, we can try inferring credentials if '.azurecr.io' not in registry_server: raise RequiredArgumentMissingError('Registry username and password are required if not using Azure Container Registry.') - logger.warning("Infer acr credentials") not disable_warnings and 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] diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d86982970a6..46594d7f61d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -49,7 +49,7 @@ _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, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, - get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr) + get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name) logger = get_logger(__name__) @@ -1962,6 +1962,11 @@ def containerapp_up(cmd, src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) + new_rg = "Existing" + new_managed_env = "Existing" + new_ca = "New" + new_cr = "Existing" + if source is None and image is None: raise RequiredArgumentMissingError("You must specify either --source or --image.") @@ -2016,6 +2021,7 @@ def containerapp_up(cmd, env_name = "" if not managed_env else managed_env.split('/')[6] if not containerapp_def: if not resource_group_name: + new_rg = "New" user = get_profile_username() rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name if not dryrun: @@ -2023,6 +2029,7 @@ def containerapp_up(cmd, create_resource_group(cmd, rg_name, location) resource_group_name = rg_name if not managed_env: + new_managed_env = "New" env_name = "{}-env".format(name).replace("_", "-") if not dryrun: logger.warning("Creating new managed environment {}".format(env_name)) @@ -2030,6 +2037,7 @@ def containerapp_up(cmd, else: managed_env = env_name else: + new_ca = "Existing" location = containerapp_def["location"] managed_env = containerapp_def["properties"]["managedEnvironmentId"] env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] @@ -2068,6 +2076,7 @@ def containerapp_up(cmd, except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex else: + new_cr = "New" registry_rg = resource_group_name user = get_profile_username() registry_name = "{}acr".format(name) @@ -2088,20 +2097,48 @@ def containerapp_up(cmd, queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) - dry_run_str = r""" { - "name" : "%s", - "resourcegroup" : "%s", - "location" : "%s", - "environment" : "%s", - "registry": "%s", - "image" : "%s", - "src_path" : "%s" - } - """ % (name, resource_group_name, location, env_name, registry_server, image, _src_path_escaped) + log_analytics_workspace_name = "" + env_def = None + try: + env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) + except: + pass + if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": + env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] + log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) + + containerapp_def = None + if dryrun: logger.warning("Containerapp will be created with the below configuration, re-run command " "without the --dryrun flag to create & deploy a new containerapp.") else: - create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) + containerapp_def = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) + + fqdn = "" + + dry_run_str = """ { + "name" : "%s (%s)", + "resourcegroup" : "%s (%s)", + "location" : "%s", + "environment" : "%s (%s)", + "registry": "%s (%s)", + "image" : "%s", + "src_path" : "%s", + """ % (name, new_ca, resource_group_name, new_rg, location, env_name, new_managed_env, registry_server, new_cr, image, _src_path_escaped) + + if containerapp_def: + r = containerapp_def + if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: + fqdn = r["properties"]["configuration"]["ingress"]["fqdn"] + + if len(fqdn) > 0: + dry_run_str += '"fqdn" : "{}",\n'.format(fqdn) + + if len(log_analytics_workspace_name) > 0: + dry_run_str += '"log_analytics_workspace_name" : "{}"\n'.format(log_analytics_workspace_name) + + dry_run_str += '}' + print(dry_run_str) return json.loads(dry_run_str) From 45faea742901f09d18861021a8c5bbc683acc896 Mon Sep 17 00:00:00 2001 From: StrawnSC Date: Fri, 8 Apr 2022 14:09:44 -0700 Subject: [PATCH 122/158] fix ssh test for windows --- .../tests/latest/test_containerapp_scenario.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 2692ccfc4ef..16e9e7a961e 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import os +import platform from unittest import mock from azure.cli.testsdk.reverse_dependency import get_dummy_cli @@ -39,8 +40,6 @@ def test_containerapp_e2e(self, resource_group): @live_only() # VCR.py can't seem to handle websockets (only --live works) # @ResourceGroupPreparer(location="centraluseuap") @mock.patch("azext_containerapp._ssh_utils._resize_terminal") - # @mock.patch("azext_containerapp._ssh_utils.enable_vt_mode") -- TODO handle this on windows - @mock.patch("tty.setcbreak") # TODO don't do this on windows @mock.patch("sys.stdin") def test_containerapp_ssh(self, resource_group=None, *args): # containerapp_name = self.create_random_name(prefix='capp', length=24) @@ -89,7 +88,11 @@ class Namespace: pass validate_ssh(cmd=cmd, namespace=namespace) # needed to set values for container, replica, revision - with mock.patch("builtins.print", side_effect=mock_print): + mock_lib = "tty.setcbreak" + if platform.system() == "Windows": + mock_lib = "azext_containerapp._ssh_utils.enable_vt_mode" + + with mock.patch("builtins.print", side_effect=mock_print), mock.patch(mock_lib): with mock.patch("azext_containerapp._ssh_utils._getch_unix", side_effect=mock_getch), mock.patch("azext_containerapp._ssh_utils._getch_windows", side_effect=mock_getch): containerapp_ssh(cmd=cmd, resource_group_name=namespace.resource_group_name, name=namespace.name, container=namespace.container, revision=namespace.revision, replica=namespace.replica, startup_command="sh") From 0329a53382a26a3cc6996ead6fb3c00da41d8c3b Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 8 Apr 2022 17:22:35 -0400 Subject: [PATCH 123/158] Check RP for location when not provided. Open Dockerfile and use EXPOSE for CA port. --- src/containerapp/azext_containerapp/_utils.py | 21 +++++++++++++++++++ src/containerapp/azext_containerapp/custom.py | 16 ++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 203f88bdfd8..2f78a303020 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -205,6 +205,27 @@ def _get_default_log_analytics_location(cmd): return default_location +def _get_default_containerapps_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.App"), 'resource_types', []) + res_locations = [] + for res in resource_types: + if res and getattr(res, 'resource_type', "") == "workspaces": + res_locations = getattr(res, 'locations', []) + + if len(res_locations) > 0: + location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") + if location: + return location + + except Exception: # pylint: disable=broad-except + return default_location + return default_location + + # Generate random 4 character string def _new_tiny_guid(): import random diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 46594d7f61d..3f93e1295b0 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -49,7 +49,8 @@ _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, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, - get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name) + get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, + _get_default_containerapps_location) logger = get_logger(__name__) @@ -453,6 +454,8 @@ def update_containerapp(cmd, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") + if location: + return get if yaml: if image or min_replicas or max_replicas or\ set_env_vars or remove_env_vars or replace_env_vars or remove_all_env_vars or cpu or memory or\ @@ -1983,7 +1986,16 @@ def containerapp_up(cmd, image = image.replace(':', '') if not location: - location = "eastus2" # check user's default location? find least populated server? + location = _get_default_containerapps_location(cmd) + + # Open dockerfile and check for EXPOSE + dockerfile_location = source + '/' + dockerfile + with open(dockerfile_location, 'r') as fh: + for line in fh: + if "EXPOSE" in line: + if not target_port and not ingress: + target_port = line.replace('\n', '').split(" ")[1] + ingress = "external" custom_rg_name = None # user passes bad resource group name, we create it for them From dc8953fe2561e6e6f6b975810412e4c76ee10885 Mon Sep 17 00:00:00 2001 From: StrawnSC Date: Fri, 8 Apr 2022 15:10:44 -0700 Subject: [PATCH 124/158] fix windows arrow keys after exit --- .../azext_containerapp/_ssh_utils.py | 21 +++++++++++++------ src/containerapp/azext_containerapp/_utils.py | 5 +++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index 3085ebf5400..f359697d9ce 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -6,7 +6,6 @@ import os import sys import time -import platform import threading import requests import websocket @@ -16,11 +15,13 @@ from azure.cli.core.commands.client_factory import get_subscription_id from ._clients import ContainerAppClient -from ._utils import safe_get +from ._utils import safe_get, is_platform_windows -if platform.system() == "Windows": - import msvcrt # pylint: disable=import-error - from azure.cli.command_modules.container._vt_helper import enable_vt_mode # pylint: disable=ungrouped-imports +# pylint: disable=import-error,ungrouped-imports +if is_platform_windows(): + import msvcrt + from azure.cli.command_modules.container._vt_helper import (enable_vt_mode, _get_conout_mode, + _set_conout_mode, _get_conin_mode, _set_conin_mode) logger = get_logger(__name__) @@ -56,6 +57,11 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found self._socket.connect(self._url) self.is_connected = True + self.windows_conout_mode = None + self._windows_conin_mode = None + if is_platform_windows(): + self._windows_conout_mode = _get_conout_mode() + self._windows_conin_mode = _get_conin_mode() @classmethod def _remove_token(cls, url): @@ -78,6 +84,9 @@ def disconnect(self): logger.warning("Disconnecting...") self.is_connected = False self._socket.close() + if self._windows_conout_mode and self._windows_conin_mode: + _set_conout_mode(self._windows_conout_mode) + _set_conin_mode(self._windows_conin_mode) def send(self, *args, **kwargs): return self._socket.send(*args, **kwargs) @@ -161,7 +170,7 @@ def ping_container_app(app): def get_stdin_writer(connection: WebSocketConnection): - if platform.system() != "Windows": + if not is_platform_windows(): import tty tty.setcbreak(sys.stdin.fileno()) # needed to prevent printing arrow key characters writer = threading.Thread(target=_send_stdin, args=(connection, _getch_unix)) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index be6a1b56036..b8247c9b807 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument +import platform from urllib.parse import urlparse from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) @@ -593,3 +594,7 @@ def safe_get(model, *keys): for k in keys[:-1]: model = model.get(k, {}) return model.get(keys[-1]) + + +def is_platform_windows(): + return platform.system() == "Windows" From fe28fce79425c7f85c6102b788c5a7753a6a1ce0 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Fri, 8 Apr 2022 15:52:36 -0700 Subject: [PATCH 125/158] fix typo, add logstream test, remove token from logstream output --- .../azext_containerapp/_ssh_utils.py | 16 ++++++++-------- src/containerapp/azext_containerapp/custom.py | 4 ++-- .../tests/latest/test_containerapp_scenario.py | 13 +++++++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index f359697d9ce..42b6a9ea8eb 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -52,23 +52,17 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, self._url = self._get_url(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, replica=replica, container=container, startup_command=startup_command) self._socket = websocket.WebSocket(enable_multithread=True) - logger.warning("Attempting to connect to %s", self._remove_token(self._url)) + logger.warning("Attempting to connect to %s", remove_token(self._url)) # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found self._socket.connect(self._url) self.is_connected = True - self.windows_conout_mode = None + self._windows_conout_mode = None self._windows_conin_mode = None if is_platform_windows(): self._windows_conout_mode = _get_conout_mode() self._windows_conin_mode = _get_conin_mode() - @classmethod - def _remove_token(cls, url): - if "?token=" in url: - return url[:url.index("?token=")] - return url - @classmethod def _get_url(cls, cmd, resource_group_name, name, revision, replica, container, startup_command): sub = get_subscription_id(cmd.cli_ctx) @@ -95,6 +89,12 @@ def recv(self, *args, **kwargs): return self._socket.recv(*args, **kwargs) +def remove_token(url): + if "?token=" in url: + return url[:url.index("?token=")] + return url + + def _decode_and_output_to_terminal(connection: WebSocketConnection, response, encodings): for i, encoding in enumerate(encodings): try: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 6920f06642f..42967c4a368 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -53,7 +53,7 @@ _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) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, - SSH_BACKUP_ENCODING) + SSH_BACKUP_ENCODING, remove_token) logger = get_logger(__name__) @@ -2001,7 +2001,7 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev url = (f"{base_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" f"/revisions/{revision}/replicas/{replica}/containers/{container}/logstream?token={token}") - logger.warning("connecting to : %s", url) + logger.warning("connecting to : %s", remove_token(url)) request_params = {"follow": str(follow).lower(), "output": output_format, "tailLines": tail} resp = requests.get(url, timeout=None, stream=True, params=request_params) 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 16e9e7a961e..f90a9b69506 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -6,6 +6,7 @@ import os import platform from unittest import mock +from azext_containerapp.custom import containerapp_ssh from azure.cli.testsdk.reverse_dependency import get_dummy_cli from azure.cli.testsdk.scenario_tests import AllowLargeResponse @@ -98,3 +99,15 @@ class Namespace: pass container=namespace.container, revision=namespace.revision, replica=namespace.replica, startup_command="sh") for line in expected_output: self.assertIn(line, expected_output) + + + @live_only + @ResourceGroupPreparer(location="centraluseuap") + def test_containerapp_logstream(self, resource_group): + containerapp_name = self.create_random_name(prefix='capp', length=24) + env_name = self.create_random_name(prefix='env', length=24) + + self.cmd(f'containerapp env create -g {resource_group} -n {env_name}') + self.cmd(f'containerapp create -g {resource_group} -n {containerapp_name} --environment {env_name} --min-replicas 1 --ingress external --target-port 80') + + self.cmd(f'containerapp log tail -n {containerapp_name} -g {resource_group}') From 51a868f52277fdc9213d8456de52c567263e1da5 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 11 Apr 2022 13:18:02 -0400 Subject: [PATCH 126/158] Removed print statement. --- src/containerapp/azext_containerapp/custom.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 3f93e1295b0..edd20a94989 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2151,6 +2151,5 @@ def containerapp_up(cmd, dry_run_str += '"log_analytics_workspace_name" : "{}"\n'.format(log_analytics_workspace_name) dry_run_str += '}' - print(dry_run_str) return json.loads(dry_run_str) From 429fbaaf0939951b97456f023f2145a2b45cff2f Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 11 Apr 2022 13:37:46 -0400 Subject: [PATCH 127/158] Updated dockerfile expose automatic ingress feature. --- src/containerapp/azext_containerapp/custom.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index edd20a94989..707773457ca 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1989,13 +1989,15 @@ def containerapp_up(cmd, location = _get_default_containerapps_location(cmd) # Open dockerfile and check for EXPOSE - dockerfile_location = source + '/' + dockerfile - with open(dockerfile_location, 'r') as fh: - for line in fh: - if "EXPOSE" in line: - if not target_port and not ingress: - target_port = line.replace('\n', '').split(" ")[1] - ingress = "external" + if source: + dockerfile_location = source + '/' + dockerfile + with open(dockerfile_location, 'r') as fh: + for line in fh: + if "EXPOSE" in line: + if not target_port and not ingress: + target_port = line.replace('\n', '').split(" ")[1] + ingress = "external" + logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) custom_rg_name = None # user passes bad resource group name, we create it for them From 402f6df253a8bae13d710e0fbaabf402ffa67a71 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Mon, 11 Apr 2022 17:57:16 -0400 Subject: [PATCH 128/158] Removed dry run str, added dry run obj instead. --- src/containerapp/azext_containerapp/custom.py | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 707773457ca..12812a82525 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2131,27 +2131,25 @@ def containerapp_up(cmd, fqdn = "" - dry_run_str = """ { - "name" : "%s (%s)", - "resourcegroup" : "%s (%s)", - "location" : "%s", - "environment" : "%s (%s)", - "registry": "%s (%s)", - "image" : "%s", - "src_path" : "%s", - """ % (name, new_ca, resource_group_name, new_rg, location, env_name, new_managed_env, registry_server, new_cr, image, _src_path_escaped) - + dry_run = { + "location" : location, + "registry": registry_server, + "image": image, + "src_path": _src_path_escaped + } + dry_run["name"] = "{} ({})".format(name, new_ca) + dry_run["resourcegroup"] = "{} ({})".format(resource_group_name, new_rg) + dry_run["environment"] = "{} ({})".format(env_name, new_managed_env) + if registry_server: + dry_run["registry"] = "{} ({})".format(registry_server, new_cr) + if containerapp_def: r = containerapp_def if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: fqdn = r["properties"]["configuration"]["ingress"]["fqdn"] if len(fqdn) > 0: - dry_run_str += '"fqdn" : "{}",\n'.format(fqdn) - + dry_run["fqdn"] = fqdn if len(log_analytics_workspace_name) > 0: - dry_run_str += '"log_analytics_workspace_name" : "{}"\n'.format(log_analytics_workspace_name) - - dry_run_str += '}' - - return json.loads(dry_run_str) + dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name + return dry_run From 337a45480e6c9cf40dd0a534676f1df9260ec01e Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 12 Apr 2022 09:53:04 -0700 Subject: [PATCH 129/158] use bearer auth; fix --command bug --- src/containerapp/azext_containerapp/_params.py | 2 +- src/containerapp/azext_containerapp/_ssh_utils.py | 15 ++++++++------- src/containerapp/azext_containerapp/custom.py | 10 +++++++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 91464547fbc..808ede2986f 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('container', help="The name of the container to ssh into") c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") c.argument('revision', help="The name of the container app revision to ssh into. Defaults to the latest revision.") - c.argument('startup_command', options_list=["--command"], default="sh", help="The startup command (bash, zsh, sh, etc.).") + c.argument('startup_command', options_list=["--command"], help="The startup command (bash, zsh, sh, etc.).") c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index 42b6a9ea8eb..a0feeeed0b5 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -49,13 +49,16 @@ class WebSocketConnection: def __init__(self, cmd, resource_group_name, name, revision, replica, container, startup_command): + token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) + self._token = token_response["properties"]["token"] + self._logstream_endpoint = token_response["properties"]["logStreamEndpoint"] self._url = self._get_url(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, replica=replica, container=container, startup_command=startup_command) self._socket = websocket.WebSocket(enable_multithread=True) logger.warning("Attempting to connect to %s", remove_token(self._url)) # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found - self._socket.connect(self._url) + self._socket.connect(self._url, header=[f"Authorization: Bearer {self._token}"]) self.is_connected = True self._windows_conout_mode = None self._windows_conin_mode = None @@ -63,13 +66,11 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, self._windows_conout_mode = _get_conout_mode() self._windows_conin_mode = _get_conin_mode() - @classmethod - def _get_url(cls, cmd, resource_group_name, name, revision, replica, container, startup_command): + def _get_url(self, cmd, resource_group_name, name, revision, replica, container, startup_command): sub = get_subscription_id(cmd.cli_ctx) - token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) - token = token_response["properties"]["token"] - logstream_endpoint = token_response["properties"]["logStreamEndpoint"] - proxy_api_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")].replace("https://", "") + base_url = self._logstream_endpoint + proxy_api_url = base_url[:base_url.index("/subscriptions/")].replace("https://", "") + token = self._token return (f"wss://{proxy_api_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}?token={token}") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 42967c4a368..448dd6fbf55 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1962,7 +1962,10 @@ def get_replica(cmd, resource_group_name, name, replica, revision=None): # TODO token will be read from header at some point -- a PR has apparently been opened for this -def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command=None): +def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command="sh"): + if isinstance(startup_command, list): + startup_command = startup_command[0] # CLI seems a little buggy when calling a param "--command" + conn = WebSocketConnection(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, replica=replica, container=container, startup_command=startup_command) @@ -2003,7 +2006,8 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev logger.warning("connecting to : %s", remove_token(url)) request_params = {"follow": str(follow).lower(), "output": output_format, "tailLines": tail} - resp = requests.get(url, timeout=None, stream=True, params=request_params) + headers = {"Authorization": f"Bearer {token}"} + resp = requests.get(url, timeout=None, stream=True, params=request_params, headers=headers) if not resp.ok: ValidationError(f"Got bad status from the logstream API: {resp.status_code}") @@ -2013,4 +2017,4 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev logger.info("received raw log line: %s", line) # these .replaces are needed to display color/quotations properly # for some reason the API returns garbled unicode special characters (may need to add more in the future) - print(line.decode("utf-8").replace("\\u0022", "\u0022").replace("\\u001B", "\u001B")) + print(line.decode("utf-8").replace("\\u0022", "\u0022").replace("\\u001B", "\u001B").replace("\\u002B", "\u002B").replace("\\u0027", "\u0027")) From 74fe23be827b293eabe2fb4bf5c9c2a7c61dc6c2 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 12 Apr 2022 11:16:40 -0700 Subject: [PATCH 130/158] add handling for smooth transition to new URL path --- .../azext_containerapp/_ssh_utils.py | 33 +++++++++++++++++-- src/containerapp/azext_containerapp/custom.py | 1 + 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index a0feeeed0b5..cb76294c1ac 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -7,6 +7,7 @@ import sys import time import threading +import urllib import requests import websocket @@ -49,6 +50,8 @@ class WebSocketConnection: def __init__(self, cmd, resource_group_name, name, revision, replica, container, startup_command): + from websocket._exceptions import WebSocketBadStatusException + token_response = ContainerAppClient.get_auth_token(cmd, resource_group_name, name) self._token = token_response["properties"]["token"] self._logstream_endpoint = token_response["properties"]["logStreamEndpoint"] @@ -57,8 +60,16 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, self._socket = websocket.WebSocket(enable_multithread=True) logger.warning("Attempting to connect to %s", remove_token(self._url)) - # TODO maybe catch websocket._exceptions.WebSocketBadStatusException: Handshake status 404 Not Found - self._socket.connect(self._url, header=[f"Authorization: Bearer {self._token}"]) + # TODO may be worth including some retry policy here + try: + self._socket.connect(self._url, header=[f"Authorization: Bearer {self._token}"]) + except WebSocketBadStatusException: + logger.info("Caught WebSocketBadStatusException") + url = self._get_url_no_command(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, + replica=replica, container=container, startup_command=startup_command) + logger.warning("Attempting to connect to %s", remove_token(url)) + self._socket.connect(url, header=[f"Authorization: Bearer {self._token}"]) + self.is_connected = True self._windows_conout_mode = None self._windows_conin_mode = None @@ -66,14 +77,30 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, self._windows_conout_mode = _get_conout_mode() self._windows_conin_mode = _get_conin_mode() + # TODO remove once encorporated into _get_url + def _get_url_no_command(self, cmd, resource_group_name, name, revision, replica, container, startup_command): + sub = get_subscription_id(cmd.cli_ctx) + base_url = self._logstream_endpoint + proxy_api_url = base_url[:base_url.index("/subscriptions/")].replace("https://", "") + token = self._token + encoded_cmd = urllib.parse.quote_plus(startup_command) + + return (f"wss://{proxy_api_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" + f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec" + f"?token={token}&command={encoded_cmd}") + def _get_url(self, cmd, resource_group_name, name, revision, replica, container, startup_command): sub = get_subscription_id(cmd.cli_ctx) base_url = self._logstream_endpoint proxy_api_url = base_url[:base_url.index("/subscriptions/")].replace("https://", "") token = self._token + encoded_cmd = urllib.parse.quote_plus(startup_command) + # TODO remove token from URL once token is read from header + # TODO remove startup command from path return (f"wss://{proxy_api_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" - f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}?token={token}") + f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}" + f"?token={token}&command={encoded_cmd}") def disconnect(self): logger.warning("Disconnecting...") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 448dd6fbf55..f292974cd95 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2001,6 +2001,7 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev logstream_endpoint = token_response["properties"]["logStreamEndpoint"] base_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")] + # TODO remove token from URL once token is read from header url = (f"{base_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" f"/revisions/{revision}/replicas/{replica}/containers/{container}/logstream?token={token}") From f53be71e2c9262155b997ce78972731375ef9885 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Tue, 12 Apr 2022 15:16:52 -0400 Subject: [PATCH 131/158] Fixed merge conflict. --- src/containerapp/HISTORY.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 0c5d0a628d9..2abbd24f08a 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,13 +3,10 @@ Release History =============== -<<<<<<< HEAD -======= 0.3.0 ++++++ * Subgroup commands for managed identities: az containerapp identity ->>>>>>> main 0.1.0 ++++++ * Initial release for Container App support with Microsoft.App RP. From f114468d575b583126cbc11bb7d8ef2c665e4ff0 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 12 Apr 2022 16:37:14 -0700 Subject: [PATCH 132/158] fix merge conflicts --- .../azext_containerapp/_acr_run_polling.py | 112 ++++++++ .../azext_containerapp/_params.py | 19 ++ src/containerapp/azext_containerapp/_utils.py | 221 ++++++++++++++- .../azext_containerapp/commands.py | 1 + src/containerapp/azext_containerapp/custom.py | 251 ++++++++++++++++-- 5 files changed, 580 insertions(+), 24 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_acr_run_polling.py diff --git a/src/containerapp/azext_containerapp/_acr_run_polling.py b/src/containerapp/azext_containerapp/_acr_run_polling.py new file mode 100644 index 00000000000..1a71a87c99a --- /dev/null +++ b/src/containerapp/azext_containerapp/_acr_run_polling.py @@ -0,0 +1,112 @@ +# -------------------------------------------------------------------------------------------- +# 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 + +import time + +from msrest import Deserializer +from msrestazure.azure_exceptions import CloudError +from azure.cli.core.profiles import ResourceType +from azure.cli.command_modules.acr._constants import get_acr_task_models +from azure.core.polling import PollingMethod, LROPoller + + +def get_run_with_polling(cmd, + client, + run_id, + registry_name, + resource_group_name): + deserializer = Deserializer( + {k: v for k, v in get_acr_task_models(cmd).__dict__.items() if isinstance(v, type)}) + + def deserialize_run(response): + return deserializer('Run', response) + + return LROPoller( + client=client, + initial_response=client.get( + resource_group_name, registry_name, run_id, cls=lambda x, y, z: x), + deserialization_callback=deserialize_run, + polling_method=RunPolling( + cmd=cmd, + registry_name=registry_name, + run_id=run_id + )) + + +class RunPolling(PollingMethod): # pylint: disable=too-many-instance-attributes + + def __init__(self, cmd, registry_name, run_id, timeout=30): + self._cmd = cmd + self._registry_name = registry_name + self._run_id = run_id + self._timeout = timeout + self._client = None + self._response = None # Will hold latest received response + self._url = None # The URL used to get the run + self._deserialize = None # The deserializer for Run + self.operation_status = "" + self.operation_result = None + + def initialize(self, client, initial_response, deserialization_callback): + self._client = client._client # pylint: disable=protected-access + self._response = initial_response + self._url = initial_response.http_request.url + self._deserialize = deserialization_callback + + self._set_operation_status(initial_response) + + def run(self): + while not self.finished(): + time.sleep(self._timeout) + self._update_status() + + if self.operation_status not in get_succeeded_run_status(self._cmd): + from knack.util import CLIError + raise CLIError("The run with ID '{}' finished with unsuccessful status '{}'. " + "Show run details by 'az acr task show-run -r {} --run-id {}'. " + "Show run logs by 'az acr task logs -r {} --run-id {}'.".format( + self._run_id, + self.operation_status, + self._registry_name, + self._run_id, + self._registry_name, + self._run_id + )) + + def status(self): + return self.operation_status + + def finished(self): + return self.operation_status in get_finished_run_status(self._cmd) + + def resource(self): + return self.operation_result + + def _set_operation_status(self, response): + if response.http_response.status_code == 200: + self.operation_result = self._deserialize(response) + self.operation_status = self.operation_result.status + return + raise CloudError(response) + + def _update_status(self): + self._response = self._client._pipeline.run( # pylint: disable=protected-access + self._client.get(self._url), stream=False) + self._set_operation_status(self._response) + + +def get_succeeded_run_status(cmd): + RunStatus = cmd.get_models('RunStatus', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='task_runs') + return [RunStatus.succeeded.value] + + +def get_finished_run_status(cmd): + RunStatus = cmd.get_models('RunStatus', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='task_runs') + return [RunStatus.succeeded.value, + RunStatus.failed.value, + RunStatus.canceled.value, + RunStatus.error.value, + RunStatus.timeout.value] diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index f2ab4ff2cb9..f419b3f28b9 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -24,6 +24,7 @@ def load_arguments(self, _): 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)) + c.ignore('disable_warnings') with self.argument_context('containerapp') as c: c.argument('tags', arg_type=tags_type) @@ -209,3 +210,21 @@ def load_arguments(self, _): with self.argument_context('containerapp revision list') as c: c.argument('name', id_part=None) + + with self.argument_context('containerapp up') as c: + c.argument('resource_group_name', configured_default='resource_group_name') + c.argument('location', configured_default='location') + c.argument('name', configured_default='name', id_part=None) + c.argument('managed_env', configured_default='managed_env') + c.argument('registry_server', configured_default='registry_server') + c.argument('quiet', help="Disable logs output from ACR build when using --source.") + c.argument('dockerfile', help="Name of the dockerfile.") + c.argument('dryrun', help="Show summary of the operation instead of executing it.") + + with self.argument_context('containerapp up', arg_group='Log Analytics (Environment)') as c: + c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Name or resource ID of the Log Analytics workspace to send diagnostics logs to. You can use \"az monitor log-analytics workspace create\" to create one. Extra billing may apply.') + c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log Analytics workspace key to configure your Log Analytics workspace. You can use \"az monitor log-analytics workspace get-shared-keys\" to retrieve the key.') + c.ignore('no_wait') + + with self.argument_context('containerapp', arg_group='Container') as c: + c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index b8247c9b807..773d2155b30 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -2,12 +2,11 @@ # 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 +# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals import platform from urllib.parse import urlparse -from azure.cli.command_modules.appservice.custom import (_get_acr_cred) -from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError) +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError, ResourceNotFoundError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from msrestazure.tools import parse_resource_id @@ -133,7 +132,7 @@ def _update_revision_env_secretrefs(containers, name): 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): +def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_server, registry_pass, update_existing_secret=False, disable_warnings=False): if registry_pass.startswith("secretref:"): # If user passed in registry password using a secret @@ -163,7 +162,8 @@ def store_as_secret_and_return_secret_ref(secrets_list, registry_user, registry_ 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 + if not disable_warnings: + 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 @@ -206,6 +206,27 @@ def _get_default_log_analytics_location(cmd): return default_location +def _get_default_containerapps_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.App"), 'resource_types', []) + res_locations = [] + for res in resource_types: + if res and getattr(res, 'resource_type', "") == "workspaces": + res_locations = getattr(res, 'locations', []) + + if len(res_locations) > 0: + location = res_locations[0].lower().replace(" ", "").replace("(", "").replace(")", "") + if location: + return location + + except Exception: # pylint: disable=broad-except + return default_location + return default_location + + # Generate random 4 character string def _new_tiny_guid(): import random @@ -233,6 +254,15 @@ def _generate_log_analytics_workspace_name(resource_group_name): return name +def _get_log_analytics_workspace_name(cmd, logs_customer_id, resource_group_name): + log_analytics_client = log_analytics_client_factory(cmd.cli_ctx) + logs_list = log_analytics_client.list_by_resource_group(resource_group_name) + for log in logs_list: + if log.customer_id.lower() == logs_customer_id.lower(): + return log.name + return ResourceNotFoundError("Cannot find Log Analytics workspace with customer ID {}".format(logs_customer_id)) + + 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.") @@ -563,16 +593,16 @@ def _get_app_from_revision(revision): return revision -def _infer_acr_credentials(cmd, registry_server): +def _infer_acr_credentials(cmd, registry_server, disable_warnings=False): # If registry is Azure Container Registry, we can try inferring credentials if '.azurecr.io' not in registry_server: raise RequiredArgumentMissingError('Registry username and password are required if not using Azure Container Registry.') - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up credentials...') + not disable_warnings and 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) + registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) # pylint: disable=unused-variable return (registry_user, registry_pass) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry {}. Please provide the registry username and password'.format(registry_name)) from ex @@ -598,3 +628,178 @@ def safe_get(model, *keys): def is_platform_windows(): return platform.system() == "Windows" + + +def get_randomized_name(prefix, name=None, initial="rg"): + from random import randint + default = "{}_{}_{:04}".format(prefix, initial, randint(0, 9999)) + if name is not None: + return name + return default + + +def _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server): + from azure.cli.core.util import ConfiguredDefaultSetter + with ConfiguredDefaultSetter(cmd.cli_ctx.config, True): + logger.warning("Setting 'az containerapp up' default arguments for current directory. " + "Manage defaults with 'az configure --scope local'") + + cmd.cli_ctx.config.set_value('defaults', 'resource_group_name', resource_group_name) + logger.warning("--resource-group/-g default: %s", resource_group_name) + + cmd.cli_ctx.config.set_value('defaults', 'location', location) + logger.warning("--location/-l default: %s", location) + + cmd.cli_ctx.config.set_value('defaults', 'name', name) + logger.warning("--name/-n default: %s", name) + + # cmd.cli_ctx.config.set_value('defaults', 'managed_env', managed_env) + # logger.warning("--environment default: %s", managed_env) + + cmd.cli_ctx.config.set_value('defaults', 'registry_server', registry_server) + logger.warning("--registry-server default: %s", registry_server) + + +def get_profile_username(): + from azure.cli.core._profile import Profile + user = Profile().get_current_account_user() + user = user.split('@', 1)[0] + if len(user.split('#', 1)) > 1: # on cloudShell user is in format live.com#user@domain.com + user = user.split('#', 1)[1] + return user + + +def create_resource_group(cmd, rg_name, location): + from azure.cli.core.profiles import ResourceType, get_sdk + rcf = _resource_client_factory(cmd.cli_ctx) + resource_group = get_sdk(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, 'ResourceGroup', mod='models') + rg_params = resource_group(location=location) + return rcf.resource_groups.create_or_update(rg_name, rg_params) + + +def get_resource_group(cmd, rg_name): + rcf = _resource_client_factory(cmd.cli_ctx) + return rcf.resource_groups.get(rg_name) + + +def _resource_client_factory(cli_ctx, **_): + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.cli.core.profiles import ResourceType + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES) + + +def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfile="Dockerfile", quiet=False): + import os + import uuid + import tempfile + from azure.cli.command_modules.acr._archive_utils import upload_source_code + from azure.cli.command_modules.acr._stream_utils import stream_logs + from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks + from azure.cli.core.commands import LongRunningOperation + + # client_registries = get_acr_service_client(cmd.cli_ctx).registries + client_registries = cf_acr_registries_tasks(cmd.cli_ctx) + + if not os.path.isdir(src_dir): + raise ValidationError("Source directory should be a local directory path.") + + docker_file_path = os.path.join(src_dir, dockerfile) + if not os.path.isfile(docker_file_path): + raise ValidationError("Unable to find '{}'.".format(docker_file_path)) + + # NOTE: os.path.basename is unable to parse "\" in the file path + original_docker_file_name = os.path.basename(docker_file_path.replace("\\", "/")) + docker_file_in_tar = '{}_{}'.format(uuid.uuid4().hex, original_docker_file_name) + tar_file_path = os.path.join(tempfile.gettempdir(), 'build_archive_{}.tar.gz'.format(uuid.uuid4().hex)) + + source_location = upload_source_code(cmd, client_registries, registry_name, registry_rg, src_dir, tar_file_path, docker_file_path, docker_file_in_tar) + + # For local source, the docker file is added separately into tar as the new file name (docker_file_in_tar) + # So we need to update the docker_file_path + docker_file_path = docker_file_in_tar + + from azure.cli.core.profiles import ResourceType + OS, Architecture = cmd.get_models('OS', 'Architecture', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') + # Default platform values + platform_os = OS.linux.value + platform_arch = Architecture.amd64.value + platform_variant = None + + DockerBuildRequest, PlatformProperties = cmd.get_models('DockerBuildRequest', 'PlatformProperties', + resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group='runs') + docker_build_request = DockerBuildRequest( + image_names=[img_name], + is_push_enabled=True, + source_location=source_location, + platform=PlatformProperties( + os=platform_os, + architecture=platform_arch, + variant=platform_variant + ), + docker_file_path=docker_file_path, + timeout=None, + arguments=[]) + + queued_build = LongRunningOperation(cmd.cli_ctx)(client_registries.begin_schedule_run( + resource_group_name=registry_rg, + registry_name=registry_name, + run_request=docker_build_request)) + + run_id = queued_build.run_id + logger.warning("Queued a build with ID: %s", run_id) + not quiet and logger.warning("Waiting for agent...") + + from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) + from ._acr_run_polling import get_run_with_polling + client_runs = cf_acr_runs(cmd.cli_ctx) + + if quiet: + lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) + acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) + logger.warning("Build {}.".format(acr.status.lower())) # pylint: disable=logging-format-interpolation + if acr.status.lower() != "succeeded": + raise CLIInternalError("ACR build {}.".format(acr.status.lower())) + return acr + + return stream_logs(cmd, client_runs, run_id, registry_name, registry_rg, None, False, True) + + +def _get_acr_cred(cli_ctx, registry_name): + from azure.mgmt.containerregistry import ContainerRegistryManagementClient + from azure.cli.core.commands.parameters import get_resources_in_subscription + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + client = get_mgmt_service_client(cli_ctx, ContainerRegistryManagementClient).registries + + result = get_resources_in_subscription(cli_ctx, 'Microsoft.ContainerRegistry/registries') + result = [item for item in result if item.name.lower() == registry_name] + if not result or len(result) > 1: + raise ResourceNotFoundError("No resource or more than one were found with name '{}'.".format(registry_name)) + resource_group_name = parse_resource_id(result[0].id)['resource_group'] + + registry = client.get(resource_group_name, registry_name) + + if registry.admin_user_enabled: # pylint: disable=no-member + cred = client.list_credentials(resource_group_name, registry_name) + return cred.username, cred.passwords[0].value, resource_group_name + raise ResourceNotFoundError("Failed to retrieve container registry credentials. Please either provide the " + "credentials or run 'az acr update -n {} --admin-enabled true' to enable " + "admin first.".format(registry_name)) + + +def create_new_acr(cmd, registry_name, resource_group_name, location=None, sku="Basic"): + # from azure.cli.command_modules.acr.custom import acr_create + from azure.cli.command_modules.acr._client_factory import cf_acr_registries + from azure.cli.core.profiles import ResourceType + from azure.cli.core.commands import LongRunningOperation + + client = cf_acr_registries(cmd.cli_ctx) + # return acr_create(cmd, client, registry_name, resource_group_name, sku, location) + + Registry, Sku = cmd.get_models('Registry', 'Sku', resource_type=ResourceType.MGMT_CONTAINERREGISTRY, operation_group="registries") + registry = Registry(location=location, sku=Sku(name=sku), admin_user_enabled=True, + zone_redundancy=None, tags=None) + + lro_poller = client.begin_create(resource_group_name, registry_name, registry) + acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) + return acr diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 74c2703d8ad..10088756c22 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -51,6 +51,7 @@ def load_command_table(self, _): 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()) g.custom_command('exec', 'containerapp_ssh', validator=validate_ssh) + g.custom_command('up', 'containerapp_up', supports_no_wait=True, exception_handler=ex_handler_factory()) with self.command_group('containerapp replica', is_preview=True) as g: g.custom_show_command('show', 'get_replica') # TODO implement the table transformer diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1c6f456d93d..405dd058cf0 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.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, 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 +# 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, expression-not-assigned, unbalanced-tuple-unpacking import threading import sys @@ -10,7 +10,6 @@ from urllib.parse import urlparse import requests -from azure.cli.command_modules.appservice.custom import (_get_acr_cred) from azure.cli.core.azclierror import ( RequiredArgumentMissingError, ValidationError, @@ -51,10 +50,14 @@ _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, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, + get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, + _get_default_containerapps_location) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING, remove_token) + logger = get_logger(__name__) @@ -300,6 +303,7 @@ def create_containerapp(cmd, tags=None, no_wait=False, system_assigned=False, + disable_warnings=False, user_assigned=None): _validate_subscription_registered(cmd, "Microsoft.App") @@ -308,7 +312,7 @@ def create_containerapp(cmd, 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') + not disable_warnings and logger.warning('Additional flags were passed along with --yaml. These flags will be ignored, and the configuration defined in the yaml will be used instead') return create_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait) if not image: @@ -358,14 +362,14 @@ def create_containerapp(cmd, # Infer credentials if not supplied and its azurecr if registry_user is None or registry_pass is None: - registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server) + registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server, disable_warnings) registries_def["server"] = registry_server registries_def["username"] = registry_user if secrets_def is None: secrets_def = [] - registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass) + registries_def["passwordSecretRef"] = store_as_secret_and_return_secret_ref(secrets_def, registry_user, registry_server, registry_pass, disable_warnings=disable_warnings) dapr_def = None if dapr_enabled: @@ -451,12 +455,12 @@ def create_containerapp(cmd, 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)) + not disable_warnings and 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"])) + not disable_warnings and 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") + not disable_warnings and logger.warning("\nContainer app created. To access it over HTTPS, enable ingress: az containerapp ingress enable --help\n") return r except Exception as e: @@ -746,6 +750,7 @@ def create_managed_environment(cmd, platform_reserved_dns_ip=None, internal_only=False, tags=None, + disable_warnings=False, no_wait=False): location = location or _get_location_from_resource_group(cmd.cli_ctx, resource_group_name) @@ -800,9 +805,9 @@ 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 env show -n {} -g {}`'.format(name, resource_group_name)) + not disable_warnings and 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") + not disable_warnings and logger.warning("\nContainer Apps environment created. To deploy a container app, use: az containerapp create --help\n") return r except Exception as e: @@ -1355,7 +1360,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="auto", allow_insecure=False, disable_warnings=False, no_wait=False): # pylint: disable=redefined-builtin _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1389,7 +1394,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"])) + not disable_warnings and 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) @@ -1511,7 +1516,7 @@ def list_registry(cmd, name, resource_group_name): 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): +def set_registry(cmd, name, resource_group_name, server, username=None, password=None, disable_warnings=False, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") containerapp_def = None @@ -1537,7 +1542,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, 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...') + not disable_warnings and 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] @@ -1550,7 +1555,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password updating_existing_registry = False for r in registries_def: if r['server'].lower() == server.lower(): - logger.warning("Updating existing registry.") + not disable_warnings and logger.warning("Updating existing registry.") updating_existing_registry = True if username: r["username"] = username @@ -1965,3 +1970,217 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev # these .replaces are needed to display color/quotations properly # for some reason the API returns garbled unicode special characters (may need to add more in the future) print(line.decode("utf-8").replace("\\u0022", "\u0022").replace("\\u001B", "\u001B").replace("\\u002B", "\u002B").replace("\\u0027", "\u0027")) + + +def containerapp_up(cmd, + name=None, + resource_group_name=None, + managed_env=None, + location=None, + registry_server=None, + image=None, + source=None, + dockerfile="Dockerfile", + # compose=None, + ingress=None, + target_port=None, + registry_user=None, + registry_pass=None, + env_vars=None, + dryrun=False, + logs_customer_id=None, + logs_key=None, + quiet=False): + import os + import json + src_dir = os.getcwd() + _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) + + new_rg = "Existing" + new_managed_env = "Existing" + new_ca = "New" + new_cr = "Existing" + + if source is None and image is None: + raise RequiredArgumentMissingError("You must specify either --source or --image.") + + if not name: + if image: + name = image.split('/')[-1].split(':')[0].lower() + if source: + temp = source[1:] if source[0] == '.' else source # replace first . if it exists + name = temp.split('/')[-1].lower() # isolate folder name + if len(name) == 0: + name = _src_path_escaped.rsplit('\\', maxsplit=1)[-1] + + if source and image: + image = image.replace(':', '') + + if not location: + location = _get_default_containerapps_location(cmd) + + # Open dockerfile and check for EXPOSE + if source: + dockerfile_location = source + '/' + dockerfile + with open(dockerfile_location, 'r') as fh: + for line in fh: + if "EXPOSE" in line: + if not target_port and not ingress: + target_port = line.replace('\n', '').split(" ")[1] + ingress = "external" + logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + + custom_rg_name = None + # user passes bad resource group name, we create it for them + if resource_group_name: + try: + get_resource_group(cmd, resource_group_name) + except: + custom_rg_name = resource_group_name + resource_group_name = None + + # if custom_rg_name, that means rg doesn't exist no need to look for CA + if not resource_group_name and not custom_rg_name: + try: + rg_found = False + containerapps = list_containerapp(cmd) + for containerapp in containerapps: + if containerapp["name"].lower() == name.lower(): + if rg_found: + raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) + # could also just do resource_group_name = None here and create a new one, ask Anthony + # break + if containerapp["id"][0] != '/': + containerapp["id"] = '/' + containerapp["id"] + rg_found = True + resource_group_name = containerapp["id"].split('/')[4] + except: + pass + + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + env_name = "" if not managed_env else managed_env.split('/')[6] + if not containerapp_def: + if not resource_group_name: + new_rg = "New" + user = get_profile_username() + rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name + if not dryrun: + logger.warning("Creating new resource group {}".format(rg_name)) + create_resource_group(cmd, rg_name, location) + resource_group_name = rg_name + if not managed_env: + new_managed_env = "New" + env_name = "{}-env".format(name).replace("_", "-") + if not dryrun: + logger.warning("Creating new managed environment {}".format(env_name)) + managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + else: + managed_env = env_name + else: + new_ca = "Existing" + location = containerapp_def["location"] + managed_env = containerapp_def["properties"]["managedEnvironmentId"] + env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] + if logs_customer_id and logs_key: + if not dryrun: + managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + + if image is not None and "azurecr.io" in image and not dryrun: + if registry_user is None or registry_pass is None: + # If registry is Azure Container Registry, we can try inferring credentials + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + registry_server = image.split('/')[0] + parsed = urlparse(image) + 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) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + + if source is not None: + if containerapp_def: + if "registries" in containerapp_def["properties"]["configuration"] and len(containerapp_def["properties"]["configuration"]["registries"]) == 1: + registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] + registry_name = "" + registry_rg = "" + if registry_server: + if "azurecr.io" not in registry_server: + raise ValidationError("Cannot supply non-Azure registry when using --source.") + if not dryrun and (registry_user is None or registry_pass is None): + # If registry is Azure Container Registry, we can try inferring credentials + logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + else: + new_cr = "New" + registry_rg = resource_group_name + user = get_profile_username() + registry_name = "{}acr".format(name) + registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") + if not dryrun: + logger.warning("Creating new acr {}".format(registry_name)) + registry_def = create_new_acr(cmd, registry_name, registry_rg, location) + registry_server = registry_def.login_server + else: + registry_server = registry_name + ".azurecr.io" + + image_name = image if image is not None else name + from datetime import datetime + now = datetime.now() + image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + image = registry_server + '/' + image_name + if not dryrun: + queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) + _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) + + log_analytics_workspace_name = "" + env_def = None + try: + env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) + except: + pass + if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": + env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] + log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) + + containerapp_def = None + + if dryrun: + logger.warning("Containerapp will be created with the below configuration, re-run command " + "without the --dryrun flag to create & deploy a new containerapp.") + else: + containerapp_def = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) + + fqdn = "" + + dry_run = { + "location" : location, + "registry": registry_server, + "image": image, + "src_path": _src_path_escaped + } + dry_run["name"] = "{} ({})".format(name, new_ca) + dry_run["resourcegroup"] = "{} ({})".format(resource_group_name, new_rg) + dry_run["environment"] = "{} ({})".format(env_name, new_managed_env) + if registry_server: + dry_run["registry"] = "{} ({})".format(registry_server, new_cr) + + if containerapp_def: + r = containerapp_def + if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: + fqdn = r["properties"]["configuration"]["ingress"]["fqdn"] + + if len(fqdn) > 0: + dry_run["fqdn"] = fqdn + if len(log_analytics_workspace_name) > 0: + dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name + return dry_run From 2f3e8b303e98f40f89f26fb34ad0abdc0336427f Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 13 Apr 2022 12:06:56 -0400 Subject: [PATCH 133/158] Create env if name passed and it doesn't exist. --- src/containerapp/azext_containerapp/custom.py | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 1c4863cb246..0b5d093fa55 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1913,6 +1913,7 @@ def containerapp_up(cmd, new_managed_env = "Existing" new_ca = "New" new_cr = "Existing" + new_law = "New" if source is None and image is None: raise RequiredArgumentMissingError("You must specify either --source or --image.") @@ -1935,14 +1936,16 @@ def containerapp_up(cmd, # Open dockerfile and check for EXPOSE if source: dockerfile_location = source + '/' + dockerfile - with open(dockerfile_location, 'r') as fh: - for line in fh: - if "EXPOSE" in line: - if not target_port and not ingress: - target_port = line.replace('\n', '').split(" ")[1] - ingress = "external" - logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) - + try: + with open(dockerfile_location, 'r') as fh: + for line in fh: + if "EXPOSE" in line: + if not target_port and not ingress: + target_port = line.replace('\n', '').split(" ")[1] + ingress = "external" + logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + except: + raise InvalidArgumentValueError("Cannot find specified Dockerfile. Check dockerfile name and/or path.") custom_rg_name = None # user passes bad resource group name, we create it for them if resource_group_name: @@ -1952,6 +1955,14 @@ def containerapp_up(cmd, custom_rg_name = resource_group_name resource_group_name = None + custom_env_name = None + if managed_env and not custom_rg_name: + try: + env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) + except: + custom_env_name = managed_env.split('/')[-1] + managed_env = None + # if custom_rg_name, that means rg doesn't exist no need to look for CA if not resource_group_name and not custom_rg_name: try: @@ -1976,7 +1987,7 @@ def containerapp_up(cmd, except: pass - env_name = "" if not managed_env else managed_env.split('/')[6] + env_name = "" if not managed_env else managed_env.split('/')[-1] if not containerapp_def: if not resource_group_name: new_rg = "New" @@ -1988,18 +1999,23 @@ def containerapp_up(cmd, resource_group_name = rg_name if not managed_env: new_managed_env = "New" - env_name = "{}-env".format(name).replace("_", "-") + env_name = custom_env_name if custom_env_name else "{}-env".format(name).replace("_", "-") if not dryrun: - logger.warning("Creating new managed environment {}".format(env_name)) - managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + try: + managed_env = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name)["id"] + logger.warning("Using existing managed environment {}".format(env_name)) + except: + logger.warning("Creating new managed environment {}".format(env_name)) + managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] else: managed_env = env_name else: new_ca = "Existing" location = containerapp_def["location"] - managed_env = containerapp_def["properties"]["managedEnvironmentId"] - env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] + managed_env = containerapp_def["properties"]["managedEnvironmentId"] if not managed_env else managed_env + env_name = managed_env.split('/')[-1] if logs_customer_id and logs_key: + new_law = "Existing" if not dryrun: managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] @@ -2055,16 +2071,6 @@ def containerapp_up(cmd, queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) - log_analytics_workspace_name = "" - env_def = None - try: - env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) - except: - pass - if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": - env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] - log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) - containerapp_def = None if dryrun: @@ -2075,12 +2081,16 @@ def containerapp_up(cmd, fqdn = "" + if new_managed_env == "Existing": + new_law = "Existing" + dry_run = { "location" : location, "registry": registry_server, "image": image, - "src_path": _src_path_escaped + "src_path": src_dir } + dry_run["name"] = "{} ({})".format(name, new_ca) dry_run["resourcegroup"] = "{} ({})".format(resource_group_name, new_rg) dry_run["environment"] = "{} ({})".format(env_name, new_managed_env) @@ -2092,8 +2102,19 @@ def containerapp_up(cmd, if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: fqdn = r["properties"]["configuration"]["ingress"]["fqdn"] + log_analytics_workspace_name = "" + env_def = None + try: + env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) + except: + pass + if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": + env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] + log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) + if len(fqdn) > 0: dry_run["fqdn"] = fqdn if len(log_analytics_workspace_name) > 0: - dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name + dry_run["log_analytics_workspace_name"] = "{} ({})".format(log_analytics_workspace_name, new_law) + return dry_run From 45dd7f8daaec7f8e6c001129c0daeecc1a04fc7d Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 13 Apr 2022 12:17:34 -0400 Subject: [PATCH 134/158] Added missing import from merge. --- src/containerapp/azext_containerapp/custom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 405dd058cf0..bfa061214aa 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -43,7 +43,8 @@ GitHubActionConfiguration, RegistryInfo as RegistryInfoModel, AzureCredentials as AzureCredentialsModel, - SourceControl as SourceControlModel) + SourceControl as SourceControlModel, + ManagedServiceIdentity as ManagedServiceIdentityModel) from ._utils import (_validate_subscription_registered, _get_location_from_resource_group, _ensure_location_allowed, parse_secret_flags, store_as_secret_and_return_secret_ref, parse_env_var_flags, _generate_log_analytics_if_not_provided, _get_existing_secrets, _convert_object_from_snake_to_camel_case, From 3380c63c041fb7d0ae0d0945c9eb2852bccb54b1 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Wed, 13 Apr 2022 17:47:12 -0400 Subject: [PATCH 135/158] Added prototype for new environment workflow. --- src/containerapp/azext_containerapp/custom.py | 73 +++++++++++++++---- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 0b5d093fa55..5cbf8858a66 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1946,8 +1946,9 @@ def containerapp_up(cmd, logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) except: raise InvalidArgumentValueError("Cannot find specified Dockerfile. Check dockerfile name and/or path.") + custom_rg_name = None - # user passes bad resource group name, we create it for them + # user passes non-existing rg, we create it for them if resource_group_name: try: get_resource_group(cmd, resource_group_name) @@ -1956,29 +1957,69 @@ def containerapp_up(cmd, resource_group_name = None custom_env_name = None + # user passes environment, check if it exists or not if managed_env and not custom_rg_name: try: - env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) + env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) except: + env_list = [] # Server error, not sure what to do here + env_found = False + for env in env_list: + if env["name"].lower() == managed_env.split('/')[-1].lower(): + if(env_found): + raise ValidationError("Multiple environments found on subscription with name {}. Specify resource id of environment.".format(managed_env.split('/')[-1])) + env_found = True + managed_env = env["id"] + if not env_found: custom_env_name = managed_env.split('/')[-1] managed_env = None - # if custom_rg_name, that means rg doesn't exist no need to look for CA + # look for existing containerapp with same name if not resource_group_name and not custom_rg_name: + rg_found = False try: - rg_found = False containerapps = list_containerapp(cmd) - for containerapp in containerapps: - if containerapp["name"].lower() == name.lower(): - if rg_found: - raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) - # could also just do resource_group_name = None here and create a new one, ask Anthony - # break - if containerapp["id"][0] != '/': - containerapp["id"] = '/' + containerapp["id"] - rg_found = True - resource_group_name = containerapp["id"].split('/')[4] except: + containerapps = [] # Server error, not sure what to do here + for containerapp in containerapps: + if containerapp["name"].lower() == name.lower(): + if rg_found: + raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) + # could also just do resource_group_name = None here and create a new one, ask Anthony + # break + if containerapp["id"][0] != '/': + containerapp["id"] = '/' + containerapp["id"] + rg_found = True + resource_group_name = containerapp["id"].split('/')[4] + managed_env = containerapp["properties"]["managedEnvironmentId"] + if custom_env_name: + # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + + # User doesn't pass environment, do they have an env on subscription? + if not managed_env and not custom_rg_name and not custom_env_name: + try: + env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) + if env_list is None or len(env_list) == 0: + managed_env = None # we need to create one for them + resource_group_name = managed_env.split('/')[4] + elif len(env_list) == 1: + managed_env = env_list[0]["id"] + resource_group_name = managed_env.split('/')[4] + else: + if logs_customer_id and logs_key: + managed_env = next(x for x in env_list if 'logAnalyticsConfiguration' in x['properties']['appLogsConfiguration'] and x['properties']['appLogsConfiguration']['logAnalyticsConfiguration']['customerId'] == logs_customer_id)["id"] + resource_group_name = managed_env.split('/')[4] + elif location: + # need to convert location here to lowercase conformity probably, not sure how atm + managed_env = next(x for x in env_list if x['location'] == location)["id"] + resource_group_name = managed_env.split('/')[4] + else: + # there are multiple envs, none of them match anything + managed_env = env_list[0]["id"] + resource_group_name = managed_env.split('/')[4] + except: + # They don't have any or server error pass containerapp_def = None @@ -2053,7 +2094,7 @@ def containerapp_up(cmd, new_cr = "New" registry_rg = resource_group_name user = get_profile_username() - registry_name = "{}acr".format(name) + registry_name = "{}acr".format(name).replace('-','') registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") if not dryrun: logger.warning("Creating new acr {}".format(registry_name)) @@ -2069,7 +2110,7 @@ def containerapp_up(cmd, image = registry_server + '/' + image_name if not dryrun: queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) - _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) + # _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) containerapp_def = None From 8fbf3601b8dd78742c917ecca4a93d7f6af78341 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 14 Apr 2022 16:58:38 -0400 Subject: [PATCH 136/158] Finished environment logic. --- src/containerapp/azext_containerapp/_utils.py | 2 +- src/containerapp/azext_containerapp/custom.py | 109 ++++++++---------- 2 files changed, 48 insertions(+), 63 deletions(-) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 513d6e1419d..26a42d98c75 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -259,7 +259,7 @@ def _get_log_analytics_workspace_name(cmd, logs_customer_id, resource_group_name for log in logs_list: if log.customer_id.lower() == logs_customer_id.lower(): return log.name - return ResourceNotFoundError("Cannot find Log Analytics workspace with customer ID {}".format(logs_customer_id)) + raise ResourceNotFoundError("Cannot find Log Analytics workspace with customer ID {}".format(logs_customer_id)) def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, location, resource_group_name): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 5cbf8858a66..4f6b970fe08 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1886,7 +1886,7 @@ def remove_dapr_component(cmd, resource_group_name, dapr_component_name, environ def containerapp_up(cmd, - name=None, + name, resource_group_name=None, managed_env=None, location=None, @@ -1909,6 +1909,7 @@ def containerapp_up(cmd, src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) + # Variables for output json new_rg = "Existing" new_managed_env = "Existing" new_ca = "New" @@ -1918,21 +1919,9 @@ def containerapp_up(cmd, if source is None and image is None: raise RequiredArgumentMissingError("You must specify either --source or --image.") - if not name: - if image: - name = image.split('/')[-1].split(':')[0].lower() - if source: - temp = source[1:] if source[0] == '.' else source # replace first . if it exists - name = temp.split('/')[-1].lower() # isolate folder name - if len(name) == 0: - name = _src_path_escaped.rsplit('\\', maxsplit=1)[-1] - if source and image: image = image.replace(':', '') - if not location: - location = _get_default_containerapps_location(cmd) - # Open dockerfile and check for EXPOSE if source: dockerfile_location = source + '/' + dockerfile @@ -1940,15 +1929,17 @@ def containerapp_up(cmd, with open(dockerfile_location, 'r') as fh: for line in fh: if "EXPOSE" in line: - if not target_port and not ingress: + if not target_port: target_port = line.replace('\n', '').split(" ")[1] - ingress = "external" logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + break except: raise InvalidArgumentValueError("Cannot find specified Dockerfile. Check dockerfile name and/or path.") + ingress = "external" if target_port and not ingress else ingress + custom_rg_name = None - # user passes non-existing rg, we create it for them + # User passes non-existing rg, we create it for them if resource_group_name: try: get_resource_group(cmd, resource_group_name) @@ -1957,70 +1948,60 @@ def containerapp_up(cmd, resource_group_name = None custom_env_name = None - # user passes environment, check if it exists or not + # User passes environment, check if it exists or not if managed_env and not custom_rg_name: try: env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) except: env_list = [] # Server error, not sure what to do here - env_found = False - for env in env_list: - if env["name"].lower() == managed_env.split('/')[-1].lower(): - if(env_found): - raise ValidationError("Multiple environments found on subscription with name {}. Specify resource id of environment.".format(managed_env.split('/')[-1])) - env_found = True - managed_env = env["id"] - if not env_found: + + env_list = [x for x in env_list if x['name'].lower() == managed_env.split('/')[-1].lower()] + if len(env_list) == 1: + managed_env = env_list[0]["id"] + resource_group_name = managed_env.split('/')[4] + if len(env_list) > 1: + raise ValidationError("Multiple environments found on subscription with name {}. Specify resource id of the environment.".format(managed_env.split('/')[-1])) + if len(env_list) == 0: custom_env_name = managed_env.split('/')[-1] managed_env = None - # look for existing containerapp with same name + # Look for existing containerapp with same name if not resource_group_name and not custom_rg_name: - rg_found = False try: containerapps = list_containerapp(cmd) except: containerapps = [] # Server error, not sure what to do here - for containerapp in containerapps: - if containerapp["name"].lower() == name.lower(): - if rg_found: - raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) - # could also just do resource_group_name = None here and create a new one, ask Anthony - # break - if containerapp["id"][0] != '/': - containerapp["id"] = '/' + containerapp["id"] - rg_found = True - resource_group_name = containerapp["id"].split('/')[4] - managed_env = containerapp["properties"]["managedEnvironmentId"] + + containerapps = [x for x in containerapps if x['name'].lower() == name.lower()] + if len(containerapps) == 1: + if containerapps[0]["properties"]["managedEnvironmentId"] == managed_env: + resource_group_name = containerapps[0]["id"].split('/')[4] + managed_env = containerapps[0]["properties"]["managedEnvironmentId"] if custom_env_name: # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + if len(containerapps) > 1: + raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) - # User doesn't pass environment, do they have an env on subscription? if not managed_env and not custom_rg_name and not custom_env_name: try: env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) - if env_list is None or len(env_list) == 0: - managed_env = None # we need to create one for them - resource_group_name = managed_env.split('/')[4] - elif len(env_list) == 1: - managed_env = env_list[0]["id"] - resource_group_name = managed_env.split('/')[4] - else: - if logs_customer_id and logs_key: - managed_env = next(x for x in env_list if 'logAnalyticsConfiguration' in x['properties']['appLogsConfiguration'] and x['properties']['appLogsConfiguration']['logAnalyticsConfiguration']['customerId'] == logs_customer_id)["id"] - resource_group_name = managed_env.split('/')[4] - elif location: - # need to convert location here to lowercase conformity probably, not sure how atm - managed_env = next(x for x in env_list if x['location'] == location)["id"] - resource_group_name = managed_env.split('/')[4] - else: - # there are multiple envs, none of them match anything - managed_env = env_list[0]["id"] - resource_group_name = managed_env.split('/')[4] except: - # They don't have any or server error - pass + env_list = [] # server error + + if logs_customer_id: + env_list = [x for x in env_list if 'logAnalyticsConfiguration' in x['properties']['appLogsConfiguration'] and x['properties']['appLogsConfiguration']['logAnalyticsConfiguration']['customerId'] == logs_customer_id] + if location: + env_list = [x for x in env_list if x['location'] == location] + if len(env_list) == 0: + managed_env = None + else: + # check how many CA in env + managed_env = env_list[0]["id"] + resource_group_name = managed_env.split('/')[4] + + if not location: + location = _get_default_containerapps_location(cmd) containerapp_def = None try: @@ -2052,8 +2033,9 @@ def containerapp_up(cmd, managed_env = env_name else: new_ca = "Existing" - location = containerapp_def["location"] - managed_env = containerapp_def["properties"]["managedEnvironmentId"] if not managed_env else managed_env + # location = containerapp_def["location"] + # This should be be defined no matter what + # managed_env = containerapp_def["properties"]["managedEnvironmentId"] if not managed_env else managed_env env_name = managed_env.split('/')[-1] if logs_customer_id and logs_key: new_law = "Existing" @@ -2106,7 +2088,9 @@ def containerapp_up(cmd, image_name = image if image is not None else name from datetime import datetime now = datetime.now() + # Add version tag for acr image image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + image = registry_server + '/' + image_name if not dryrun: queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) @@ -2126,7 +2110,7 @@ def containerapp_up(cmd, new_law = "Existing" dry_run = { - "location" : location, + "location" : containerapp_def["location"], "registry": registry_server, "image": image, "src_path": src_dir @@ -2155,6 +2139,7 @@ def containerapp_up(cmd, if len(fqdn) > 0: dry_run["fqdn"] = fqdn + if len(log_analytics_workspace_name) > 0: dry_run["log_analytics_workspace_name"] = "{} ({})".format(log_analytics_workspace_name, new_law) From c65196fa40631cb54fc769b3f8135c7205815d5e Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 15 Apr 2022 16:48:27 -0400 Subject: [PATCH 137/158] Minor updates before demo. --- .../azext_containerapp/_archive_utils.py | 242 ++++++++++++++++++ .../azext_containerapp/_params.py | 18 +- src/containerapp/azext_containerapp/_utils.py | 8 +- src/containerapp/azext_containerapp/custom.py | 68 +++-- 4 files changed, 287 insertions(+), 49 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_archive_utils.py diff --git a/src/containerapp/azext_containerapp/_archive_utils.py b/src/containerapp/azext_containerapp/_archive_utils.py new file mode 100644 index 00000000000..a32e380ad69 --- /dev/null +++ b/src/containerapp/azext_containerapp/_archive_utils.py @@ -0,0 +1,242 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import tarfile +import os +import re +import codecs +from io import open +import requests +from knack.log import get_logger +from knack.util import CLIError +from msrestazure.azure_exceptions import CloudError +from azure.cli.core.profiles import ResourceType, get_sdk +from azure.cli.command_modules.acr._azure_utils import get_blob_info +from azure.cli.command_modules.acr._constants import TASK_VALID_VSTS_URLS + +logger = get_logger(__name__) + + +def upload_source_code(cmd, client, + registry_name, + resource_group_name, + source_location, + tar_file_path, + docker_file_path, + docker_file_in_tar): + _pack_source_code(source_location, + tar_file_path, + docker_file_path, + docker_file_in_tar) + + size = os.path.getsize(tar_file_path) + unit = 'GiB' + for S in ['Bytes', 'KiB', 'MiB', 'GiB']: + if size < 1024: + unit = S + break + size = size / 1024.0 + + logger.info("Uploading archived source code from '%s'...", tar_file_path) + upload_url = None + relative_path = None + try: + source_upload_location = client.get_build_source_upload_url( + resource_group_name, registry_name) + upload_url = source_upload_location.upload_url + relative_path = source_upload_location.relative_path + except (AttributeError, CloudError) as e: + raise CLIError("Failed to get a SAS URL to upload context. Error: {}".format(e.message)) + + if not upload_url: + raise CLIError("Failed to get a SAS URL to upload context.") + + account_name, endpoint_suffix, container_name, blob_name, sas_token = get_blob_info(upload_url) + BlockBlobService = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE, 'blob#BlockBlobService') + BlockBlobService(account_name=account_name, + sas_token=sas_token, + endpoint_suffix=endpoint_suffix, + # Increase socket timeout from default of 20s for clients with slow network connection. + socket_timeout=300).create_blob_from_path( + container_name=container_name, + blob_name=blob_name, + file_path=tar_file_path) + logger.info("Sending context ({0:.3f} {1}) to registry: {2}...".format( + size, unit, registry_name)) + return relative_path + + +def _pack_source_code(source_location, tar_file_path, docker_file_path, docker_file_in_tar): + logger.info("Packing source code into tar to upload...") + + original_docker_file_name = os.path.basename(docker_file_path.replace("\\", os.sep)) + ignore_list, ignore_list_size = _load_dockerignore_file(source_location, original_docker_file_name) + common_vcs_ignore_list = {'.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn'} + + def _ignore_check(tarinfo, parent_ignored, parent_matching_rule_index): + # ignore common vcs dir or file + if tarinfo.name in common_vcs_ignore_list: + logger.info("Excluding '%s' based on default ignore rules", tarinfo.name) + return True, parent_matching_rule_index + + if ignore_list is None: + # if .dockerignore doesn't exists, inherit from parent + # eg, it will ignore the files under .git folder. + return parent_ignored, parent_matching_rule_index + + for index, item in enumerate(ignore_list): + # stop checking the remaining rules whose priorities are lower than the parent matching rule + # at this point, current item should just inherit from parent + if index >= parent_matching_rule_index: + break + if re.match(item.pattern, tarinfo.name): + logger.debug(".dockerignore: rule '%s' matches '%s'.", + item.rule, tarinfo.name) + return item.ignore, index + + logger.debug(".dockerignore: no rule for '%s'. parent ignore '%s'", + tarinfo.name, parent_ignored) + # inherit from parent + return parent_ignored, parent_matching_rule_index + + with tarfile.open(tar_file_path, "w:gz") as tar: + # need to set arcname to empty string as the archive root path + _archive_file_recursively(tar, + source_location, + arcname="", + parent_ignored=False, + parent_matching_rule_index=ignore_list_size, + ignore_check=_ignore_check) + + # Add the Dockerfile if it's specified. + # In the case of run, there will be no Dockerfile. + if docker_file_path: + docker_file_tarinfo = tar.gettarinfo( + docker_file_path, docker_file_in_tar) + with open(docker_file_path, "rb") as f: + tar.addfile(docker_file_tarinfo, f) + + +class IgnoreRule: # pylint: disable=too-few-public-methods + def __init__(self, rule): + + self.rule = rule + self.ignore = True + # ! makes exceptions to exclusions + if rule.startswith('!'): + self.ignore = False + rule = rule[1:] # remove ! + # load path without leading slash in linux and windows + # environments (interferes with dockerignore file) + if rule.startswith('/'): + rule = rule[1:] # remove beginning '/' + + self.pattern = "^" + tokens = rule.split('/') + token_length = len(tokens) + for index, token in enumerate(tokens, 1): + # ** matches any number of directories + if token == "**": + self.pattern += ".*" # treat **/ as ** + else: + # * matches any sequence of non-seperator characters + # ? matches any single non-seperator character + # . matches dot character + self.pattern += token.replace( + "*", "[^/]*").replace("?", "[^/]").replace(".", "\\.") + if index < token_length: + self.pattern += "/" # add back / if it's not the last + self.pattern += "$" + + +def _load_dockerignore_file(source_location, original_docker_file_name): + # reference: https://docs.docker.com/engine/reference/builder/#dockerignore-file + docker_ignore_file = os.path.join(source_location, ".dockerignore") + docker_ignore_file_override = None + if original_docker_file_name != "Dockerfile": + docker_ignore_file_override = os.path.join( + source_location, "{}.dockerignore".format(original_docker_file_name)) + if os.path.exists(docker_ignore_file_override): + logger.info("Overriding .dockerignore with %s", docker_ignore_file_override) + docker_ignore_file = docker_ignore_file_override + + if not os.path.exists(docker_ignore_file): + return None, 0 + + encoding = "utf-8" + header = open(docker_ignore_file, "rb").read(len(codecs.BOM_UTF8)) + if header.startswith(codecs.BOM_UTF8): + encoding = "utf-8-sig" + + ignore_list = [] + if docker_ignore_file == docker_ignore_file_override: + ignore_list.append(IgnoreRule(".dockerignore")) + + for line in open(docker_ignore_file, 'r', encoding=encoding).readlines(): + rule = line.rstrip() + + # skip empty line and comment + if not rule or rule.startswith('#'): + continue + + # the ignore rule at the end has higher priority + ignore_list = [IgnoreRule(rule)] + ignore_list + + return ignore_list, len(ignore_list) + + +def _archive_file_recursively(tar, name, arcname, parent_ignored, parent_matching_rule_index, ignore_check): + # create a TarInfo object from the file + tarinfo = tar.gettarinfo(name, arcname) + + if tarinfo is None: + raise CLIError("tarfile: unsupported type {}".format(name)) + + # check if the file/dir is ignored + ignored, matching_rule_index = ignore_check( + tarinfo, parent_ignored, parent_matching_rule_index) + + if not ignored: + # append the tar header and data to the archive + if tarinfo.isreg(): + with open(name, "rb") as f: + tar.addfile(tarinfo, f) + else: + tar.addfile(tarinfo) + + # even the dir is ignored, its child items can still be included, so continue to scan + if tarinfo.isdir(): + for f in os.listdir(name): + _archive_file_recursively(tar, os.path.join(name, f), os.path.join(arcname, f), + parent_ignored=ignored, parent_matching_rule_index=matching_rule_index, + ignore_check=ignore_check) + + +def check_remote_source_code(source_location): + lower_source_location = source_location.lower() + + # git + if lower_source_location.startswith("git@") or lower_source_location.startswith("git://"): + return source_location + + # http + if lower_source_location.startswith("https://") or lower_source_location.startswith("http://") \ + or lower_source_location.startswith("github.com/"): + isVSTS = any(url in lower_source_location for url in TASK_VALID_VSTS_URLS) + if isVSTS or re.search(r"\.git(?:#.+)?$", lower_source_location): + # git url must contain ".git" or be from VSTS/Azure DevOps. + # This is because Azure DevOps doesn't follow the standard git server convention of putting + # .git at the end of their URLs, so we have to special case them. + return source_location + if not lower_source_location.startswith("github.com/"): + # Others are tarball + if requests.head(source_location).status_code < 400: + return source_location + raise CLIError("'{}' doesn't exist.".format(source_location)) + + # oci + if lower_source_location.startswith("oci://"): + return source_location + raise CLIError("'{}' doesn't exist.".format(source_location)) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 1dc93a6ac5b..67fb5d88a64 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -33,7 +33,7 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container') as c: - c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + # c.argument('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 from 0.25 - 2.0, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, help="Required memory from 0.5 - 4.0 ending with \"Gi\", e.g. 1.0Gi") @@ -82,6 +82,12 @@ def load_arguments(self, _): c.argument('user_assigned', nargs='+', help="Space-separated user identities to be assigned.") c.argument('system_assigned', help="Boolean indicating whether to assign system-assigned identity.") + with self.argument_context('containerapp create', arg_group='Container') as c: + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + + with self.argument_context('containerapp update', arg_group='Container') as c: + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + with self.argument_context('containerapp scale') as c: c.argument('min_replicas', type=int, help="The minimum number of replicas.") c.argument('max_replicas', type=int, help="The maximum number of replicas.") @@ -192,14 +198,14 @@ def load_arguments(self, _): c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') c.argument('registry_server', configured_default='registry_server') - c.argument('quiet', help="Disable logs output from ACR build when using --source.") - c.argument('dockerfile', help="Name of the dockerfile.") c.argument('dryrun', help="Show summary of the operation instead of executing it.") + c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + + with self.argument_context('containerapp up', arg_group='Source') as c: + c.argument('dockerfile', help="Name of the dockerfile.") with self.argument_context('containerapp up', arg_group='Log Analytics (Environment)') as c: c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Name or resource ID of the Log Analytics workspace to send diagnostics logs to. You can use \"az monitor log-analytics workspace create\" to create one. Extra billing may apply.') c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log Analytics workspace key to configure your Log Analytics workspace. You can use \"az monitor log-analytics workspace get-shared-keys\" to retrieve the key.') c.ignore('no_wait') - - with self.argument_context('containerapp', arg_group='Container') as c: - c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 26a42d98c75..1e89befda26 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -679,7 +679,7 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi import os import uuid import tempfile - from azure.cli.command_modules.acr._archive_utils import upload_source_code + from ._archive_utils import upload_source_code from azure.cli.command_modules.acr._stream_utils import stream_logs from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks from azure.cli.core.commands import LongRunningOperation @@ -733,8 +733,8 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi run_request=docker_build_request)) run_id = queued_build.run_id - logger.warning("Queued a build with ID: %s", run_id) - not quiet and logger.warning("Waiting for agent...") + logger.info("Queued a build with ID: %s", run_id) + not quiet and logger.info("Waiting for agent...") from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) from ._acr_run_polling import get_run_with_polling @@ -743,7 +743,7 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi if quiet: lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) - logger.warning("Build {}.".format(acr.status.lower())) # pylint: disable=logging-format-interpolation + logger.info("Build {}.".format(acr.status.lower())) # pylint: disable=logging-format-interpolation if acr.status.lower() != "succeeded": raise CLIInternalError("ACR build {}.".format(acr.status.lower())) return acr diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4f6b970fe08..40bb320ce34 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1902,24 +1902,21 @@ def containerapp_up(cmd, env_vars=None, dryrun=False, logs_customer_id=None, - logs_key=None, - quiet=False): + logs_key=None): import os import json src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) - - # Variables for output json - new_rg = "Existing" - new_managed_env = "Existing" - new_ca = "New" - new_cr = "Existing" - new_law = "New" + quiet = False if source is None and image is None: raise RequiredArgumentMissingError("You must specify either --source or --image.") + # if source and image: + # raise ValidationError("You cannot specify both --source and --image.") + if source and image: + image = image.split('/')[-1] # if link is given image = image.replace(':', '') # Open dockerfile and check for EXPOSE @@ -1931,7 +1928,7 @@ def containerapp_up(cmd, if "EXPOSE" in line: if not target_port: target_port = line.replace('\n', '').split(" ")[1] - logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + logger.info("Adding external ingress port {} based on dockerfile expose.".format(target_port)) break except: raise InvalidArgumentValueError("Cannot find specified Dockerfile. Check dockerfile name and/or path.") @@ -1974,12 +1971,12 @@ def containerapp_up(cmd, containerapps = [x for x in containerapps if x['name'].lower() == name.lower()] if len(containerapps) == 1: - if containerapps[0]["properties"]["managedEnvironmentId"] == managed_env: - resource_group_name = containerapps[0]["id"].split('/')[4] - managed_env = containerapps[0]["properties"]["managedEnvironmentId"] - if custom_env_name: - # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") - logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + # if containerapps[0]["properties"]["managedEnvironmentId"] == managed_env: + resource_group_name = containerapps[0]["id"].split('/')[4] + managed_env = containerapps[0]["properties"]["managedEnvironmentId"] + if custom_env_name: + # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") if len(containerapps) > 1: raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) @@ -2012,7 +2009,6 @@ def containerapp_up(cmd, env_name = "" if not managed_env else managed_env.split('/')[-1] if not containerapp_def: if not resource_group_name: - new_rg = "New" user = get_profile_username() rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name if not dryrun: @@ -2020,32 +2016,31 @@ def containerapp_up(cmd, create_resource_group(cmd, rg_name, location) resource_group_name = rg_name if not managed_env: - new_managed_env = "New" env_name = custom_env_name if custom_env_name else "{}-env".format(name).replace("_", "-") if not dryrun: try: managed_env = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name)["id"] - logger.warning("Using existing managed environment {}".format(env_name)) + logger.info("Using existing managed environment {}".format(env_name)) except: logger.warning("Creating new managed environment {}".format(env_name)) managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] else: managed_env = env_name else: - new_ca = "Existing" - # location = containerapp_def["location"] + location = containerapp_def["location"] # This should be be defined no matter what - # managed_env = containerapp_def["properties"]["managedEnvironmentId"] if not managed_env else managed_env + if custom_env_name: + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + managed_env = containerapp_def["properties"]["managedEnvironmentId"] env_name = managed_env.split('/')[-1] if logs_customer_id and logs_key: - new_law = "Existing" if not dryrun: managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] if image is not None and "azurecr.io" in image and not dryrun: if registry_user is None or registry_pass is None: # If registry is Azure Container Registry, we can try inferring credentials - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') registry_server = image.split('/')[0] parsed = urlparse(image) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] @@ -2065,7 +2060,7 @@ def containerapp_up(cmd, raise ValidationError("Cannot supply non-Azure registry when using --source.") if not dryrun and (registry_user is None or registry_pass is None): # If registry is Azure Container Registry, we can try inferring credentials - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') parsed = urlparse(registry_server) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: @@ -2073,7 +2068,6 @@ def containerapp_up(cmd, except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex else: - new_cr = "New" registry_rg = resource_group_name user = get_profile_username() registry_name = "{}acr".format(name).replace('-','') @@ -2103,29 +2097,25 @@ def containerapp_up(cmd, "without the --dryrun flag to create & deploy a new containerapp.") else: containerapp_def = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) + location = containerapp_def["location"] fqdn = "" - if new_managed_env == "Existing": - new_law = "Existing" - dry_run = { - "location" : containerapp_def["location"], + "name" : name, + "resourcegroup" : resource_group_name, + "environment" : env_name, + "location" : location, "registry": registry_server, "image": image, - "src_path": src_dir + "src_path": src_dir, + "registry": registry_server } - - dry_run["name"] = "{} ({})".format(name, new_ca) - dry_run["resourcegroup"] = "{} ({})".format(resource_group_name, new_rg) - dry_run["environment"] = "{} ({})".format(env_name, new_managed_env) - if registry_server: - dry_run["registry"] = "{} ({})".format(registry_server, new_cr) if containerapp_def: r = containerapp_def if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: - fqdn = r["properties"]["configuration"]["ingress"]["fqdn"] + fqdn = "https://" + r["properties"]["configuration"]["ingress"]["fqdn"] log_analytics_workspace_name = "" env_def = None @@ -2141,6 +2131,6 @@ def containerapp_up(cmd, dry_run["fqdn"] = fqdn if len(log_analytics_workspace_name) > 0: - dry_run["log_analytics_workspace_name"] = "{} ({})".format(log_analytics_workspace_name, new_law) + dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name return dry_run From 67d6858f8964cc937c02daee7c107f4e4c8af936 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Sun, 17 Apr 2022 01:53:52 -0700 Subject: [PATCH 138/158] add 'az containerapp github up' (wip) --- .../azext_containerapp/_clients.py | 5 +- .../azext_containerapp/commands.py | 2 + src/containerapp/azext_containerapp/custom.py | 322 +++++++++++++++--- 3 files changed, 286 insertions(+), 43 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index cd1debfc0d2..4a4243610b0 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -588,7 +588,9 @@ 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 + print(management_hostname) + api_version = "2022-03-01" # NEW_API_VERSION + # 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( @@ -617,6 +619,7 @@ def create_or_update(cls, cmd, resource_group_name, name, github_action_envelope @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager + # api_version = "2022-03-01" # NEW_API_VERSION 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={}" diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 10088756c22..f8a1b54cb52 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -52,6 +52,8 @@ def load_command_table(self, _): g.custom_command('delete', 'delete_containerapp', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory()) g.custom_command('exec', 'containerapp_ssh', validator=validate_ssh) g.custom_command('up', 'containerapp_up', supports_no_wait=True, exception_handler=ex_handler_factory()) + g.custom_command('browse', 'open_containerapp_in_browser') + g.custom_command('github up', 'github_up') with self.command_group('containerapp replica', is_preview=True) as g: g.custom_show_command('show', 'get_replica') # TODO implement the table transformer diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index bfa061214aa..b2f7ebd0941 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -18,6 +18,9 @@ CLIInternalError, InvalidArgumentValueError) from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.core.util import open_page_in_browser, get_file_json +from azure.cli.command_modules.appservice._create_util import check_resource_group_exists +from azure.cli.command_modules.appservice._constants import GENERATE_RANDOM_APP_NAMES from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id @@ -54,7 +57,7 @@ _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, - _get_default_containerapps_location) + _get_default_containerapps_location, safe_get) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING, remove_token) @@ -1033,6 +1036,86 @@ def show_managed_identity(cmd, name, resource_group_name): return r["identity"] +def _validate_github(repo, branch, token): + from github import Github, GithubException + from github.GithubException import BadCredentialsException + + if repo: + g = Github(token) + github_repo = None + try: + github_repo = g.get_repo(repo) + if not github_repo.permissions.push or not github_repo.permissions.maintain: + raise ValidationError("The token does not have appropriate access rights to repository {}.".format(repo)) + try: + github_repo.get_branch(branch=branch) + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIInternalError(error_msg) from e + logger.warning('Verified GitHub repo and branch') + except BadCredentialsException as e: + raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " + "the --token argument. Run 'az webapp deployment github-actions add --help' " + "for more information.") from e + except GithubException as e: + error_msg = "Encountered GitHub error when accessing {} repo".format(repo) + if e.data and e.data['message']: + error_msg += " Error: {}".format(e.data['message']) + raise CLIInternalError(error_msg) from e + + +def _trigger_github_action(token, repo, branch, name): + from github import Github + from time import sleep + from ._clients import PollingAnimation + + logger.warning("Triggering Github action...") + animation = PollingAnimation() + animation.tick() + g = Github(token) + + github_repo = g.get_repo(repo) + + + workflow = None + while workflow is None: # TODO timeout? + workflows = github_repo.get_workflows() + animation.flush() + for wf in workflows: + if wf.path.startswith(f".github/workflows/{name}") and wf.name == "Trigger auto deployment for containerapps": + workflow = wf + break + else: + sleep(1) + animation.tick() + print(workflow) + workflow.create_dispatch(ref=github_repo.get_branch(branch)) + animation.flush() + logger.warning("Waiting for deployment to complete...") + animation.tick(); animation.flush() + run = workflow.get_runs()[0] + run_id = run.id + status = run.status + while status == "queued" or status == "in_progress": # TODO timeout? + sleep(1) + # animation.tick() + status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] + print(status) + # animation.flush() + if status != "completed": + raise ValidationError(f"Github action deployment ended with status: {status}") + + +def _repo_url_to_name(repo_url): + repo = None + repo = repo_url.split('/') + if len(repo) >= 2: + repo = '/'.join(repo[-2:]) + return repo + + def create_or_update_github_action(cmd, name, resource_group_name, @@ -1055,45 +1138,9 @@ def create_or_update_github_action(cmd, elif token and login_with_github: logger.warning("Both token and --login-with-github flag are provided. Will use provided token") - try: - # Verify github repo - from github import Github, GithubException - from github.GithubException import BadCredentialsException + repo = _repo_url_to_name(repo_url) - repo = None - repo = repo_url.split('/') - if len(repo) >= 2: - repo = '/'.join(repo[-2:]) - - if repo: - g = Github(token) - github_repo = None - try: - github_repo = g.get_repo(repo) - if not github_repo.permissions.push or not github_repo.permissions.maintain: - raise ValidationError("The token does not have appropriate access rights to repository {}.".format(repo)) - try: - github_repo.get_branch(branch=branch) - except GithubException as e: - error_msg = "Encountered GitHub error when accessing {} branch in {} repo.".format(branch, repo) - if e.data and e.data['message']: - error_msg += " Error: {}".format(e.data['message']) - raise CLIInternalError(error_msg) from e - logger.warning('Verified GitHub repo and branch') - except BadCredentialsException as e: - raise ValidationError("Could not authenticate to the repository. Please create a Personal Access Token and use " - "the --token argument. Run 'az webapp deployment github-actions add --help' " - "for more information.") from e - except GithubException as e: - error_msg = "Encountered GitHub error when accessing {} repo".format(repo) - if e.data and e.data['message']: - error_msg += " Error: {}".format(e.data['message']) - raise CLIInternalError(error_msg) from e - except CLIError as clierror: - raise clierror - except Exception: - # If exception due to github package missing, etc just continue without validating the repo and rely on api validation - pass + _validate_github(repo, branch, token) source_control_info = None @@ -1150,7 +1197,11 @@ def create_or_update_github_action(cmd, headers = ["x-ms-github-auxiliary={}".format(token)] try: + logger.warning("Creating Github action...") r = GitHubActionClient.create_or_update(cmd=cmd, resource_group_name=resource_group_name, name=name, github_action_envelope=source_control_info, headers=headers) + # _trigger_github_action(token, repo, branch, name) + # from ._ssh_utils import ping_container_app + # ping_container_app(ContainerAppClient.show(cmd, resource_group_name, name)) return r except Exception as e: handle_raw_exception(e) @@ -1548,7 +1599,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: - username, password = _get_acr_cred(cmd.cli_ctx, registry_name) + username, password = _get_acr_cred(cmd.cli_ctx, registry_name) # TODO this will always fail with "too many values to unpack" except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex @@ -1973,6 +2024,193 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev print(line.decode("utf-8").replace("\\u0022", "\u0022").replace("\\u001B", "\u001B").replace("\\u002B", "\u002B").replace("\\u0027", "\u0027")) +# TODO remove bc of rune's +def open_containerapp_in_browser(cmd, name, resource_group_name): + app = ContainerAppClient.show(cmd, resource_group_name, name) + url = safe_get(app, "properties", "configuration", "ingress", "fqdn") + if not url: + raise ValidationError("Could not find an external URL for this app") + if not url.startswith("http"): + url = f"http://{url}" + open_page_in_browser(url) + +# TODO move all these helper methods to their own file + +def _get_or_create_resource_group(cmd, resource_group_name): + location = "eastus" # TODO revisit this choice + if resource_group_name: + if check_resource_group_exists(cmd, resource_group_name): + return resource_group_name + else: + from random import randint + resource_group_name = "container_app_group_{:04}".format(randint(0, 9999)) + logger.warning(f"Creating resource group: {resource_group_name}") + create_resource_group(cmd, resource_group_name, location) + return resource_group_name + + +def _get_or_create_name(cmd, resource_group_name, name): + if name: + return name + + from random import choice + noun = choice(get_file_json(GENERATE_RANDOM_APP_NAMES)['APP_NAME_NOUNS']) + adjective = choice(get_file_json(GENERATE_RANDOM_APP_NAMES)['APP_NAME_ADJECTIVES']) + return f"{adjective}-{noun}" + + +def _get_or_create_managed_env(cmd, resource_group_name, managed_env, app_name): + if managed_env: + return managed_env # TODO test for existance + envs = ManagedEnvironmentClient.list_by_subscription(cmd) + if len(envs) == 0: + managed_env = managed_env or f"{app_name}-env" # TODO better name? + logger.warning(f"Creating Container App Environment {envs[0]['name']}") + return create_managed_environment(cmd, managed_env, resource_group_name)["name"] + logger.warning(f"Using Container App Environment: {envs[0]['name']}") + return envs[0]["id"] # TODO better selection logic (<5 apps in the selected sub) + + +def _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username): + from azure.cli.command_modules.acr.custom import acr_list + from azure.mgmt.containerregistry import ContainerRegistryManagementClient + from azure.cli.core.commands.client_factory import get_mgmt_service_client + + client = get_mgmt_service_client(cmd.cli_ctx, ContainerRegistryManagementClient).registries + + if not registry_url: + acrs = list(acr_list(client)) + assert len(acrs) != 0 # TODO create an ACR if registry not provided or found + registry_url = acrs[0].login_server + registry_username, registry_password, _ = _get_acr_cred(cmd.cli_ctx, acrs[0].name) + logger.warning(f"Using Azure Container Registry: {acrs[0].name}") + + # TODO fetch ACR creds if needed + + return registry_url, registry_password, registry_username + + +def _create_service_principal(cmd, resource_group_name): + from azure.cli.command_modules.role.custom import create_service_principal_for_rbac + + logger.warning("No valid service principal provided. Creating a new service principal...") + scopes = [f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}"] + sp = create_service_principal_for_rbac(cmd, scopes=scopes, role="contributor") + + logger.info(f"Created service principal: {sp['displayName']}") + + return sp["appId"], sp["password"], sp["tenant"] + + +def _get_or_create_sp(cmd, resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id): + try: + GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + return service_principal_client_id, service_principal_client_secret, service_principal_tenant_id + except: + return _create_service_principal(cmd, resource_group_name) + + +def is_int(s): + try: + int(s) + return True + except ValueError: + pass + return False + + +# currently assumes docker_file_name is in the root +# should allow docker_file_name to be a full path +def _get_dockerfile_content(repo_url, branch, docker_file_name, token): + from github import Github + g = Github(token) + repo = _repo_url_to_name(repo_url) + r = g.get_repo(repo) + # TODO support repos with dockerfile outside the root + files = r.get_contents(".", ref=branch) + for f in files: + if f.path == docker_file_name: + resp = requests.get(f.download_url) + if resp.ok and resp.content: + return resp.content.decode("utf-8").split("\n") + + +def _get_ingress_and_target_port(ingress, target_port, dockerfile_content): + if not target_port and not ingress and dockerfile_content is not None: + for line in dockerfile_content: + if line: + line = line.upper().strip().replace("/TCP", "").replace("/UDP", "").replace("\n","") + if line and line[0] != "#": + if "EXPOSE" in line: + parts = line.split(" ") + for i, p in enumerate(parts[:-1]): + if "EXPOSE" in p and is_int(parts[i+1]): + target_port = parts[i+1] + ingress = "external" + logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + return ingress, target_port + + +# create an app from GH repo +def github_up(cmd, + repo_url, + name=None, + resource_group_name=None, + managed_env=None, + registry_url=None, + registry_username=None, + registry_password=None, + branch="main", + token=None, + docker_file="Dockerfile", # TODO shouldn't this really be a path?? + service_principal_client_id=None, + service_principal_client_secret=None, + service_principal_tenant_id=None, + ingress=None, + target_port=None): + + # TODO will use up's behavior getting defaults/creating resources + resource_group_name = _get_or_create_resource_group(cmd, resource_group_name) + name = _get_or_create_name(cmd, resource_group_name, name) + managed_env = _get_or_create_managed_env(cmd, resource_group_name, managed_env, name) + registry_url, registry_password, registry_username = _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username) + service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = _get_or_create_sp(cmd, resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id) + dockerfile_content = _get_dockerfile_content(repo_url, branch, docker_file, token) + ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) + + + # TODO need to figure out which of these the GH action can set and which it can't + logger.warning(f"Creating Container App {name} in resource group {resource_group_name}") + create_containerapp(cmd=cmd, + name=name, + resource_group_name=resource_group_name, + image=None,# image, + managed_env=managed_env, + target_port=target_port,#target_port, + registry_server=None,#registry_server, + registry_pass=None,#registry_pass, + registry_user=None,#registry_user, + env_vars=None,#env_vars, + ingress=ingress,#ingress, + disable_warnings=True) + + create_or_update_github_action(cmd=cmd, + name=name, + resource_group_name=resource_group_name, + repo_url=repo_url, + registry_url=registry_url, + registry_username=registry_username, + registry_password=registry_password, + branch=branch, + token=token, + login_with_github=not token, + docker_file_path=".", # TODO support different dockerfile locations + service_principal_client_id=service_principal_client_id, + service_principal_client_secret=service_principal_client_secret, + service_principal_tenant_id=service_principal_tenant_id) + # TODO need to trigger the github action + + def containerapp_up(cmd, name=None, resource_group_name=None, @@ -2025,7 +2263,7 @@ def containerapp_up(cmd, dockerfile_location = source + '/' + dockerfile with open(dockerfile_location, 'r') as fh: for line in fh: - if "EXPOSE" in line: + if "EXPOSE" in line: # TODO this will fail on some dockerfile formats if not target_port and not ingress: target_port = line.replace('\n', '').split(" ")[1] ingress = "external" @@ -2099,7 +2337,7 @@ def containerapp_up(cmd, parsed = urlparse(image) 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) + registry_user, registry_pass = _get_acr_cred(cmd.cli_ctx, registry_name) # TODO this will always fail with "too many values to unpack" except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex From 4f04ac9c12ea58340d479b15c757157b0661ba4d Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Sun, 17 Apr 2022 16:33:09 -0700 Subject: [PATCH 139/158] various fixes for demo --- .../azext_containerapp/_params.py | 13 ++ src/containerapp/azext_containerapp/custom.py | 112 ++++++++++++------ 2 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 7d2a594e401..76332421bdc 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -31,6 +31,19 @@ def load_arguments(self, _): 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') + with self.argument_context('containerapp github up') as c: + c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com// or /') + 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.') + 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('context_path', help='Path in the repo from which to run the docker build. Defaults to "./"') + 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.') + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image name that the Github Action should use. Defaults to the Container App name.") + with self.argument_context('containerapp exec') as c: c.argument('container', help="The name of the container to ssh into") c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index ca48e66560d..19ed7b626e6 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1066,12 +1066,12 @@ def _validate_github(repo, branch, token): raise CLIInternalError(error_msg) from e -def _trigger_github_action(token, repo, branch, name): +def _await_github_action(token, repo, branch, name): from github import Github from time import sleep from ._clients import PollingAnimation - logger.warning("Triggering Github action...") + animation = PollingAnimation() animation.tick() g = Github(token) @@ -1087,22 +1087,23 @@ def _trigger_github_action(token, repo, branch, name): if wf.path.startswith(f".github/workflows/{name}") and wf.name == "Trigger auto deployment for containerapps": workflow = wf break - else: - sleep(1) - animation.tick() - print(workflow) - workflow.create_dispatch(ref=github_repo.get_branch(branch)) + sleep(1) + animation.tick() + + # print(workflow) + # workflow.create_dispatch(ref=github_repo.get_branch(branch)) animation.flush() - logger.warning("Waiting for deployment to complete...") animation.tick(); animation.flush() run = workflow.get_runs()[0] + logger.warning(f"Github action run: https://github.com/{repo}/actions/runs/{run.id}") + logger.warning("Waiting for deployment to complete...") run_id = run.id status = run.status while status == "queued" or status == "in_progress": # TODO timeout? - sleep(1) + sleep(3) # animation.tick() status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] - print(status) + # print(status) # animation.flush() if status != "completed": raise ValidationError(f"Github action deployment ended with status: {status}") @@ -1140,6 +1141,7 @@ def create_or_update_github_action(cmd, logger.warning("Both token and --login-with-github flag are provided. Will use provided token") repo = _repo_url_to_name(repo_url) + repo_url = f"https://github.com/{repo}" # allow specifying repo as / without the full github url _validate_github(repo, branch, token) @@ -1201,7 +1203,7 @@ def create_or_update_github_action(cmd, try: logger.warning("Creating Github action...") r = GitHubActionClient.create_or_update(cmd=cmd, resource_group_name=resource_group_name, name=name, github_action_envelope=source_control_info, headers=headers) - # _trigger_github_action(token, repo, branch, name) + _await_github_action(token, repo, branch, name) # from ._ssh_utils import ping_container_app # ping_container_app(ContainerAppClient.show(cmd, resource_group_name, name)) return r @@ -2058,19 +2060,22 @@ def _get_or_create_name(cmd, resource_group_name, name): from random import choice noun = choice(get_file_json(GENERATE_RANDOM_APP_NAMES)['APP_NAME_NOUNS']) adjective = choice(get_file_json(GENERATE_RANDOM_APP_NAMES)['APP_NAME_ADJECTIVES']) - return f"{adjective}-{noun}" + return f"{adjective}{noun}" def _get_or_create_managed_env(cmd, resource_group_name, managed_env, app_name): if managed_env: - return managed_env # TODO test for existance - envs = ManagedEnvironmentClient.list_by_subscription(cmd) + if is_valid_resource_id(managed_env): + parsed = parse_resource_id(managed_env) + return parsed["name"], parsed["resource_group"] + return managed_env, resource_group_name # TODO test for existance + envs = [e for e in ManagedEnvironmentClient.list_by_subscription(cmd) if e["location"] != "northcentralusstage"] # TODO remove if len(envs) == 0: managed_env = managed_env or f"{app_name}-env" # TODO better name? logger.warning(f"Creating Container App Environment {envs[0]['name']}") - return create_managed_environment(cmd, managed_env, resource_group_name)["name"] + return create_managed_environment(cmd, managed_env, resource_group_name)["name"], resource_group_name logger.warning(f"Using Container App Environment: {envs[0]['name']}") - return envs[0]["id"] # TODO better selection logic (<5 apps in the selected sub) + return envs[0]["id"], parse_resource_id(envs[0]["id"])["resource_group"] # TODO better selection logic (<5 apps in the selected sub) def _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username): @@ -2092,11 +2097,13 @@ def _get_or_create_registry(cmd, name, resource_group_name, registry_url, regist return registry_url, registry_password, registry_username -def _create_service_principal(cmd, resource_group_name): +def _create_service_principal(cmd, resource_group_name, env_resource_group_name): from azure.cli.command_modules.role.custom import create_service_principal_for_rbac logger.warning("No valid service principal provided. Creating a new service principal...") scopes = [f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}"] + if env_resource_group_name is not None and env_resource_group_name != resource_group_name: + scopes.append(f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{env_resource_group_name}") sp = create_service_principal_for_rbac(cmd, scopes=scopes, role="contributor") logger.info(f"Created service principal: {sp['displayName']}") @@ -2104,12 +2111,27 @@ def _create_service_principal(cmd, resource_group_name): return sp["appId"], sp["password"], sp["tenant"] -def _get_or_create_sp(cmd, resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id): +def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id): try: GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) return service_principal_client_id, service_principal_client_secret, service_principal_tenant_id except: - return _create_service_principal(cmd, resource_group_name) + # from azure.cli.command_modules.role.custom import list_sps + + # service_principals = list_sps(cmd, show_mine=True) + service_principal = None + # for sp in service_principals: + # if sp.oauth2_permissions: + # for p in sp.oauth2_permissions: + # if p.admin_consent_display_name and p.admin_consent_display_name == f"Access {resource_group_name}": + # service_principal = sp + # break + # if service_principal: + # break + + if not service_principal: + return _create_service_principal(cmd, resource_group_name, env_resource_group_name) + # return sp.app_id, def is_int(s): @@ -2121,9 +2143,8 @@ def is_int(s): return False -# currently assumes docker_file_name is in the root -# should allow docker_file_name to be a full path -def _get_dockerfile_content(repo_url, branch, docker_file_name, token): +# currently assumes docker_file_name is in the root and named "Dockerfile" +def _get_dockerfile_content(repo_url, branch, token): from github import Github g = Github(token) repo = _repo_url_to_name(repo_url) @@ -2131,7 +2152,7 @@ def _get_dockerfile_content(repo_url, branch, docker_file_name, token): # TODO support repos with dockerfile outside the root files = r.get_contents(".", ref=branch) for f in files: - if f.path == docker_file_name: + if f.path == "Dockerfile": resp = requests.get(f.download_url) if resp.ok and resp.content: return resp.content.decode("utf-8").split("\n") @@ -2155,7 +2176,7 @@ def _get_ingress_and_target_port(ingress, target_port, dockerfile_content): # create an app from GH repo def github_up(cmd, - repo_url, + repo, name=None, resource_group_name=None, managed_env=None, @@ -2164,26 +2185,31 @@ def github_up(cmd, registry_password=None, branch="main", token=None, - docker_file="Dockerfile", # TODO shouldn't this really be a path?? + context_path=None, # TODO shouldn't this really be a path?? + image=None, service_principal_client_id=None, service_principal_client_secret=None, service_principal_tenant_id=None, ingress=None, target_port=None): + if not token: + scopes = ["admin:repo_hook", "repo", "workflow"] + token = get_github_access_token(cmd, scopes) + # TODO will use up's behavior getting defaults/creating resources resource_group_name = _get_or_create_resource_group(cmd, resource_group_name) name = _get_or_create_name(cmd, resource_group_name, name) - managed_env = _get_or_create_managed_env(cmd, resource_group_name, managed_env, name) + managed_env, env_resource_group_name = _get_or_create_managed_env(cmd, resource_group_name, managed_env, name) registry_url, registry_password, registry_username = _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username) - service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = _get_or_create_sp(cmd, resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id) - dockerfile_content = _get_dockerfile_content(repo_url, branch, docker_file, token) + service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id) + dockerfile_content = _get_dockerfile_content(repo, branch, token) ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) # TODO need to figure out which of these the GH action can set and which it can't logger.warning(f"Creating Container App {name} in resource group {resource_group_name}") - create_containerapp(cmd=cmd, + app = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=None,# image, @@ -2194,23 +2220,37 @@ def github_up(cmd, registry_user=None,#registry_user, env_vars=None,#env_vars, ingress=ingress,#ingress, - disable_warnings=True) + disable_warnings=True, + min_replicas=1) # TODO remove - create_or_update_github_action(cmd=cmd, + gh_action = create_or_update_github_action(cmd=cmd, name=name, resource_group_name=resource_group_name, - repo_url=repo_url, + repo_url=repo, registry_url=registry_url, registry_username=registry_username, registry_password=registry_password, branch=branch, token=token, - login_with_github=not token, - docker_file_path=".", # TODO support different dockerfile locations + login_with_github=False, service_principal_client_id=service_principal_client_id, service_principal_client_secret=service_principal_client_secret, - service_principal_tenant_id=service_principal_tenant_id) - # TODO need to trigger the github action + service_principal_tenant_id=service_principal_tenant_id, + image=image, + context_path=context_path) + + dry_run = { + "name": name, + "resourceGroup": resource_group_name, + "environment": managed_env, + "fqdn": f'https://{safe_get(app, "properties", "configuration", "ingress", "fqdn")}', + "location" : app["location"], + "registry": registry_url, + "image": gh_action["properties"]["githubActionConfiguration"]["image"], + "githubAction": gh_action + } + + return dry_run def containerapp_up(cmd, From 125d56fa9f21eb2f06691398238d45e055097382 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 19 Apr 2022 10:10:33 -0700 Subject: [PATCH 140/158] rearrange github up code --- .../azext_containerapp/_github_oauth.py | 2 + src/containerapp/azext_containerapp/_utils.py | 141 +++++++++++++++++- src/containerapp/azext_containerapp/custom.py | 56 +++---- 3 files changed, 160 insertions(+), 39 deletions(-) diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py index 659d43afc39..fe883bcc5d5 100644 --- a/src/containerapp/azext_containerapp/_github_oauth.py +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -6,6 +6,7 @@ from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) from knack.log import get_logger +from azure.cli.core.util import open_page_in_browser logger = get_logger(__name__) @@ -52,6 +53,7 @@ def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-arg 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) + open_page_in_browser("https://github.com/login/device") timeout = time.time() + expires_in_seconds logger.warning("Waiting up to '%s' minutes for activation", str(expires_in_seconds // 60)) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 773d2155b30..458d91ba679 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -4,9 +4,14 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals +import time +import json +import datetime +from dateutil.relativedelta import relativedelta import platform from urllib.parse import urlparse -from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError, ResourceNotFoundError) +from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError, + ResourceNotFoundError, ArgumentUsageError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger from msrestazure.tools import parse_resource_id @@ -17,6 +22,140 @@ logger = get_logger(__name__) + +# original implementation at azure.cli.command_modules.role.custom.create_service_principal_for_rbac +# reimplemented to remove unnecessary warning statements +def create_service_principal_for_rbac( + # pylint:disable=too-many-statements,too-many-locals, too-many-branches, unused-argument + cmd, name=None, years=None, create_cert=False, cert=None, scopes=None, role=None, + show_auth_for_sdk=None, skip_assignment=False, keyvault=None): + from azure.cli.command_modules.role.custom import (_graph_client_factory, TZ_UTC, _process_service_principal_creds, + _validate_app_dates, create_application, + _create_service_principal, _create_role_assignment, + _error_caused_by_role_assignment_exists) + + + if role and not scopes or not role and scopes: + raise ArgumentUsageError("Usage error: To create role assignments, specify both --role and --scopes.") + + graph_client = _graph_client_factory(cmd.cli_ctx) + + years = years or 1 + _RETRY_TIMES = 36 + existing_sps = None + + if not name: + # No name is provided, create a new one + app_display_name = 'azure-cli-' + datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S') + else: + app_display_name = name + # patch existing app with the same displayName to make the command idempotent + query_exp = "displayName eq '{}'".format(name) + existing_sps = list(graph_client.service_principals.list(filter=query_exp)) + + app_start_date = datetime.datetime.now(TZ_UTC) + app_end_date = app_start_date + relativedelta(years=years or 1) + + password, public_cert_string, cert_file, cert_start_date, cert_end_date = \ + _process_service_principal_creds(cmd.cli_ctx, years, app_start_date, app_end_date, cert, create_cert, + None, keyvault) + + app_start_date, app_end_date, cert_start_date, cert_end_date = \ + _validate_app_dates(app_start_date, app_end_date, cert_start_date, cert_end_date) + + aad_application = create_application(cmd, + display_name=app_display_name, + available_to_other_tenants=False, + password=password, + key_value=public_cert_string, + start_date=app_start_date, + end_date=app_end_date, + credential_description='rbac') + # pylint: disable=no-member + app_id = aad_application.app_id + + # retry till server replication is done + aad_sp = existing_sps[0] if existing_sps else None + if not aad_sp: + for retry_time in range(0, _RETRY_TIMES): + try: + aad_sp = _create_service_principal(cmd.cli_ctx, app_id, resolve_app=False) + break + except Exception as ex: # pylint: disable=broad-except + err_msg = str(ex) + if retry_time < _RETRY_TIMES and ( + ' does not reference ' in err_msg or + ' does not exist ' in err_msg or + 'service principal being created must in the local tenant' in err_msg): + logger.warning("Creating service principal failed with error '%s'. Retrying: %s/%s", + err_msg, retry_time + 1, _RETRY_TIMES) + time.sleep(5) + else: + logger.warning( + "Creating service principal failed for '%s'. Trace followed:\n%s", + app_id, ex.response.headers + if hasattr(ex, 'response') else ex) # pylint: disable=no-member + raise + sp_oid = aad_sp.object_id + + if role: + for scope in scopes: + # logger.warning("Creating '%s' role assignment under scope '%s'", role, scope) + # retry till server replication is done + for retry_time in range(0, _RETRY_TIMES): + try: + _create_role_assignment(cmd.cli_ctx, role, sp_oid, None, scope, resolve_assignee=False, + assignee_principal_type='ServicePrincipal') + break + except Exception as ex: + if retry_time < _RETRY_TIMES and ' does not exist in the directory ' in str(ex): + time.sleep(5) + logger.warning(' Retrying role assignment creation: %s/%s', retry_time + 1, + _RETRY_TIMES) + continue + if _error_caused_by_role_assignment_exists(ex): + logger.warning(' Role assignment already exists.\n') + break + + # dump out history for diagnoses + logger.warning(' Role assignment creation failed.\n') + if getattr(ex, 'response', None) is not None: + logger.warning(' role assignment response headers: %s\n', + ex.response.headers) # pylint: disable=no-member + raise + + if show_auth_for_sdk: + from azure.cli.core._profile import Profile + profile = Profile(cli_ctx=cmd.cli_ctx) + result = profile.get_sp_auth_info(scopes[0].split('/')[2] if scopes else None, + app_id, password, cert_file) + # sdk-auth file should be in json format all the time, hence the print + print(json.dumps(result, indent=2)) + return + + result = { + 'appId': app_id, + 'password': password, + 'displayName': app_display_name, + 'tenant': graph_client.config.tenant_id + } + if cert_file: + logger.warning( + "Please copy %s to a safe place. When you run 'az login', provide the file path in the --password argument", + cert_file) + result['fileWithCertAndPrivateKey'] = cert_file + return result + + +def is_int(s): + try: + int(s) + return True + except ValueError: + pass + return False + + def _get_location_from_resource_group(cli_ctx, resource_group_name): client = cf_resource_groups(cli_ctx) group = client.get(resource_group_name) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 19ed7b626e6..f4c8805d703 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -57,7 +57,7 @@ _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, - _get_default_containerapps_location, safe_get) + _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING, remove_token) @@ -1084,7 +1084,7 @@ def _await_github_action(token, repo, branch, name): workflows = github_repo.get_workflows() animation.flush() for wf in workflows: - if wf.path.startswith(f".github/workflows/{name}") and wf.name == "Trigger auto deployment for containerapps": + if wf.path.startswith(f".github/workflows/{name}") and "Trigger auto deployment for containerapp" in wf.name: workflow = wf break sleep(1) @@ -2085,7 +2085,7 @@ def _get_or_create_registry(cmd, name, resource_group_name, registry_url, regist client = get_mgmt_service_client(cmd.cli_ctx, ContainerRegistryManagementClient).registries - if not registry_url: + if not registry_url and not registry_password and not registry_username: acrs = list(acr_list(client)) assert len(acrs) != 0 # TODO create an ACR if registry not provided or found registry_url = acrs[0].login_server @@ -2098,8 +2098,6 @@ def _get_or_create_registry(cmd, name, resource_group_name, registry_url, regist def _create_service_principal(cmd, resource_group_name, env_resource_group_name): - from azure.cli.command_modules.role.custom import create_service_principal_for_rbac - logger.warning("No valid service principal provided. Creating a new service principal...") scopes = [f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}"] if env_resource_group_name is not None and env_resource_group_name != resource_group_name: @@ -2111,36 +2109,20 @@ def _create_service_principal(cmd, resource_group_name, env_resource_group_name) return sp["appId"], sp["password"], sp["tenant"] -def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id): +def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, + service_principal_client_secret, service_principal_tenant_id): try: GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) return service_principal_client_id, service_principal_client_secret, service_principal_tenant_id except: - # from azure.cli.command_modules.role.custom import list_sps - - # service_principals = list_sps(cmd, show_mine=True) service_principal = None - # for sp in service_principals: - # if sp.oauth2_permissions: - # for p in sp.oauth2_permissions: - # if p.admin_consent_display_name and p.admin_consent_display_name == f"Access {resource_group_name}": - # service_principal = sp - # break - # if service_principal: - # break + + # TODO if possible, search for SPs with the right credentials + # I haven't found a way to get SP creds + secrets yet from the API if not service_principal: return _create_service_principal(cmd, resource_group_name, env_resource_group_name) - # return sp.app_id, - - -def is_int(s): - try: - int(s) - return True - except ValueError: - pass - return False + # return client_id, secret, tenant_id # currently assumes docker_file_name is in the root and named "Dockerfile" @@ -2158,7 +2140,7 @@ def _get_dockerfile_content(repo_url, branch, token): return resp.content.decode("utf-8").split("\n") -def _get_ingress_and_target_port(ingress, target_port, dockerfile_content): +def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: 'list[str]'): if not target_port and not ingress and dockerfile_content is not None: for line in dockerfile_content: if line: @@ -2206,22 +2188,20 @@ def github_up(cmd, dockerfile_content = _get_dockerfile_content(repo, branch, token) ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) - - # TODO need to figure out which of these the GH action can set and which it can't logger.warning(f"Creating Container App {name} in resource group {resource_group_name}") app = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, - image=None,# image, + image=None, # GH action handles setting this property managed_env=managed_env, - target_port=target_port,#target_port, - registry_server=None,#registry_server, - registry_pass=None,#registry_pass, - registry_user=None,#registry_user, - env_vars=None,#env_vars, - ingress=ingress,#ingress, + target_port=target_port, + registry_server=None, # GH action handles setting this property + registry_pass=None, # GH action handles setting this property + registry_user=None, # GH action handles setting this property + env_vars=None, # TODO + ingress=ingress, disable_warnings=True, - min_replicas=1) # TODO remove + min_replicas=1) # TODO gh_action = create_or_update_github_action(cmd=cmd, name=name, From 0e869471ae524a2817cd601ad049ad03e906244a Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Tue, 19 Apr 2022 10:28:59 -0700 Subject: [PATCH 141/158] merge haroonf/containerappup --- .../azext_containerapp/_archive_utils.py | 242 ++++++++++++++++++ .../azext_containerapp/_params.py | 18 +- src/containerapp/azext_containerapp/_utils.py | 10 +- src/containerapp/azext_containerapp/custom.py | 182 +++++++------ 4 files changed, 369 insertions(+), 83 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_archive_utils.py diff --git a/src/containerapp/azext_containerapp/_archive_utils.py b/src/containerapp/azext_containerapp/_archive_utils.py new file mode 100644 index 00000000000..a32e380ad69 --- /dev/null +++ b/src/containerapp/azext_containerapp/_archive_utils.py @@ -0,0 +1,242 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import tarfile +import os +import re +import codecs +from io import open +import requests +from knack.log import get_logger +from knack.util import CLIError +from msrestazure.azure_exceptions import CloudError +from azure.cli.core.profiles import ResourceType, get_sdk +from azure.cli.command_modules.acr._azure_utils import get_blob_info +from azure.cli.command_modules.acr._constants import TASK_VALID_VSTS_URLS + +logger = get_logger(__name__) + + +def upload_source_code(cmd, client, + registry_name, + resource_group_name, + source_location, + tar_file_path, + docker_file_path, + docker_file_in_tar): + _pack_source_code(source_location, + tar_file_path, + docker_file_path, + docker_file_in_tar) + + size = os.path.getsize(tar_file_path) + unit = 'GiB' + for S in ['Bytes', 'KiB', 'MiB', 'GiB']: + if size < 1024: + unit = S + break + size = size / 1024.0 + + logger.info("Uploading archived source code from '%s'...", tar_file_path) + upload_url = None + relative_path = None + try: + source_upload_location = client.get_build_source_upload_url( + resource_group_name, registry_name) + upload_url = source_upload_location.upload_url + relative_path = source_upload_location.relative_path + except (AttributeError, CloudError) as e: + raise CLIError("Failed to get a SAS URL to upload context. Error: {}".format(e.message)) + + if not upload_url: + raise CLIError("Failed to get a SAS URL to upload context.") + + account_name, endpoint_suffix, container_name, blob_name, sas_token = get_blob_info(upload_url) + BlockBlobService = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE, 'blob#BlockBlobService') + BlockBlobService(account_name=account_name, + sas_token=sas_token, + endpoint_suffix=endpoint_suffix, + # Increase socket timeout from default of 20s for clients with slow network connection. + socket_timeout=300).create_blob_from_path( + container_name=container_name, + blob_name=blob_name, + file_path=tar_file_path) + logger.info("Sending context ({0:.3f} {1}) to registry: {2}...".format( + size, unit, registry_name)) + return relative_path + + +def _pack_source_code(source_location, tar_file_path, docker_file_path, docker_file_in_tar): + logger.info("Packing source code into tar to upload...") + + original_docker_file_name = os.path.basename(docker_file_path.replace("\\", os.sep)) + ignore_list, ignore_list_size = _load_dockerignore_file(source_location, original_docker_file_name) + common_vcs_ignore_list = {'.git', '.gitignore', '.bzr', 'bzrignore', '.hg', '.hgignore', '.svn'} + + def _ignore_check(tarinfo, parent_ignored, parent_matching_rule_index): + # ignore common vcs dir or file + if tarinfo.name in common_vcs_ignore_list: + logger.info("Excluding '%s' based on default ignore rules", tarinfo.name) + return True, parent_matching_rule_index + + if ignore_list is None: + # if .dockerignore doesn't exists, inherit from parent + # eg, it will ignore the files under .git folder. + return parent_ignored, parent_matching_rule_index + + for index, item in enumerate(ignore_list): + # stop checking the remaining rules whose priorities are lower than the parent matching rule + # at this point, current item should just inherit from parent + if index >= parent_matching_rule_index: + break + if re.match(item.pattern, tarinfo.name): + logger.debug(".dockerignore: rule '%s' matches '%s'.", + item.rule, tarinfo.name) + return item.ignore, index + + logger.debug(".dockerignore: no rule for '%s'. parent ignore '%s'", + tarinfo.name, parent_ignored) + # inherit from parent + return parent_ignored, parent_matching_rule_index + + with tarfile.open(tar_file_path, "w:gz") as tar: + # need to set arcname to empty string as the archive root path + _archive_file_recursively(tar, + source_location, + arcname="", + parent_ignored=False, + parent_matching_rule_index=ignore_list_size, + ignore_check=_ignore_check) + + # Add the Dockerfile if it's specified. + # In the case of run, there will be no Dockerfile. + if docker_file_path: + docker_file_tarinfo = tar.gettarinfo( + docker_file_path, docker_file_in_tar) + with open(docker_file_path, "rb") as f: + tar.addfile(docker_file_tarinfo, f) + + +class IgnoreRule: # pylint: disable=too-few-public-methods + def __init__(self, rule): + + self.rule = rule + self.ignore = True + # ! makes exceptions to exclusions + if rule.startswith('!'): + self.ignore = False + rule = rule[1:] # remove ! + # load path without leading slash in linux and windows + # environments (interferes with dockerignore file) + if rule.startswith('/'): + rule = rule[1:] # remove beginning '/' + + self.pattern = "^" + tokens = rule.split('/') + token_length = len(tokens) + for index, token in enumerate(tokens, 1): + # ** matches any number of directories + if token == "**": + self.pattern += ".*" # treat **/ as ** + else: + # * matches any sequence of non-seperator characters + # ? matches any single non-seperator character + # . matches dot character + self.pattern += token.replace( + "*", "[^/]*").replace("?", "[^/]").replace(".", "\\.") + if index < token_length: + self.pattern += "/" # add back / if it's not the last + self.pattern += "$" + + +def _load_dockerignore_file(source_location, original_docker_file_name): + # reference: https://docs.docker.com/engine/reference/builder/#dockerignore-file + docker_ignore_file = os.path.join(source_location, ".dockerignore") + docker_ignore_file_override = None + if original_docker_file_name != "Dockerfile": + docker_ignore_file_override = os.path.join( + source_location, "{}.dockerignore".format(original_docker_file_name)) + if os.path.exists(docker_ignore_file_override): + logger.info("Overriding .dockerignore with %s", docker_ignore_file_override) + docker_ignore_file = docker_ignore_file_override + + if not os.path.exists(docker_ignore_file): + return None, 0 + + encoding = "utf-8" + header = open(docker_ignore_file, "rb").read(len(codecs.BOM_UTF8)) + if header.startswith(codecs.BOM_UTF8): + encoding = "utf-8-sig" + + ignore_list = [] + if docker_ignore_file == docker_ignore_file_override: + ignore_list.append(IgnoreRule(".dockerignore")) + + for line in open(docker_ignore_file, 'r', encoding=encoding).readlines(): + rule = line.rstrip() + + # skip empty line and comment + if not rule or rule.startswith('#'): + continue + + # the ignore rule at the end has higher priority + ignore_list = [IgnoreRule(rule)] + ignore_list + + return ignore_list, len(ignore_list) + + +def _archive_file_recursively(tar, name, arcname, parent_ignored, parent_matching_rule_index, ignore_check): + # create a TarInfo object from the file + tarinfo = tar.gettarinfo(name, arcname) + + if tarinfo is None: + raise CLIError("tarfile: unsupported type {}".format(name)) + + # check if the file/dir is ignored + ignored, matching_rule_index = ignore_check( + tarinfo, parent_ignored, parent_matching_rule_index) + + if not ignored: + # append the tar header and data to the archive + if tarinfo.isreg(): + with open(name, "rb") as f: + tar.addfile(tarinfo, f) + else: + tar.addfile(tarinfo) + + # even the dir is ignored, its child items can still be included, so continue to scan + if tarinfo.isdir(): + for f in os.listdir(name): + _archive_file_recursively(tar, os.path.join(name, f), os.path.join(arcname, f), + parent_ignored=ignored, parent_matching_rule_index=matching_rule_index, + ignore_check=ignore_check) + + +def check_remote_source_code(source_location): + lower_source_location = source_location.lower() + + # git + if lower_source_location.startswith("git@") or lower_source_location.startswith("git://"): + return source_location + + # http + if lower_source_location.startswith("https://") or lower_source_location.startswith("http://") \ + or lower_source_location.startswith("github.com/"): + isVSTS = any(url in lower_source_location for url in TASK_VALID_VSTS_URLS) + if isVSTS or re.search(r"\.git(?:#.+)?$", lower_source_location): + # git url must contain ".git" or be from VSTS/Azure DevOps. + # This is because Azure DevOps doesn't follow the standard git server convention of putting + # .git at the end of their URLs, so we have to special case them. + return source_location + if not lower_source_location.startswith("github.com/"): + # Others are tarball + if requests.head(source_location).status_code < 400: + return source_location + raise CLIError("'{}' doesn't exist.".format(source_location)) + + # oci + if lower_source_location.startswith("oci://"): + return source_location + raise CLIError("'{}' doesn't exist.".format(source_location)) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 76332421bdc..59dc0167724 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -71,7 +71,7 @@ def load_arguments(self, _): # Container with self.argument_context('containerapp', arg_group='Container') as c: - c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + # c.argument('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 from 0.25 - 2.0, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, help="Required memory from 0.5 - 4.0 ending with \"Gi\", e.g. 1.0Gi") @@ -120,6 +120,12 @@ def load_arguments(self, _): c.argument('user_assigned', nargs='+', help="Space-separated user identities to be assigned.") c.argument('system_assigned', help="Boolean indicating whether to assign system-assigned identity.") + with self.argument_context('containerapp create', arg_group='Container') as c: + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + + with self.argument_context('containerapp update', arg_group='Container') as c: + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + with self.argument_context('containerapp scale') as c: c.argument('min_replicas', type=int, help="The minimum number of replicas.") c.argument('max_replicas', type=int, help="The maximum number of replicas.") @@ -231,14 +237,14 @@ def load_arguments(self, _): c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') c.argument('registry_server', configured_default='registry_server') - c.argument('quiet', help="Disable logs output from ACR build when using --source.") - c.argument('dockerfile', help="Name of the dockerfile.") c.argument('dryrun', help="Show summary of the operation instead of executing it.") + c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") + + with self.argument_context('containerapp up', arg_group='Source') as c: + c.argument('dockerfile', help="Name of the dockerfile.") with self.argument_context('containerapp up', arg_group='Log Analytics (Environment)') as c: c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Name or resource ID of the Log Analytics workspace to send diagnostics logs to. You can use \"az monitor log-analytics workspace create\" to create one. Extra billing may apply.') c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log Analytics workspace key to configure your Log Analytics workspace. You can use \"az monitor log-analytics workspace get-shared-keys\" to retrieve the key.') c.ignore('no_wait') - - with self.argument_context('containerapp', arg_group='Container') as c: - c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 458d91ba679..43d8e3a26ff 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -399,7 +399,7 @@ def _get_log_analytics_workspace_name(cmd, logs_customer_id, resource_group_name for log in logs_list: if log.customer_id.lower() == logs_customer_id.lower(): return log.name - return ResourceNotFoundError("Cannot find Log Analytics workspace with customer ID {}".format(logs_customer_id)) + raise ResourceNotFoundError("Cannot find Log Analytics workspace with customer ID {}".format(logs_customer_id)) def _generate_log_analytics_if_not_provided(cmd, logs_customer_id, logs_key, location, resource_group_name): @@ -831,7 +831,7 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi import os import uuid import tempfile - from azure.cli.command_modules.acr._archive_utils import upload_source_code + from ._archive_utils import upload_source_code from azure.cli.command_modules.acr._stream_utils import stream_logs from azure.cli.command_modules.acr._client_factory import cf_acr_registries_tasks from azure.cli.core.commands import LongRunningOperation @@ -885,8 +885,8 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi run_request=docker_build_request)) run_id = queued_build.run_id - logger.warning("Queued a build with ID: %s", run_id) - not quiet and logger.warning("Waiting for agent...") + logger.info("Queued a build with ID: %s", run_id) + not quiet and logger.info("Waiting for agent...") from azure.cli.command_modules.acr._client_factory import (cf_acr_runs) from ._acr_run_polling import get_run_with_polling @@ -895,7 +895,7 @@ def queue_acr_build(cmd, registry_rg, registry_name, img_name, src_dir, dockerfi if quiet: lro_poller = get_run_with_polling(cmd, client_runs, run_id, registry_name, registry_rg) acr = LongRunningOperation(cmd.cli_ctx)(lro_poller) - logger.warning("Build {}.".format(acr.status.lower())) # pylint: disable=logging-format-interpolation + logger.info("Build {}.".format(acr.status.lower())) # pylint: disable=logging-format-interpolation if acr.status.lower() != "succeeded": raise CLIInternalError("ACR build {}.".format(acr.status.lower())) return acr diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index f4c8805d703..8b4a50e2e0d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -57,7 +57,8 @@ _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _registry_exists, _remove_env_vars, _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, - _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac) + _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, + _get_default_containerapps_location) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING, remove_token) @@ -2234,7 +2235,7 @@ def github_up(cmd, def containerapp_up(cmd, - name=None, + name, resource_group_name=None, managed_env=None, location=None, @@ -2250,49 +2251,41 @@ def containerapp_up(cmd, env_vars=None, dryrun=False, logs_customer_id=None, - logs_key=None, - quiet=False): + logs_key=None): import os import json src_dir = os.getcwd() _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) - - new_rg = "Existing" - new_managed_env = "Existing" - new_ca = "New" - new_cr = "Existing" + quiet = False if source is None and image is None: raise RequiredArgumentMissingError("You must specify either --source or --image.") - if not name: - if image: - name = image.split('/')[-1].split(':')[0].lower() - if source: - temp = source[1:] if source[0] == '.' else source # replace first . if it exists - name = temp.split('/')[-1].lower() # isolate folder name - if len(name) == 0: - name = _src_path_escaped.rsplit('\\', maxsplit=1)[-1] + # if source and image: + # raise ValidationError("You cannot specify both --source and --image.") if source and image: + image = image.split('/')[-1] # if link is given image = image.replace(':', '') - if not location: - location = _get_default_containerapps_location(cmd) - # Open dockerfile and check for EXPOSE if source: dockerfile_location = source + '/' + dockerfile - with open(dockerfile_location, 'r') as fh: - for line in fh: - if "EXPOSE" in line: # TODO this will fail on some dockerfile formats - if not target_port and not ingress: - target_port = line.replace('\n', '').split(" ")[1] - ingress = "external" - logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + try: + with open(dockerfile_location, 'r') as fh: + for line in fh: + if "EXPOSE" in line: + if not target_port: + target_port = line.replace('\n', '').split(" ")[1] + logger.info("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + break + except: + raise InvalidArgumentValueError("Cannot find specified Dockerfile. Check dockerfile name and/or path.") + + ingress = "external" if target_port and not ingress else ingress custom_rg_name = None - # user passes bad resource group name, we create it for them + # User passes non-existing rg, we create it for them if resource_group_name: try: get_resource_group(cmd, resource_group_name) @@ -2300,23 +2293,61 @@ def containerapp_up(cmd, custom_rg_name = resource_group_name resource_group_name = None - # if custom_rg_name, that means rg doesn't exist no need to look for CA + custom_env_name = None + # User passes environment, check if it exists or not + if managed_env and not custom_rg_name: + try: + env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) + except: + env_list = [] # Server error, not sure what to do here + + env_list = [x for x in env_list if x['name'].lower() == managed_env.split('/')[-1].lower()] + if len(env_list) == 1: + managed_env = env_list[0]["id"] + resource_group_name = managed_env.split('/')[4] + if len(env_list) > 1: + raise ValidationError("Multiple environments found on subscription with name {}. Specify resource id of the environment.".format(managed_env.split('/')[-1])) + if len(env_list) == 0: + custom_env_name = managed_env.split('/')[-1] + managed_env = None + + # Look for existing containerapp with same name if not resource_group_name and not custom_rg_name: try: - rg_found = False containerapps = list_containerapp(cmd) - for containerapp in containerapps: - if containerapp["name"].lower() == name.lower(): - if rg_found: - raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) - # could also just do resource_group_name = None here and create a new one, ask Anthony - # break - if containerapp["id"][0] != '/': - containerapp["id"] = '/' + containerapp["id"] - rg_found = True - resource_group_name = containerapp["id"].split('/')[4] except: - pass + containerapps = [] # Server error, not sure what to do here + + containerapps = [x for x in containerapps if x['name'].lower() == name.lower()] + if len(containerapps) == 1: + # if containerapps[0]["properties"]["managedEnvironmentId"] == managed_env: + resource_group_name = containerapps[0]["id"].split('/')[4] + managed_env = containerapps[0]["properties"]["managedEnvironmentId"] + if custom_env_name: + # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + if len(containerapps) > 1: + raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) + + if not managed_env and not custom_rg_name and not custom_env_name: + try: + env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) + except: + env_list = [] # server error + + if logs_customer_id: + env_list = [x for x in env_list if 'logAnalyticsConfiguration' in x['properties']['appLogsConfiguration'] and x['properties']['appLogsConfiguration']['logAnalyticsConfiguration']['customerId'] == logs_customer_id] + if location: + env_list = [x for x in env_list if x['location'] == location] + if len(env_list) == 0: + managed_env = None + else: + # check how many CA in env + managed_env = env_list[0]["id"] + resource_group_name = managed_env.split('/')[4] + + if not location: + location = _get_default_containerapps_location(cmd) containerapp_def = None try: @@ -2324,10 +2355,9 @@ def containerapp_up(cmd, except: pass - env_name = "" if not managed_env else managed_env.split('/')[6] + env_name = "" if not managed_env else managed_env.split('/')[-1] if not containerapp_def: if not resource_group_name: - new_rg = "New" user = get_profile_username() rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name if not dryrun: @@ -2335,18 +2365,23 @@ def containerapp_up(cmd, create_resource_group(cmd, rg_name, location) resource_group_name = rg_name if not managed_env: - new_managed_env = "New" - env_name = "{}-env".format(name).replace("_", "-") + env_name = custom_env_name if custom_env_name else "{}-env".format(name).replace("_", "-") if not dryrun: - logger.warning("Creating new managed environment {}".format(env_name)) - managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + try: + managed_env = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name)["id"] + logger.info("Using existing managed environment {}".format(env_name)) + except: + logger.warning("Creating new managed environment {}".format(env_name)) + managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] else: managed_env = env_name else: - new_ca = "Existing" location = containerapp_def["location"] + # This should be be defined no matter what + if custom_env_name: + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") managed_env = containerapp_def["properties"]["managedEnvironmentId"] - env_name = containerapp_def["properties"]["managedEnvironmentId"].split('/')[8] + env_name = managed_env.split('/')[-1] if logs_customer_id and logs_key: if not dryrun: managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] @@ -2354,12 +2389,12 @@ def containerapp_up(cmd, if image is not None and "azurecr.io" in image and not dryrun: if registry_user is None or registry_pass is None: # If registry is Azure Container Registry, we can try inferring credentials - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') registry_server = image.split('/')[0] parsed = urlparse(image) 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) # TODO this will always fail with "too many values to unpack" + registry_user, registry_pass = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex @@ -2374,7 +2409,7 @@ def containerapp_up(cmd, raise ValidationError("Cannot supply non-Azure registry when using --source.") if not dryrun and (registry_user is None or registry_pass is None): # If registry is Azure Container Registry, we can try inferring credentials - logger.warning('No credential was provided to access Azure Container Registry. Trying to look up...') + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') parsed = urlparse(registry_server) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: @@ -2382,10 +2417,9 @@ def containerapp_up(cmd, except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex else: - new_cr = "New" registry_rg = resource_group_name user = get_profile_username() - registry_name = "{}acr".format(name) + registry_name = "{}acr".format(name).replace('-','') registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") if not dryrun: logger.warning("Creating new acr {}".format(registry_name)) @@ -2397,21 +2431,13 @@ def containerapp_up(cmd, image_name = image if image is not None else name from datetime import datetime now = datetime.now() + # Add version tag for acr image image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + image = registry_server + '/' + image_name if not dryrun: queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) - _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) - - log_analytics_workspace_name = "" - env_def = None - try: - env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) - except: - pass - if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": - env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] - log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) + # _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) containerapp_def = None @@ -2420,28 +2446,40 @@ def containerapp_up(cmd, "without the --dryrun flag to create & deploy a new containerapp.") else: containerapp_def = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) + location = containerapp_def["location"] fqdn = "" dry_run = { + "name" : name, + "resourcegroup" : resource_group_name, + "environment" : env_name, "location" : location, "registry": registry_server, "image": image, - "src_path": _src_path_escaped + "src_path": src_dir, + "registry": registry_server } - dry_run["name"] = "{} ({})".format(name, new_ca) - dry_run["resourcegroup"] = "{} ({})".format(resource_group_name, new_rg) - dry_run["environment"] = "{} ({})".format(env_name, new_managed_env) - if registry_server: - dry_run["registry"] = "{} ({})".format(registry_server, new_cr) if containerapp_def: r = containerapp_def if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: - fqdn = r["properties"]["configuration"]["ingress"]["fqdn"] + fqdn = "https://" + r["properties"]["configuration"]["ingress"]["fqdn"] + + log_analytics_workspace_name = "" + env_def = None + try: + env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) + except: + pass + if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": + env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] + log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) if len(fqdn) > 0: dry_run["fqdn"] = fqdn + if len(log_analytics_workspace_name) > 0: dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name + return dry_run From 3e1dca2a0ce8200346e0d21dd55bfc3b673a7340 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 20 Apr 2022 11:16:44 -0700 Subject: [PATCH 142/158] start up refactor --- .../azext_containerapp/_clients.py | 6 +- src/containerapp/azext_containerapp/_utils.py | 21 +- src/containerapp/azext_containerapp/custom.py | 279 +++++++++++------- 3 files changed, 195 insertions(+), 111 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 05e6c4a3479..7986ca00df8 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -367,7 +367,7 @@ def list_replicas(cls, cmd, resource_group_name, container_app_name, revision_na resource_group_name, container_app_name, revision_name, - NEW_API_VERSION) + PREVIEW_API_VERSION) r = send_raw_request(cmd.cli_ctx, "GET", request_url) j = r.json() @@ -395,7 +395,7 @@ def get_replica(cls, cmd, resource_group_name, container_app_name, revision_name container_app_name, revision_name, replica_name, - NEW_API_VERSION) + PREVIEW_API_VERSION) r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() @@ -410,7 +410,7 @@ def get_auth_token(cls, cmd, resource_group_name, name): sub_id, resource_group_name, name, - NEW_API_VERSION) + PREVIEW_API_VERSION) r = send_raw_request(cmd.cli_ctx, "POST", request_url) return r.json() diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 43d8e3a26ff..4bd02bccd26 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -14,7 +14,7 @@ ResourceNotFoundError, ArgumentUsageError) 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 msrestazure.tools import parse_resource_id, is_valid_resource_id from ._clients import ContainerAppClient from ._client_factory import handle_raw_exception, providers_client_factory, cf_resource_groups, log_analytics_client_factory, log_analytics_shared_key_client_factory @@ -345,7 +345,24 @@ def _get_default_log_analytics_location(cmd): return default_location -def _get_default_containerapps_location(cmd): +def get_container_app_if_exists(cmd, resource_group_name, name): + app = None + try: + app = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + return app + + +def _get_name(name_or_rid): + if is_valid_resource_id(name_or_rid): + return parse_resource_id(name_or_rid)["name"] + return name_or_rid + + +def _get_default_containerapps_location(cmd, location=None): + if location: + return location default_location = "eastus" providers_client = None try: diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 58602541eb8..d61fe96eeb1 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -16,7 +16,8 @@ ResourceNotFoundError, CLIError, CLIInternalError, - InvalidArgumentValueError) + InvalidArgumentValueError, + MutuallyExclusiveArgumentError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import open_page_in_browser, get_file_json from azure.cli.command_modules.appservice._create_util import check_resource_group_exists @@ -58,7 +59,7 @@ _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, - _get_default_containerapps_location) + get_container_app_if_exists, _get_name) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING, remove_token) @@ -2127,7 +2128,7 @@ def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, s # currently assumes docker_file_name is in the root and named "Dockerfile" -def _get_dockerfile_content(repo_url, branch, token): +def _get_dockerfile_content_from_repo(repo_url, branch, token): from github import Github g = Github(token) repo = _repo_url_to_name(repo_url) @@ -2154,6 +2155,7 @@ def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: 'list target_port = parts[i+1] ingress = "external" logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + ingress = "external" if target_port and not ingress else ingress return ingress, target_port @@ -2168,7 +2170,7 @@ def github_up(cmd, registry_password=None, branch="main", token=None, - context_path=None, # TODO shouldn't this really be a path?? + context_path=None, image=None, service_principal_client_id=None, service_principal_client_secret=None, @@ -2186,7 +2188,7 @@ def github_up(cmd, managed_env, env_resource_group_name = _get_or_create_managed_env(cmd, resource_group_name, managed_env, name) registry_url, registry_password, registry_username = _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username) service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id) - dockerfile_content = _get_dockerfile_content(repo, branch, token) + dockerfile_content = _get_dockerfile_content_from_repo(repo, branch, token) ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) logger.warning(f"Creating Container App {name} in resource group {resource_group_name}") @@ -2233,58 +2235,41 @@ def github_up(cmd, return dry_run +# up utils -- TODO move to their own file -def containerapp_up(cmd, - name, - resource_group_name=None, - managed_env=None, - location=None, - registry_server=None, - image=None, - source=None, - dockerfile="Dockerfile", - # compose=None, - ingress=None, - target_port=None, - registry_user=None, - registry_pass=None, - env_vars=None, - dryrun=False, - logs_customer_id=None, - logs_key=None): - import os - import json - src_dir = os.getcwd() - _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) - quiet = False - - if source is None and image is None: - raise RequiredArgumentMissingError("You must specify either --source or --image.") - - # if source and image: - # raise ValidationError("You cannot specify both --source and --image.") +def _validate_up_args(source, image, repo): + if not source and not image and not repo: + raise RequiredArgumentMissingError("You must specify either --source, --repo, or --image") + if source and repo: + raise MutuallyExclusiveArgumentError("Cannot use --source and --repo togther. " + "Can either deploy from a local directory or a Github repo") +def _reformat_image(source, image): if source and image: image = image.split('/')[-1] # if link is given image = image.replace(':', '') + return image - # Open dockerfile and check for EXPOSE +def _get_dockerfile_content_local(source, dockerfile): + lines = [] if source: - dockerfile_location = source + '/' + dockerfile + dockerfile_location = f"{source}/{dockerfile}" try: with open(dockerfile_location, 'r') as fh: - for line in fh: - if "EXPOSE" in line: - if not target_port: - target_port = line.replace('\n', '').split(" ")[1] - logger.info("Adding external ingress port {} based on dockerfile expose.".format(target_port)) - break + lines = [line for line in fh] except: - raise InvalidArgumentValueError("Cannot find specified Dockerfile. Check dockerfile name and/or path.") + raise InvalidArgumentValueError("Cannot open specified Dockerfile. Check dockerfile name, path, and permissions.") + return lines + + +def _get_dockerfile_content(repo, branch, token, source, dockerfile): + if source: + return _get_dockerfile_content_local(source, dockerfile) + return _get_dockerfile_content_from_repo(repo, branch, token) - ingress = "external" if target_port and not ingress else ingress - custom_rg_name = None +# User passes RG, check if it exists or not +def _get_resource_group(cmd, resource_group_name, custom_rg_name=None): # User passes non-existing rg, we create it for them if resource_group_name: try: @@ -2292,25 +2277,33 @@ def containerapp_up(cmd, except: custom_rg_name = resource_group_name resource_group_name = None + return resource_group_name, custom_rg_name - custom_env_name = None - # User passes environment, check if it exists or not + +# User passes environment, check if it exists or not +def _get_managed_env(cmd, resource_group_name, custom_rg_name, managed_env, custom_env_name=None): if managed_env and not custom_rg_name: try: env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) except: env_list = [] # Server error, not sure what to do here - env_list = [x for x in env_list if x['name'].lower() == managed_env.split('/')[-1].lower()] + managed_env_name = managed_env + if is_valid_resource_id(managed_env): + managed_env_name = parse_resource_id(managed_env)["name"].lower() + env_list = [x for x in env_list if x['name'].lower() == managed_env_name] if len(env_list) == 1: managed_env = env_list[0]["id"] - resource_group_name = managed_env.split('/')[4] - if len(env_list) > 1: - raise ValidationError("Multiple environments found on subscription with name {}. Specify resource id of the environment.".format(managed_env.split('/')[-1])) - if len(env_list) == 0: - custom_env_name = managed_env.split('/')[-1] + elif len(env_list) > 1: + raise ValidationError(f"Multiple environments found on subscription with name {managed_env_name}. " + "Specify resource id of the environment.") + else: + custom_env_name = managed_env_name managed_env = None + return managed_env, custom_env_name + +def _get_app_env_and_group(cmd, resource_group_name, custom_rg_name, custom_env_name, name): # Look for existing containerapp with same name if not resource_group_name and not custom_rg_name: try: @@ -2321,14 +2314,18 @@ def containerapp_up(cmd, containerapps = [x for x in containerapps if x['name'].lower() == name.lower()] if len(containerapps) == 1: # if containerapps[0]["properties"]["managedEnvironmentId"] == managed_env: - resource_group_name = containerapps[0]["id"].split('/')[4] + resource_group_name = containerapps[0]["resourceGroup"] managed_env = containerapps[0]["properties"]["managedEnvironmentId"] if custom_env_name: # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") if len(containerapps) > 1: - raise ValidationError("There are multiple containerapps with name {} on the subscription. Please specify which resource group your Containerapp is in.".format(name)) + raise ValidationError(f"There are multiple containerapps with name {name} on the subscription. " + "Please specify which resource group your Containerapp is in.") + return managed_env, resource_group_name + +def _get_env_and_group_from_log_analytics(cmd, managed_env, custom_rg_name, custom_env_name, resource_group_name, logs_customer_id, location): if not managed_env and not custom_rg_name and not custom_env_name: try: env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) @@ -2336,26 +2333,18 @@ def containerapp_up(cmd, env_list = [] # server error if logs_customer_id: + # TODO use safe_get here env_list = [x for x in env_list if 'logAnalyticsConfiguration' in x['properties']['appLogsConfiguration'] and x['properties']['appLogsConfiguration']['logAnalyticsConfiguration']['customerId'] == logs_customer_id] if location: env_list = [x for x in env_list if x['location'] == location] - if len(env_list) == 0: - managed_env = None - else: - # check how many CA in env + if env_list: + # TODO check how many CA in env managed_env = env_list[0]["id"] - resource_group_name = managed_env.split('/')[4] + resource_group_name = parse_resource_id(managed_env)["resource_group"] + return managed_env, resource_group_name - if not location: - location = _get_default_containerapps_location(cmd) - containerapp_def = None - try: - containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except: - pass - - env_name = "" if not managed_env else managed_env.split('/')[-1] +def _create_group_and_env(cmd, containerapp_def, resource_group_name, custom_rg_name, dryrun, location, logs_key, logs_customer_id, managed_env, custom_env_name, name): if not containerapp_def: if not resource_group_name: user = get_profile_username() @@ -2386,59 +2375,75 @@ def containerapp_up(cmd, if not dryrun: managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] + return managed_env, env_name + + +def _get_acr_from_image(cmd, image, dryrun, registry_user, registry_pass): if image is not None and "azurecr.io" in image and not dryrun: if registry_user is None or registry_pass is None: # If registry is Azure Container Registry, we can try inferring credentials logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - registry_server = image.split('/')[0] + registry_server = image.split('/')[0] # TODO should validate this is the same as the registry_server param? parsed = urlparse(image) 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) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + return registry_server, registry_user, registry_pass - if source is not None: - if containerapp_def: - if "registries" in containerapp_def["properties"]["configuration"] and len(containerapp_def["properties"]["configuration"]["registries"]) == 1: + +def _get_registry_from_app(containerapp_def, registry_server): + if containerapp_def: + if "registries" in containerapp_def["properties"]["configuration"] and len(containerapp_def["properties"]["configuration"]["registries"]) == 1: # TODO replace with safe_get registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] - registry_name = "" - registry_rg = "" - if registry_server: - if "azurecr.io" not in registry_server: - raise ValidationError("Cannot supply non-Azure registry when using --source.") - if not dryrun and (registry_user is None or registry_pass is None): - # If registry is Azure Container Registry, we can try inferring credentials - logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - parsed = urlparse(registry_server) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: - registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) - except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex - else: - registry_rg = resource_group_name - user = get_profile_username() - registry_name = "{}acr".format(name).replace('-','') - registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") - if not dryrun: - logger.warning("Creating new acr {}".format(registry_name)) - registry_def = create_new_acr(cmd, registry_name, registry_rg, location) - registry_server = registry_def.login_server - else: - registry_server = registry_name + ".azurecr.io" + return registry_server - image_name = image if image is not None else name - from datetime import datetime - now = datetime.now() - # Add version tag for acr image - image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) - image = registry_server + '/' + image_name +def _get_registry_details(cmd, registry_server, dryrun, registry_user, registry_pass, resource_group_name, location, name): + registry_name = "" + registry_rg = "" + if registry_server: + if "azurecr.io" not in registry_server: + raise ValidationError("Cannot supply non-Azure registry when using --source.") + if not dryrun and (registry_user is None or registry_pass is None): + # If registry is Azure Container Registry, we can try inferring credentials + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + else: + registry_rg = resource_group_name + user = get_profile_username() + registry_name = "{}acr".format(name).replace('-','') + registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") if not dryrun: - queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) - # _set_webapp_up_default_args(cmd, resource_group_name, location, name, registry_server) + logger.warning("Creating new acr {}".format(registry_name)) + registry_def = create_new_acr(cmd, registry_name, registry_rg, location) + registry_server = registry_def.login_server + else: + registry_server = registry_name + ".azurecr.io" + return registry_user, registry_pass, registry_rg, registry_name + + +def _run_acr_build(cmd, image, name, registry_server, dryrun, registry_rg, registry_name, source, dockerfile, quiet): + image_name = image if image is not None else name + from datetime import datetime + now = datetime.now() + # Add version tag for acr image + image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + + image = registry_server + '/' + image_name + if not dryrun: + queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) + + return image + +def _create_and_format_output(cmd, dryrun, name, resource_group_name, image, managed_env, target_port, ingress, registry_server, registry_pass, registry_user, env_vars, env_name, src_dir): containerapp_def = None if dryrun: @@ -2483,3 +2488,65 @@ def containerapp_up(cmd, dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name return dry_run + + +def containerapp_up(cmd, + name, + resource_group_name=None, + managed_env=None, + location=None, + registry_server=None, + image=None, + source=None, + dockerfile="Dockerfile", + # compose=None, + ingress=None, + target_port=None, + registry_user=None, + registry_pass=None, + env_vars=None, + dryrun=False, + logs_customer_id=None, + logs_key=None, + repo=None, + branch=None): + import os + import json + src_dir = os.getcwd() + _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) + quiet = False + + _validate_up_args(source, image, repo) + + image = _reformat_image(source, image) # TODO revisit for repo + token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"]) + + dockerfile_content = _get_dockerfile_content(repo, branch, token, source, dockerfile) + ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) + + # determine if the RG passed in exists or we need to make a new one + resource_group_name, custom_rg_name = _get_resource_group(cmd, resource_group_name) + # determine if the env passed in exists or we need to make a new one + managed_env, custom_env_name = _get_managed_env(cmd, resource_group_name, custom_rg_name, managed_env) + + # if a singular app exists with the same name, get its env and rg + managed_env, resource_group_name = _get_app_env_and_group(cmd, resource_group_name, custom_rg_name, custom_env_name, name) + managed_env, resource_group_name = _get_env_and_group_from_log_analytics(cmd, managed_env, custom_rg_name, custom_env_name, resource_group_name, logs_customer_id, location) + + location = _get_default_containerapps_location(cmd, location) + + containerapp_def = get_container_app_if_exists(cmd, resource_group_name, name) + + env_name = _get_name(managed_env) + + # if not dry run, create the RG (if needed) and managed env (if needed) + managed_env, env_name = _create_group_and_env(cmd, containerapp_def, resource_group_name, custom_rg_name, dryrun, location, logs_key, logs_customer_id, managed_env, custom_env_name, name) + + registry_server, registry_user, registry_pass = _get_acr_from_image(cmd, image, dryrun, registry_user, registry_pass) + + if source is not None: + registry_server = _get_registry_from_app(containerapp_def) + registry_user, registry_pass, registry_rg, registry_name = _get_registry_details(cmd, registry_server, dryrun, registry_user, registry_pass, resource_group_name, location, name) + image = _run_acr_build(cmd, image, name, registry_server, dryrun, registry_rg, registry_name, source, dockerfile, quiet) + + return _create_and_format_output(cmd, dryrun, name, resource_group_name, image, managed_env, target_port, ingress, registry_server, registry_pass, registry_user, env_vars, env_name, src_dir) From 3526d6773097b56e0c0fb52149ea64fd64486556 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 20 Apr 2022 16:56:46 -0700 Subject: [PATCH 143/158] add --repo to up and refactor up --- src/containerapp/azext_containerapp/custom.py | 681 ++++++++---------- 1 file changed, 305 insertions(+), 376 deletions(-) diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index d61fe96eeb1..6558c6171d4 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1068,11 +1068,13 @@ def _validate_github(repo, branch, token): raise CLIInternalError(error_msg) from e -def _await_github_action(token, repo, branch, name): +def _await_github_action(cmd, token, repo, branch, name, resource_group_name, timeout=300): from github import Github from time import sleep from ._clients import PollingAnimation + from datetime import datetime + start = datetime.utcnow() animation = PollingAnimation() animation.tick() @@ -1082,18 +1084,23 @@ def _await_github_action(token, repo, branch, name): workflow = None - while workflow is None: # TODO timeout? + while workflow is None: workflows = github_repo.get_workflows() animation.flush() for wf in workflows: if wf.path.startswith(f".github/workflows/{name}") and "Trigger auto deployment for containerapp" in wf.name: workflow = wf break + + gh_action_status = safe_get(show_github_action(cmd, name, resource_group_name), "properties", "operationState") + if gh_action_status == "Failed": + raise CLIInternalError("The Github Action creation failed.") sleep(1) animation.tick() - # print(workflow) - # workflow.create_dispatch(ref=github_repo.get_branch(branch)) + if (datetime.utcnow() - start).seconds >= timeout: + raise CLIInternalError("Timed out while waiting for the Github action to start.") + animation.flush() animation.tick(); animation.flush() run = workflow.get_runs()[0] @@ -1101,12 +1108,14 @@ def _await_github_action(token, repo, branch, name): logger.warning("Waiting for deployment to complete...") run_id = run.id status = run.status - while status == "queued" or status == "in_progress": # TODO timeout? + while status == "queued" or status == "in_progress": sleep(3) - # animation.tick() + animation.tick() status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] - # print(status) - # animation.flush() + animation.flush() + if (datetime.utcnow() - start).seconds >= timeout: + raise CLIInternalError("Timed out while waiting for the Github action to start.") + if status != "completed": raise ValidationError(f"Github action deployment ended with status: {status}") @@ -1133,7 +1142,8 @@ def create_or_update_github_action(cmd, context_path=None, service_principal_client_id=None, service_principal_client_secret=None, - service_principal_tenant_id=None): + service_principal_tenant_id=None, + no_wait=False): if not token and not login_with_github: raise_missing_token_suggestion() elif not token: @@ -1204,10 +1214,9 @@ def create_or_update_github_action(cmd, try: logger.warning("Creating Github action...") - r = GitHubActionClient.create_or_update(cmd=cmd, resource_group_name=resource_group_name, name=name, github_action_envelope=source_control_info, headers=headers) - _await_github_action(token, repo, branch, name) - # from ._ssh_utils import ping_container_app - # ping_container_app(ContainerAppClient.show(cmd, resource_group_name, name)) + r = GitHubActionClient.create_or_update(cmd=cmd, resource_group_name=resource_group_name, name=name, github_action_envelope=source_control_info, headers=headers, no_wait=no_wait) + if not no_wait: + _await_github_action(cmd, token, repo, branch, name, resource_group_name) return r except Exception as e: handle_raw_exception(e) @@ -1605,7 +1614,7 @@ def set_registry(cmd, name, resource_group_name, server, username=None, password registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: - username, password = _get_acr_cred(cmd.cli_ctx, registry_name) # TODO this will always fail with "too many values to unpack" + username, password, _ = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex @@ -2030,74 +2039,17 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev print(line.decode("utf-8").replace("\\u0022", "\u0022").replace("\\u001B", "\u001B").replace("\\u002B", "\u002B").replace("\\u0027", "\u0027")) -# TODO remove bc of rune's def open_containerapp_in_browser(cmd, name, resource_group_name): app = ContainerAppClient.show(cmd, resource_group_name, name) url = safe_get(app, "properties", "configuration", "ingress", "fqdn") if not url: - raise ValidationError("Could not find an external URL for this app") + raise ValidationError("Could not open in browser: no public URL for this app") if not url.startswith("http"): url = f"http://{url}" open_page_in_browser(url) -# TODO move all these helper methods to their own file - -def _get_or_create_resource_group(cmd, resource_group_name): - location = "eastus" # TODO revisit this choice - if resource_group_name: - if check_resource_group_exists(cmd, resource_group_name): - return resource_group_name - else: - from random import randint - resource_group_name = "container_app_group_{:04}".format(randint(0, 9999)) - logger.warning(f"Creating resource group: {resource_group_name}") - create_resource_group(cmd, resource_group_name, location) - return resource_group_name - - -def _get_or_create_name(cmd, resource_group_name, name): - if name: - return name - - from random import choice - noun = choice(get_file_json(GENERATE_RANDOM_APP_NAMES)['APP_NAME_NOUNS']) - adjective = choice(get_file_json(GENERATE_RANDOM_APP_NAMES)['APP_NAME_ADJECTIVES']) - return f"{adjective}{noun}" - - -def _get_or_create_managed_env(cmd, resource_group_name, managed_env, app_name): - if managed_env: - if is_valid_resource_id(managed_env): - parsed = parse_resource_id(managed_env) - return parsed["name"], parsed["resource_group"] - return managed_env, resource_group_name # TODO test for existance - envs = [e for e in ManagedEnvironmentClient.list_by_subscription(cmd) if e["location"] != "northcentralusstage"] # TODO remove - if len(envs) == 0: - managed_env = managed_env or f"{app_name}-env" # TODO better name? - logger.warning(f"Creating Container App Environment {envs[0]['name']}") - return create_managed_environment(cmd, managed_env, resource_group_name)["name"], resource_group_name - logger.warning(f"Using Container App Environment: {envs[0]['name']}") - return envs[0]["id"], parse_resource_id(envs[0]["id"])["resource_group"] # TODO better selection logic (<5 apps in the selected sub) - - -def _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username): - from azure.cli.command_modules.acr.custom import acr_list - from azure.mgmt.containerregistry import ContainerRegistryManagementClient - from azure.cli.core.commands.client_factory import get_mgmt_service_client - - client = get_mgmt_service_client(cmd.cli_ctx, ContainerRegistryManagementClient).registries - - if not registry_url and not registry_password and not registry_username: - acrs = list(acr_list(client)) - assert len(acrs) != 0 # TODO create an ACR if registry not provided or found - registry_url = acrs[0].login_server - registry_username, registry_password, _ = _get_acr_cred(cmd.cli_ctx, acrs[0].name) - logger.warning(f"Using Azure Container Registry: {acrs[0].name}") - - # TODO fetch ACR creds if needed - - return registry_url, registry_password, registry_username +# up utils -- TODO move to their own file def _create_service_principal(cmd, resource_group_name, env_resource_group_name): logger.warning("No valid service principal provided. Creating a new service principal...") @@ -2127,16 +2079,14 @@ def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, s # return client_id, secret, tenant_id -# currently assumes docker_file_name is in the root and named "Dockerfile" -def _get_dockerfile_content_from_repo(repo_url, branch, token): +def _get_dockerfile_content_from_repo(repo_url, branch, token, context_path, dockerfile): from github import Github g = Github(token) repo = _repo_url_to_name(repo_url) r = g.get_repo(repo) - # TODO support repos with dockerfile outside the root - files = r.get_contents(".", ref=branch) + files = r.get_contents(context_path, ref=branch) for f in files: - if f.path == "Dockerfile": + if f.path == dockerfile or f.path.endswith(f"/{dockerfile}"): resp = requests.get(f.download_url) if resp.ok and resp.content: return resp.content.decode("utf-8").split("\n") @@ -2159,84 +2109,6 @@ def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: 'list return ingress, target_port -# create an app from GH repo -def github_up(cmd, - repo, - name=None, - resource_group_name=None, - managed_env=None, - registry_url=None, - registry_username=None, - registry_password=None, - branch="main", - token=None, - context_path=None, - image=None, - service_principal_client_id=None, - service_principal_client_secret=None, - service_principal_tenant_id=None, - ingress=None, - target_port=None): - - if not token: - scopes = ["admin:repo_hook", "repo", "workflow"] - token = get_github_access_token(cmd, scopes) - - # TODO will use up's behavior getting defaults/creating resources - resource_group_name = _get_or_create_resource_group(cmd, resource_group_name) - name = _get_or_create_name(cmd, resource_group_name, name) - managed_env, env_resource_group_name = _get_or_create_managed_env(cmd, resource_group_name, managed_env, name) - registry_url, registry_password, registry_username = _get_or_create_registry(cmd, name, resource_group_name, registry_url, registry_password, registry_username) - service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id) - dockerfile_content = _get_dockerfile_content_from_repo(repo, branch, token) - ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) - - logger.warning(f"Creating Container App {name} in resource group {resource_group_name}") - app = create_containerapp(cmd=cmd, - name=name, - resource_group_name=resource_group_name, - image=None, # GH action handles setting this property - managed_env=managed_env, - target_port=target_port, - registry_server=None, # GH action handles setting this property - registry_pass=None, # GH action handles setting this property - registry_user=None, # GH action handles setting this property - env_vars=None, # TODO - ingress=ingress, - disable_warnings=True, - min_replicas=1) # TODO - - gh_action = create_or_update_github_action(cmd=cmd, - name=name, - resource_group_name=resource_group_name, - repo_url=repo, - registry_url=registry_url, - registry_username=registry_username, - registry_password=registry_password, - branch=branch, - token=token, - login_with_github=False, - service_principal_client_id=service_principal_client_id, - service_principal_client_secret=service_principal_client_secret, - service_principal_tenant_id=service_principal_tenant_id, - image=image, - context_path=context_path) - - dry_run = { - "name": name, - "resourceGroup": resource_group_name, - "environment": managed_env, - "fqdn": f'https://{safe_get(app, "properties", "configuration", "ingress", "fqdn")}', - "location" : app["location"], - "registry": registry_url, - "image": gh_action["properties"]["githubActionConfiguration"]["image"], - "githubAction": gh_action - } - - return dry_run - -# up utils -- TODO move to their own file - def _validate_up_args(source, image, repo): if not source and not image and not repo: raise RequiredArgumentMissingError("You must specify either --source, --repo, or --image") @@ -2244,8 +2116,8 @@ def _validate_up_args(source, image, repo): raise MutuallyExclusiveArgumentError("Cannot use --source and --repo togther. " "Can either deploy from a local directory or a Github repo") -def _reformat_image(source, image): - if source and image: +def _reformat_image(source, repo, image): + if source and (image or repo): image = image.split('/')[-1] # if link is given image = image.replace(':', '') return image @@ -2268,226 +2140,248 @@ def _get_dockerfile_content(repo, branch, token, source, dockerfile): return _get_dockerfile_content_from_repo(repo, branch, token) -# User passes RG, check if it exists or not -def _get_resource_group(cmd, resource_group_name, custom_rg_name=None): - # User passes non-existing rg, we create it for them - if resource_group_name: - try: - get_resource_group(cmd, resource_group_name) - except: - custom_rg_name = resource_group_name - resource_group_name = None - return resource_group_name, custom_rg_name - - -# User passes environment, check if it exists or not -def _get_managed_env(cmd, resource_group_name, custom_rg_name, managed_env, custom_env_name=None): - if managed_env and not custom_rg_name: - try: - env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) - except: - env_list = [] # Server error, not sure what to do here - - managed_env_name = managed_env - if is_valid_resource_id(managed_env): - managed_env_name = parse_resource_id(managed_env)["name"].lower() - env_list = [x for x in env_list if x['name'].lower() == managed_env_name] - if len(env_list) == 1: - managed_env = env_list[0]["id"] - elif len(env_list) > 1: - raise ValidationError(f"Multiple environments found on subscription with name {managed_env_name}. " - "Specify resource id of the environment.") - else: - custom_env_name = managed_env_name - managed_env = None - return managed_env, custom_env_name - - -def _get_app_env_and_group(cmd, resource_group_name, custom_rg_name, custom_env_name, name): - # Look for existing containerapp with same name - if not resource_group_name and not custom_rg_name: - try: - containerapps = list_containerapp(cmd) - except: - containerapps = [] # Server error, not sure what to do here - - containerapps = [x for x in containerapps if x['name'].lower() == name.lower()] - if len(containerapps) == 1: - # if containerapps[0]["properties"]["managedEnvironmentId"] == managed_env: - resource_group_name = containerapps[0]["resourceGroup"] - managed_env = containerapps[0]["properties"]["managedEnvironmentId"] - if custom_env_name: - # raise ValidationError("You cannot update the environment of an existing containerapp. Try re-running the command without --environment.") - logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") - if len(containerapps) > 1: +def _get_app_env_and_group(cmd, name, resource_group: 'ResourceGroup', env: 'ContainerAppEnvironment'): + if not resource_group.name and not resource_group.exists: + matched_apps = [c for c in list_containerapp(cmd) if c['name'].lower() == name.lower()] + if len(matched_apps) == 1: + if env.name: + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + resource_group.name = matched_apps[0]["resourceGroup"] + env.name = matched_apps[0]["properties"]["managedEnvironmentId"] + elif len(matched_apps) > 1: raise ValidationError(f"There are multiple containerapps with name {name} on the subscription. " - "Please specify which resource group your Containerapp is in.") - return managed_env, resource_group_name + "Please specify which resource group your Containerapp is in.") -def _get_env_and_group_from_log_analytics(cmd, managed_env, custom_rg_name, custom_env_name, resource_group_name, logs_customer_id, location): - if not managed_env and not custom_rg_name and not custom_env_name: - try: +def _get_env_and_group_from_log_analytics(cmd, resource_group_name, env:'ContainerAppEnvironment', resource_group:'ResourceGroup', logs_customer_id, location): + # resource_group_name is the value the user passed in (if present) + if not env.name: + if (resource_group_name == resource_group.name and resource_group.exists) or (not resource_group_name): env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) - except: - env_list = [] # server error - - if logs_customer_id: - # TODO use safe_get here - env_list = [x for x in env_list if 'logAnalyticsConfiguration' in x['properties']['appLogsConfiguration'] and x['properties']['appLogsConfiguration']['logAnalyticsConfiguration']['customerId'] == logs_customer_id] - if location: - env_list = [x for x in env_list if x['location'] == location] - if env_list: - # TODO check how many CA in env - managed_env = env_list[0]["id"] - resource_group_name = parse_resource_id(managed_env)["resource_group"] - return managed_env, resource_group_name - - -def _create_group_and_env(cmd, containerapp_def, resource_group_name, custom_rg_name, dryrun, location, logs_key, logs_customer_id, managed_env, custom_env_name, name): - if not containerapp_def: - if not resource_group_name: - user = get_profile_username() - rg_name = get_randomized_name(user, resource_group_name) if custom_rg_name is None else custom_rg_name - if not dryrun: - logger.warning("Creating new resource group {}".format(rg_name)) - create_resource_group(cmd, rg_name, location) - resource_group_name = rg_name - if not managed_env: - env_name = custom_env_name if custom_env_name else "{}-env".format(name).replace("_", "-") - if not dryrun: - try: - managed_env = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name)["id"] - logger.info("Using existing managed environment {}".format(env_name)) - except: - logger.warning("Creating new managed environment {}".format(env_name)) - managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] - else: - managed_env = env_name - else: - location = containerapp_def["location"] - # This should be be defined no matter what - if custom_env_name: - logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") - managed_env = containerapp_def["properties"]["managedEnvironmentId"] - env_name = managed_env.split('/')[-1] - if logs_customer_id and logs_key: - if not dryrun: - managed_env = create_managed_environment(cmd, env_name, location=location, resource_group_name=resource_group_name, logs_key=logs_key, logs_customer_id=logs_customer_id, disable_warnings=True)["id"] - - return managed_env, env_name - - -def _get_acr_from_image(cmd, image, dryrun, registry_user, registry_pass): - if image is not None and "azurecr.io" in image and not dryrun: - if registry_user is None or registry_pass is None: - # If registry is Azure Container Registry, we can try inferring credentials + if logs_customer_id: + env_list = [e for e in env_list if safe_get(e, "properties", "appLogsConfiguration", "logAnalyticsConfiguration", "customerId") == logs_customer_id] + if location: + env_list = [e for e in env_list if e['location'] == location] + if env_list: + # TODO check how many CA in env + env_details = parse_resource_id(env_list[0]["id"]) + env.name = env_details["name"] + resource_group.name = env_details["resource_group"] + + +def _get_acr_from_image(cmd, app): + if app.image is not None and "azurecr.io" in app.image: + if app.registry_user is None or app.registry_pass is None: logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - registry_server = image.split('/')[0] # TODO should validate this is the same as the registry_server param? - parsed = urlparse(image) + app.registry_server = app.image.split('/')[0] # TODO what if this conflicts with registry_server param? + parsed = urlparse(app.image) 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) + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex - return registry_server, registry_user, registry_pass -def _get_registry_from_app(containerapp_def, registry_server): +def _get_registry_from_app(app): + containerapp_def = app.get() if containerapp_def: - if "registries" in containerapp_def["properties"]["configuration"] and len(containerapp_def["properties"]["configuration"]["registries"]) == 1: # TODO replace with safe_get - registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] - return registry_server + if len(safe_get(containerapp_def, "properties", "configuration", "registries")) == 1: + app.registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] -def _get_registry_details(cmd, registry_server, dryrun, registry_user, registry_pass, resource_group_name, location, name): - registry_name = "" - registry_rg = "" - if registry_server: - if "azurecr.io" not in registry_server: +def _get_registry_details(cmd, app: 'ContainerApp'): + registry_rg = None + registry_name = None + if app.registry_server: + if "azurecr.io" not in app.registry_server: raise ValidationError("Cannot supply non-Azure registry when using --source.") - if not dryrun and (registry_user is None or registry_pass is None): - # If registry is Azure Container Registry, we can try inferring credentials + if app.registry_user is None or app.registry_pass is None: logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - parsed = urlparse(registry_server) + parsed = urlparse(app.registry_server) registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: - registry_user, registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex else: - registry_rg = resource_group_name + registry_rg = app.resource_group.name user = get_profile_username() - registry_name = "{}acr".format(name).replace('-','') - registry_name = registry_name + str(hash((registry_rg, user, name))).replace("-", "") - if not dryrun: - logger.warning("Creating new acr {}".format(registry_name)) - registry_def = create_new_acr(cmd, registry_name, registry_rg, location) - registry_server = registry_def.login_server - else: - registry_server = registry_name + ".azurecr.io" - return registry_user, registry_pass, registry_rg, registry_name - - -def _run_acr_build(cmd, image, name, registry_server, dryrun, registry_rg, registry_name, source, dockerfile, quiet): - image_name = image if image is not None else name - from datetime import datetime - now = datetime.now() - # Add version tag for acr image - image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) - - image = registry_server + '/' + image_name - if not dryrun: - queue_acr_build(cmd, registry_rg, registry_name, image_name, source, dockerfile, quiet) + registry_name = "{}acr".format(app.name).replace('-','') + registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "") + app.registry_server = registry_name + ".azurecr.io" + app.should_create_acr = True + app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) + + +class ResourceGroup: + def __init__(self, cmd, name: str, location: str, exists: bool): + self.cmd = cmd + self.name = name + self.location = _get_default_containerapps_location(cmd, location) + self.exists = exists + + self.check_exists() + + def create(self, cmd): + if not self.name: + self.name = get_randomized_name(get_profile_username()) + g = create_resource_group(cmd, self.name, self.location) + self.exists = True + return g + + def _get(self): + return get_resource_group(self.cmd, self.name) + + def get(self): + r = None + try: + r = self._get(self.cmd) + except: + pass + return r - return image + def check_exists(self) -> bool: + self.exists = check_resource_group_exists(self.cmd, self.name) + return self.exists -def _create_and_format_output(cmd, dryrun, name, resource_group_name, image, managed_env, target_port, ingress, registry_server, registry_pass, registry_user, env_vars, env_name, src_dir): - containerapp_def = None +class Resource: + def __init__(self, cmd, name: str, resource_group: 'ResourceGroup', exists: bool=None): + self.cmd = cmd + self.name = name + self.resource_group = resource_group + self.exists = exists - if dryrun: - logger.warning("Containerapp will be created with the below configuration, re-run command " - "without the --dryrun flag to create & deploy a new containerapp.") - else: - containerapp_def = create_containerapp(cmd=cmd, name=name, resource_group_name=resource_group_name, image=image, managed_env=managed_env, target_port=target_port, registry_server=registry_server, registry_pass=registry_pass, registry_user=registry_user, env_vars=env_vars, ingress=ingress, disable_warnings=True) - location = containerapp_def["location"] - - fqdn = "" - - dry_run = { - "name" : name, - "resourcegroup" : resource_group_name, - "environment" : env_name, - "location" : location, - "registry": registry_server, - "image": image, - "src_path": src_dir, - "registry": registry_server - } + self.check_exists(cmd) - if containerapp_def: - r = containerapp_def - if "configuration" in r["properties"] and "ingress" in r["properties"]["configuration"] and "fqdn" in r["properties"]["configuration"]["ingress"]: - fqdn = "https://" + r["properties"]["configuration"]["ingress"]["fqdn"] - log_analytics_workspace_name = "" - env_def = None - try: - env_def = show_managed_environment(cmd=cmd, name=env_name, resource_group_name=resource_group_name) - except: - pass - if env_def and env_def["properties"]["appLogsConfiguration"]["destination"].lower() == "log-analytics": - env_customer_id = env_def["properties"]["appLogsConfiguration"]["logAnalyticsConfiguration"]["customerId"] - log_analytics_workspace_name = _get_log_analytics_workspace_name(cmd, env_customer_id, resource_group_name) + def create(self, *args, **kwargs): + raise NotImplementedError() - if len(fqdn) > 0: - dry_run["fqdn"] = fqdn + def _get(self): + raise NotImplementedError() - if len(log_analytics_workspace_name) > 0: - dry_run["log_analytics_workspace_name"] = log_analytics_workspace_name + def get(self): + r = None + try: + r = self._get(self.cmd) + except: + pass + return r - return dry_run + def check_exists(self): + self.exists = self.get() is None + return self.exists + + +class ContainerAppEnvironment(Resource): + def __init__(self, + cmd, + name: str, + resource_group: 'ResourceGroup', + exists: bool=None, + location=None, + logs_key=None, + logs_customer_id=None): + + super().__init__(cmd, name, resource_group, exists) + if is_valid_resource_id(name): + self.name = parse_resource_id(name)["name"] + rg = parse_resource_id(name)["resource_group"] + if resource_group.name != rg: + self.resource_group = ResourceGroup(cmd, rg, location) + self.location=_get_default_containerapps_location(cmd, location) + self.logs_key=logs_key + self.logs_customer_id=logs_customer_id + + def _get(self, cmd): + return ManagedEnvironmentClient.show(cmd, self.resource_group.name, self.name) + + def create(self, cmd, app_name): + if self.name is None: + self.name = "{}-env".format(app_name).replace("_", "-") + env = create_managed_environment(cmd, + self.name, + location=self.location, + resource_group_name=self.resource_group.name, + logs_key=self.logs_key, + logs_customer_id=self.logs_customer_id, disable_warnings=True) + self.exists = True + return env + + +class AzureContainerRegistry(Resource): + def __init__(self, + name: str, + resource_group: 'ResourceGroup'): + + self.name = name + self.resource_group = resource_group + + +class ContainerApp(Resource): + def __init__(self, + cmd, + name: str, + resource_group: 'ResourceGroup', + exists: bool=None, + image=None, + env: 'ContainerAppEnvironment'=None, + target_port=None, + registry_server=None, + registry_user=None, + registry_pass=None, + env_vars=None, + ingress=None): + + super().__init__(cmd, name, resource_group, exists) + self.image=image + self.env=env + self.target_port=target_port + self.registry_server=registry_server + self.registry_user=registry_user + self.registry_pass = registry_pass + self.env_vars=env_vars + self.ingress=ingress + + self.should_create_acr = False + self.acr: 'AzureContainerRegistry' = None + + def _get(self, cmd): + return ContainerAppClient.show(cmd, self.resource_group, self.name) + + def create(self, cmd): + return create_containerapp(cmd=cmd, + name=self.name, + resource_group_name=self.resource_group.name, + image=self.image, + managed_env=self.env.name, + target_port=self.target_port, + registry_server=self.registry_server, + registry_pass=self.registry_pass, + registry_user=self.registry_user, + env_vars=self.env_vars, + ingress=self.ingress, + disable_warnings=True) + def create_acr(self): + registry_rg = self.resource_group.name + url = self.registry_server + registry_name = url[:url.rindex(".azurecr.io")] + registry_def = create_new_acr(self.cmd, registry_name, registry_rg, self.location) + self.registry_server = registry_def.login_server + + def run_acr_build(self, dockerfile): + image_name = self.image if self.image is not None else self.name + from datetime import datetime + now = datetime.now() + # Add version tag for acr image + image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + + self.registry_rg + + self.image = self.registry_server + '/' + image_name + queue_acr_build(self.cmd, self.registry_rg, self.registry_name, image_name, self.source, dockerfile, quiet=True) def containerapp_up(cmd, @@ -2498,55 +2392,90 @@ def containerapp_up(cmd, registry_server=None, image=None, source=None, - dockerfile="Dockerfile", - # compose=None, ingress=None, target_port=None, registry_user=None, registry_pass=None, env_vars=None, - dryrun=False, logs_customer_id=None, logs_key=None, repo=None, - branch=None): - import os - import json - src_dir = os.getcwd() - _src_path_escaped = "{}".format(src_dir.replace(os.sep, os.sep + os.sep)) - quiet = False + branch=None, + browse=False, + context_path=None, + service_principal_client_id=None, + service_principal_client_secret=None, + service_principal_tenant_id=None): + dockerfile="Dockerfile", # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) _validate_up_args(source, image, repo) - image = _reformat_image(source, image) # TODO revisit for repo + image = _reformat_image(source, repo, image) token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"]) dockerfile_content = _get_dockerfile_content(repo, branch, token, source, dockerfile) ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) - # determine if the RG passed in exists or we need to make a new one - resource_group_name, custom_rg_name = _get_resource_group(cmd, resource_group_name) - # determine if the env passed in exists or we need to make a new one - managed_env, custom_env_name = _get_managed_env(cmd, resource_group_name, custom_rg_name, managed_env) - - # if a singular app exists with the same name, get its env and rg - managed_env, resource_group_name = _get_app_env_and_group(cmd, resource_group_name, custom_rg_name, custom_env_name, name) - managed_env, resource_group_name = _get_env_and_group_from_log_analytics(cmd, managed_env, custom_rg_name, custom_env_name, resource_group_name, logs_customer_id, location) + resource_group = ResourceGroup(name=resource_group_name, location=location) + env = ContainerAppEnvironment(cmd, managed_env, resource_group, location=location, logs_key=logs_key, logs_customer_id=logs_customer_id) + app = ContainerApp(cmd, name, resource_group, None, image, env, target_port, registry_server, registry_user, registry_pass, env_vars, ingress) - location = _get_default_containerapps_location(cmd, location) + # If no RG passed in and a singular app exists with the same name, get its env and rg + _get_app_env_and_group(cmd, name, resource_group, env) - containerapp_def = get_container_app_if_exists(cmd, resource_group_name, name) + # If no env passed in (and not creating a new RG), then try getting an env by location / log analytics ID + _get_env_and_group_from_log_analytics(cmd, resource_group_name, env, resource_group, logs_customer_id, location) - env_name = _get_name(managed_env) + # get ACR details from --image, if possible + _get_acr_from_image(cmd, app) - # if not dry run, create the RG (if needed) and managed env (if needed) - managed_env, env_name = _create_group_and_env(cmd, containerapp_def, resource_group_name, custom_rg_name, dryrun, location, logs_key, logs_customer_id, managed_env, custom_env_name, name) + if source: + registry_server = _get_registry_from_app(app) # if the app exists, get the registry + _get_registry_details(cmd, app) # fetch ACR creds from arguments registry arguments + + if not resource_group.check_exists(): + logger.warning(f"Creating resource group {resource_group.name}") + resource_group.create() + if not env.check_exists(): + logger.warning(f"Creating Containerapp environment {env.name} in resource group {env.resource_group.name}") + env.create(name) + if app._should_create_acr(): + logger.warning(f"Creating Azure Container Registry {app.acr.name} in resource group {app.acr.resource_group.name}") + app.create_acr() - registry_server, registry_user, registry_pass = _get_acr_from_image(cmd, image, dryrun, registry_user, registry_pass) + if source: + app.run_acr_build(dockerfile) + # return image - if source is not None: - registry_server = _get_registry_from_app(containerapp_def) - registry_user, registry_pass, registry_rg, registry_name = _get_registry_details(cmd, registry_server, dryrun, registry_user, registry_pass, resource_group_name, location, name) - image = _run_acr_build(cmd, image, name, registry_server, dryrun, registry_rg, registry_name, source, dockerfile, quiet) + logger.warning(f"Creating Containerapp {app.name} in resource group {app.resource_group.name}") + app.create() - return _create_and_format_output(cmd, dryrun, name, resource_group_name, image, managed_env, target_port, ingress, registry_server, registry_pass, registry_user, env_vars, env_name, src_dir) + if repo: + sp = _get_or_create_sp(cmd, + app.resource_group.name, + env.resource_group.name, + name, + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id) + service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = sp + gh_action = create_or_update_github_action(cmd=cmd, + name=name, + resource_group_name=resource_group_name, + repo_url=repo, + registry_url=registry_server, + registry_username=registry_user, + registry_password=registry_pass, + branch=branch, + token=token, + login_with_github=False, + service_principal_client_id=service_principal_client_id, + service_principal_client_secret=service_principal_client_secret, + service_principal_tenant_id=service_principal_tenant_id, + image=image, + context_path=context_path) + + if browse: + open_containerapp_in_browser(cmd, name, resource_group) + + # TODO output From 057bdc1d1f2b237bfee9f4e77ccde1368510c82b Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Wed, 20 Apr 2022 19:22:31 -0700 Subject: [PATCH 144/158] reorganize code more; fix various bugs --- .../azext_containerapp/_github_oauth.py | 4 +- .../azext_containerapp/_params.py | 2 +- .../azext_containerapp/_up_utils.py | 466 +++++++++++++++++ src/containerapp/azext_containerapp/_utils.py | 61 +++ .../azext_containerapp/commands.py | 1 - src/containerapp/azext_containerapp/custom.py | 469 +----------------- 6 files changed, 551 insertions(+), 452 deletions(-) create mode 100644 src/containerapp/azext_containerapp/_up_utils.py diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py index fe883bcc5d5..41fe984be0d 100644 --- a/src/containerapp/azext_containerapp/_github_oauth.py +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -25,7 +25,9 @@ ] -def get_github_access_token(cmd, scope_list=None): # pylint: disable=unused-argument +def get_github_access_token(cmd, scope_list=None, token=None): # pylint: disable=unused-argument + if token: + return token if scope_list: for scope in scope_list: if scope not in GITHUB_OAUTH_SCOPES: diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index fd76ce1f430..b05878ae226 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -232,7 +232,7 @@ def load_arguments(self, _): c.argument('name', id_part=None) with self.argument_context('containerapp up') as c: - c.argument('resource_group_name', configured_default='resource_group_name') + c.argument('resource_group_name', configured_default='resource_group_name', id_part=None) c.argument('location', configured_default='location') c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py new file mode 100644 index 00000000000..842e0fff7fe --- /dev/null +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -0,0 +1,466 @@ +# -------------------------------------------------------------------------------------------- +# 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, expression-not-assigned, too-many-locals + + +from urllib.parse import urlparse +import requests + +from azure.cli.core.azclierror import ( + RequiredArgumentMissingError, + ValidationError, + InvalidArgumentValueError, + MutuallyExclusiveArgumentError) +from azure.cli.core.commands.client_factory import get_subscription_id +from azure.cli.command_modules.appservice._create_util import check_resource_group_exists +from knack.log import get_logger + +from msrestazure.tools import parse_resource_id, is_valid_resource_id, resource_id + +from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient + +from ._utils import (get_randomized_name, get_profile_username, create_resource_group, + get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, + _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, + repo_url_to_name, get_container_app_if_exists) + +from .custom import (create_managed_environment, create_containerapp, list_containerapp, + list_managed_environments, create_or_update_github_action) + +logger = get_logger(__name__) + +class ResourceGroup: + def __init__(self, cmd, name: str, location: str, exists: bool=None): + self.cmd = cmd + self.name = name + self.location = _get_default_containerapps_location(cmd, location) + self.exists = exists + + self.check_exists() + + def create(self): + if not self.name: + self.name = get_randomized_name(get_profile_username()) + g = create_resource_group(self.cmd, self.name, self.location) + self.exists = True + return g + + def _get(self): + return get_resource_group(self.cmd, self.name) + + def get(self): + r = None + try: + r = self._get(self.cmd) + except: + pass + return r + + def check_exists(self) -> bool: + if self.name is None: + self.exists = False + else: + self.exists = check_resource_group_exists(self.cmd, self.name) + return self.exists + + def create_if_needed(self): + if not self.check_exists(): + logger.warning(f"Creating resoure group '{self.name}'") + self.create() + else: + logger.warning(f"Using resoure group '{self.name}'") # TODO use .info() + + +class Resource: + def __init__(self, cmd, name: str, resource_group: 'ResourceGroup', exists: bool=None): + self.cmd = cmd + self.name = name + self.resource_group = resource_group + self.exists = exists + + self.check_exists() + + + def create(self, *args, **kwargs): + raise NotImplementedError() + + def _get(self): + raise NotImplementedError() + + def get(self): + r = None + try: + r = self._get() + except: + pass + return r + + def check_exists(self): + if self.name is None or self.resource_group.name is None: + self.exists = False + else: + self.exists = self.get() is not None + return self.exists + + def create_if_needed(self, *args, **kwargs): + if not self.check_exists(): + logger.warning(f"Creating {type(self).__name__} '{self.name}' in resource group {self.resource_group.name}") + self.create(*args, **kwargs) + else: + logger.warning(f"Using {type(self).__name__} '{self.name}' in resource group {self.resource_group.name}") # TODO use .info() + + +class ContainerAppEnvironment(Resource): + def __init__(self, + cmd, + name: str, + resource_group: 'ResourceGroup', + exists: bool=None, + location=None, + logs_key=None, + logs_customer_id=None): + + super().__init__(cmd, name, resource_group, exists) + if is_valid_resource_id(name): + self.name = parse_resource_id(name)["name"] + rg = parse_resource_id(name)["resource_group"] + if resource_group.name != rg: + self.resource_group = ResourceGroup(cmd, rg, location) + self.location=_get_default_containerapps_location(cmd, location) + self.logs_key=logs_key + self.logs_customer_id=logs_customer_id + + def set_name(self, name_or_rid): + if is_valid_resource_id(name_or_rid): + self.name = parse_resource_id(name_or_rid)["name"] + rg = parse_resource_id(name_or_rid)["resource_group"] + if self.resource_group.name != rg: + self.resource_group = ResourceGroup(self.cmd, rg, _get_default_containerapps_location(self.cmd, + self.location)) + else: + self.name = name_or_rid + + + def _get(self): + return ManagedEnvironmentClient.show(self.cmd, self.resource_group.name, self.name) + + def create(self, app_name): + if self.name is None: + self.name = "{}-env".format(app_name).replace("_", "-") + env = create_managed_environment(self.cmd, + self.name, + location=self.location, + resource_group_name=self.resource_group.name, + logs_key=self.logs_key, + logs_customer_id=self.logs_customer_id, disable_warnings=True) + self.exists = True + return env + + def get_rid(self): + rid = self.name + if not is_valid_resource_id(self.name): + rid = resource_id(subscription=get_subscription_id(self.cmd.cli_ctx), + resource_group=self.resource_group.name, + namespace='Microsoft.App', + type='managedEnvironments', + name=self.name) + return rid + + +class AzureContainerRegistry(Resource): + def __init__(self, + name: str, + resource_group: 'ResourceGroup'): + + self.name = name + self.resource_group = resource_group + + +class ContainerApp(Resource): + def __init__(self, + cmd, + name: str, + resource_group: 'ResourceGroup', + exists: bool=None, + image=None, + env: 'ContainerAppEnvironment'=None, + target_port=None, + registry_server=None, + registry_user=None, + registry_pass=None, + env_vars=None, + ingress=None): + + super().__init__(cmd, name, resource_group, exists) + self.image=image + self.env=env + self.target_port=target_port + self.registry_server=registry_server + self.registry_user=registry_user + self.registry_pass = registry_pass + self.env_vars=env_vars + self.ingress=ingress + + self.should_create_acr = False + self.acr: 'AzureContainerRegistry' = None + + def _get(self): + return ContainerAppClient.show(self.cmd, self.resource_group.name, self.name) + + def create(self): + if get_container_app_if_exists(self.cmd, self.resource_group.name, self.name): + logger.warning(f"Updating Containerapp {self.name} in resource group {self.resource_group.name}") + else: + logger.warning(f"Creating Containerapp {self.name} in resource group {self.resource_group.name}") + + return create_containerapp(cmd=self.cmd, + name=self.name, + resource_group_name=self.resource_group.name, + image=self.image, + managed_env=self.env.get_rid(), + target_port=self.target_port, + registry_server=self.registry_server, + registry_pass=self.registry_pass, + registry_user=self.registry_user, + env_vars=self.env_vars, + ingress=self.ingress, + disable_warnings=True) + + def create_acr_if_needed(self): + if self.should_create_acr: + logger.warning(f"Creating Azure Container Registry {self.acr.name} in resource group " + f"{self.acr.resource_group.name}") + self.create_acr() + + def create_acr(self): + registry_rg = self.resource_group.name + url = self.registry_server + registry_name = url[:url.rindex(".azurecr.io")] + registry_def = create_new_acr(self.cmd, registry_name, registry_rg, self.location) + self.registry_server = registry_def.login_server + + def run_acr_build(self, dockerfile): + image_name = self.image if self.image is not None else self.name + from datetime import datetime + now = datetime.now() + # Add version tag for acr image + image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + + self.registry_rg + + self.image = self.registry_server + '/' + image_name + queue_acr_build(self.cmd, self.registry_rg, self.registry_name, image_name, self.source, dockerfile, quiet=True) + +# up utils -- TODO move to their own file + +def _create_service_principal(cmd, resource_group_name, env_resource_group_name): + logger.warning("No valid service principal provided. Creating a new service principal...") + scopes = [f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}"] + if env_resource_group_name is not None and env_resource_group_name != resource_group_name: + scopes.append(f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{env_resource_group_name}") + sp = create_service_principal_for_rbac(cmd, scopes=scopes, role="contributor") + + logger.info(f"Created service principal: {sp['displayName']}") + + return sp["appId"], sp["password"], sp["tenant"] + + +def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, + service_principal_client_secret, service_principal_tenant_id): + try: + GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + return service_principal_client_id, service_principal_client_secret, service_principal_tenant_id + except: + service_principal = None + + # TODO if possible, search for SPs with the right credentials + # I haven't found a way to get SP creds + secrets yet from the API + + if not service_principal: + return _create_service_principal(cmd, resource_group_name, env_resource_group_name) + # return client_id, secret, tenant_id + + +def _get_dockerfile_content_from_repo(repo_url, branch, token, context_path, dockerfile): + from github import Github + g = Github(token) + context_path = context_path or "." + repo = repo_url_to_name(repo_url) + r = g.get_repo(repo) + files = r.get_contents(context_path, ref=branch) + for f in files: + if f.path == dockerfile or f.path.endswith(f"/{dockerfile}"): + resp = requests.get(f.download_url) + if resp.ok and resp.content: + return resp.content.decode("utf-8").split("\n") + + +def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: 'list[str]'): + if not target_port and not ingress and dockerfile_content is not None: + for line in dockerfile_content: + if line: + line = line.upper().strip().replace("/TCP", "").replace("/UDP", "").replace("\n","") + if line and line[0] != "#": + if "EXPOSE" in line: + parts = line.split(" ") + for i, p in enumerate(parts[:-1]): + if "EXPOSE" in p and is_int(parts[i+1]): + target_port = parts[i+1] + ingress = "external" + logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + ingress = "external" if target_port and not ingress else ingress + return ingress, target_port + + +def _validate_up_args(source, image, repo): + if not source and not image and not repo: + raise RequiredArgumentMissingError("You must specify either --source, --repo, or --image") + if source and repo: + raise MutuallyExclusiveArgumentError("Cannot use --source and --repo togther. " + "Can either deploy from a local directory or a Github repo") + +def _reformat_image(source, repo, image): + if source and (image or repo): + image = image.split('/')[-1] # if link is given + image = image.replace(':', '') + return image + +def _get_dockerfile_content_local(source, dockerfile): + lines = [] + if source: + dockerfile_location = f"{source}/{dockerfile}" + try: + with open(dockerfile_location, 'r') as fh: + lines = [line for line in fh] + except: + raise InvalidArgumentValueError("Cannot open specified Dockerfile. Check dockerfile name, path, and permissions.") + return lines + + +def _get_dockerfile_content(repo, branch, token, source, context_path, dockerfile): + if source: + return _get_dockerfile_content_local(source, dockerfile) + elif repo: + return _get_dockerfile_content_from_repo(repo, branch, token, context_path, dockerfile) + return [] + + +def _get_app_env_and_group(cmd, name, resource_group: 'ResourceGroup', env: 'ContainerAppEnvironment'): + if not resource_group.name and not resource_group.exists: + matched_apps = [c for c in list_containerapp(cmd) if c['name'].lower() == name.lower()] + if len(matched_apps) == 1: + if env.name: + logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") + resource_group.name = parse_resource_id(matched_apps[0]["id"])["resource_group"] + env.set_name(matched_apps[0]["properties"]["managedEnvironmentId"]) + elif len(matched_apps) > 1: + raise ValidationError(f"There are multiple containerapps with name {name} on the subscription. " + "Please specify which resource group your Containerapp is in.") + + +def _get_env_and_group_from_log_analytics(cmd, resource_group_name, env:'ContainerAppEnvironment', resource_group:'ResourceGroup', logs_customer_id, location): + # resource_group_name is the value the user passed in (if present) + if not env.name: + if (resource_group_name == resource_group.name and resource_group.exists) or (not resource_group_name): + env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) + if logs_customer_id: + env_list = [e for e in env_list if safe_get(e, "properties", "appLogsConfiguration", "logAnalyticsConfiguration", "customerId") == logs_customer_id] + if location: + env_list = [e for e in env_list if e['location'] == location] + if env_list: + # TODO check how many CA in env + env_details = parse_resource_id(env_list[0]["id"]) + env.set_name(env_details["name"]) + resource_group.name = env_details["resource_group"] + + +def _get_acr_from_image(cmd, app): + if app.image is not None and "azurecr.io" in app.image: + if app.registry_user is None or app.registry_pass is None: + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') + app.registry_server = app.image.split('/')[0] # TODO what if this conflicts with registry_server param? + parsed = urlparse(app.image) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + + +def _get_registry_from_app(app): + containerapp_def = app.get() + if containerapp_def: + if len(safe_get(containerapp_def, "properties", "configuration", "registries")) == 1: + app.registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] + + +def _get_registry_details(cmd, app: 'ContainerApp'): + registry_rg = None + registry_name = None + if app.registry_server: + if "azurecr.io" not in app.registry_server: + raise ValidationError("Cannot supply non-Azure registry when using --source.") + if app.registry_user is None or app.registry_pass is None: + logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') + parsed = urlparse(app.registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + try: + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + else: + registry_rg = app.resource_group.name + user = get_profile_username() + registry_name = "{}acr".format(app.name).replace('-','') + registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "") + app.registry_server = registry_name + ".azurecr.io" + app.should_create_acr = True + app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) + + +# attempt to populate defaults for managed env, RG, ACR, etc +def _set_up_defaults(cmd, name, resource_group_name, logs_customer_id, location, + resource_group: 'ResourceGroup', env:'ContainerAppEnvironment', app:'ContainerApp'): + # If no RG passed in and a singular app exists with the same name, get its env and rg + _get_app_env_and_group(cmd, name, resource_group, env) + + # If no env passed in (and not creating a new RG), then try getting an env by location / log analytics ID + _get_env_and_group_from_log_analytics(cmd, resource_group_name, env, resource_group, logs_customer_id, location) + + # get ACR details from --image, if possible + _get_acr_from_image(cmd, app) + +def _create_github_action(app:'ContainerApp', + env:'ContainerAppEnvironment', + service_principal_client_id, service_principal_client_secret, service_principal_tenant_id, + branch, + token, + repo, + context_path): + + sp = _get_or_create_sp(app.cmd, + app.resource_group.name, + env.resource_group.name, + app.name, + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id) + service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = sp + gh_action = create_or_update_github_action(cmd=app.cmd, + name=app.name, + resource_group_name=app.resource_group.name, + repo_url=repo, + registry_url=app.registry_server, + registry_username=app.registry_user, + registry_password=app.registry_pass, + branch=branch, + token=token, + login_with_github=False, + service_principal_client_id=service_principal_client_id, + service_principal_client_secret=service_principal_client_secret, + service_principal_tenant_id=service_principal_tenant_id, + image=app.image, + context_path=context_path) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 4bd02bccd26..17ba1bb2d29 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -156,6 +156,67 @@ def is_int(s): return False +def await_github_action(cmd, token, repo, branch, name, resource_group_name, timeout=300): + from .custom import show_github_action + from github import Github + from time import sleep + from ._clients import PollingAnimation + from datetime import datetime + + start = datetime.utcnow() + + animation = PollingAnimation() + animation.tick() + g = Github(token) + + github_repo = g.get_repo(repo) + + + workflow = None + while workflow is None: + workflows = github_repo.get_workflows() + animation.flush() + for wf in workflows: + if wf.path.startswith(f".github/workflows/{name}") and "Trigger auto deployment for containerapp" in wf.name: + workflow = wf + break + + gh_action_status = safe_get(show_github_action(cmd, name, resource_group_name), "properties", "operationState") + if gh_action_status == "Failed": + raise CLIInternalError("The Github Action creation failed.") + sleep(1) + animation.tick() + + if (datetime.utcnow() - start).seconds >= timeout: + raise CLIInternalError("Timed out while waiting for the Github action to start.") + + animation.flush() + animation.tick(); animation.flush() + run = workflow.get_runs()[0] + logger.warning(f"Github action run: https://github.com/{repo}/actions/runs/{run.id}") + logger.warning("Waiting for deployment to complete...") + run_id = run.id + status = run.status + while status == "queued" or status == "in_progress": + sleep(3) + animation.tick() + status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] + animation.flush() + if (datetime.utcnow() - start).seconds >= timeout: + raise CLIInternalError("Timed out while waiting for the Github action to start.") + + if status != "completed": + raise ValidationError(f"Github action deployment ended with status: {status}") + + +def repo_url_to_name(repo_url): + repo = None + repo = repo_url.split('/') + if len(repo) >= 2: + repo = '/'.join(repo[-2:]) + return repo + + def _get_location_from_resource_group(cli_ctx, resource_group_name): client = cf_resource_groups(cli_ctx) group = client.get(resource_group_name) diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index f8a1b54cb52..ff659d7735e 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -53,7 +53,6 @@ def load_command_table(self, _): g.custom_command('exec', 'containerapp_ssh', validator=validate_ssh) g.custom_command('up', 'containerapp_up', supports_no_wait=True, exception_handler=ex_handler_factory()) g.custom_command('browse', 'open_containerapp_in_browser') - g.custom_command('github up', 'github_up') with self.command_group('containerapp replica', is_preview=True) as g: g.custom_show_command('show', 'get_replica') # TODO implement the table transformer diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 6558c6171d4..c8272bb884f 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -16,12 +16,9 @@ ResourceNotFoundError, CLIError, CLIInternalError, - InvalidArgumentValueError, - MutuallyExclusiveArgumentError) + InvalidArgumentValueError) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.core.util import open_page_in_browser, get_file_json -from azure.cli.command_modules.appservice._create_util import check_resource_group_exists -from azure.cli.command_modules.appservice._constants import GENERATE_RANDOM_APP_NAMES +from azure.cli.core.util import open_page_in_browser from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id @@ -59,7 +56,7 @@ _update_revision_env_secretrefs, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, - get_container_app_if_exists, _get_name) + _get_name, await_github_action, repo_url_to_name) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING, remove_token) @@ -1068,66 +1065,6 @@ def _validate_github(repo, branch, token): raise CLIInternalError(error_msg) from e -def _await_github_action(cmd, token, repo, branch, name, resource_group_name, timeout=300): - from github import Github - from time import sleep - from ._clients import PollingAnimation - from datetime import datetime - - start = datetime.utcnow() - - animation = PollingAnimation() - animation.tick() - g = Github(token) - - github_repo = g.get_repo(repo) - - - workflow = None - while workflow is None: - workflows = github_repo.get_workflows() - animation.flush() - for wf in workflows: - if wf.path.startswith(f".github/workflows/{name}") and "Trigger auto deployment for containerapp" in wf.name: - workflow = wf - break - - gh_action_status = safe_get(show_github_action(cmd, name, resource_group_name), "properties", "operationState") - if gh_action_status == "Failed": - raise CLIInternalError("The Github Action creation failed.") - sleep(1) - animation.tick() - - if (datetime.utcnow() - start).seconds >= timeout: - raise CLIInternalError("Timed out while waiting for the Github action to start.") - - animation.flush() - animation.tick(); animation.flush() - run = workflow.get_runs()[0] - logger.warning(f"Github action run: https://github.com/{repo}/actions/runs/{run.id}") - logger.warning("Waiting for deployment to complete...") - run_id = run.id - status = run.status - while status == "queued" or status == "in_progress": - sleep(3) - animation.tick() - status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] - animation.flush() - if (datetime.utcnow() - start).seconds >= timeout: - raise CLIInternalError("Timed out while waiting for the Github action to start.") - - if status != "completed": - raise ValidationError(f"Github action deployment ended with status: {status}") - - -def _repo_url_to_name(repo_url): - repo = None - repo = repo_url.split('/') - if len(repo) >= 2: - repo = '/'.join(repo[-2:]) - return repo - - def create_or_update_github_action(cmd, name, resource_group_name, @@ -1152,7 +1089,7 @@ def create_or_update_github_action(cmd, elif token and login_with_github: logger.warning("Both token and --login-with-github flag are provided. Will use provided token") - repo = _repo_url_to_name(repo_url) + repo = repo_url_to_name(repo_url) repo_url = f"https://github.com/{repo}" # allow specifying repo as / without the full github url _validate_github(repo, branch, token) @@ -1216,7 +1153,7 @@ def create_or_update_github_action(cmd, logger.warning("Creating Github action...") r = GitHubActionClient.create_or_update(cmd=cmd, resource_group_name=resource_group_name, name=name, github_action_envelope=source_control_info, headers=headers, no_wait=no_wait) if not no_wait: - _await_github_action(cmd, token, repo, branch, name, resource_group_name) + await_github_action(cmd, token, repo, branch, name, resource_group_name) return r except Exception as e: handle_raw_exception(e) @@ -2049,341 +1986,6 @@ def open_containerapp_in_browser(cmd, name, resource_group_name): open_page_in_browser(url) -# up utils -- TODO move to their own file - -def _create_service_principal(cmd, resource_group_name, env_resource_group_name): - logger.warning("No valid service principal provided. Creating a new service principal...") - scopes = [f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}"] - if env_resource_group_name is not None and env_resource_group_name != resource_group_name: - scopes.append(f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{env_resource_group_name}") - sp = create_service_principal_for_rbac(cmd, scopes=scopes, role="contributor") - - logger.info(f"Created service principal: {sp['displayName']}") - - return sp["appId"], sp["password"], sp["tenant"] - - -def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, - service_principal_client_secret, service_principal_tenant_id): - try: - GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - return service_principal_client_id, service_principal_client_secret, service_principal_tenant_id - except: - service_principal = None - - # TODO if possible, search for SPs with the right credentials - # I haven't found a way to get SP creds + secrets yet from the API - - if not service_principal: - return _create_service_principal(cmd, resource_group_name, env_resource_group_name) - # return client_id, secret, tenant_id - - -def _get_dockerfile_content_from_repo(repo_url, branch, token, context_path, dockerfile): - from github import Github - g = Github(token) - repo = _repo_url_to_name(repo_url) - r = g.get_repo(repo) - files = r.get_contents(context_path, ref=branch) - for f in files: - if f.path == dockerfile or f.path.endswith(f"/{dockerfile}"): - resp = requests.get(f.download_url) - if resp.ok and resp.content: - return resp.content.decode("utf-8").split("\n") - - -def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: 'list[str]'): - if not target_port and not ingress and dockerfile_content is not None: - for line in dockerfile_content: - if line: - line = line.upper().strip().replace("/TCP", "").replace("/UDP", "").replace("\n","") - if line and line[0] != "#": - if "EXPOSE" in line: - parts = line.split(" ") - for i, p in enumerate(parts[:-1]): - if "EXPOSE" in p and is_int(parts[i+1]): - target_port = parts[i+1] - ingress = "external" - logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) - ingress = "external" if target_port and not ingress else ingress - return ingress, target_port - - -def _validate_up_args(source, image, repo): - if not source and not image and not repo: - raise RequiredArgumentMissingError("You must specify either --source, --repo, or --image") - if source and repo: - raise MutuallyExclusiveArgumentError("Cannot use --source and --repo togther. " - "Can either deploy from a local directory or a Github repo") - -def _reformat_image(source, repo, image): - if source and (image or repo): - image = image.split('/')[-1] # if link is given - image = image.replace(':', '') - return image - -def _get_dockerfile_content_local(source, dockerfile): - lines = [] - if source: - dockerfile_location = f"{source}/{dockerfile}" - try: - with open(dockerfile_location, 'r') as fh: - lines = [line for line in fh] - except: - raise InvalidArgumentValueError("Cannot open specified Dockerfile. Check dockerfile name, path, and permissions.") - return lines - - -def _get_dockerfile_content(repo, branch, token, source, dockerfile): - if source: - return _get_dockerfile_content_local(source, dockerfile) - return _get_dockerfile_content_from_repo(repo, branch, token) - - -def _get_app_env_and_group(cmd, name, resource_group: 'ResourceGroup', env: 'ContainerAppEnvironment'): - if not resource_group.name and not resource_group.exists: - matched_apps = [c for c in list_containerapp(cmd) if c['name'].lower() == name.lower()] - if len(matched_apps) == 1: - if env.name: - logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") - resource_group.name = matched_apps[0]["resourceGroup"] - env.name = matched_apps[0]["properties"]["managedEnvironmentId"] - elif len(matched_apps) > 1: - raise ValidationError(f"There are multiple containerapps with name {name} on the subscription. " - "Please specify which resource group your Containerapp is in.") - - -def _get_env_and_group_from_log_analytics(cmd, resource_group_name, env:'ContainerAppEnvironment', resource_group:'ResourceGroup', logs_customer_id, location): - # resource_group_name is the value the user passed in (if present) - if not env.name: - if (resource_group_name == resource_group.name and resource_group.exists) or (not resource_group_name): - env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) - if logs_customer_id: - env_list = [e for e in env_list if safe_get(e, "properties", "appLogsConfiguration", "logAnalyticsConfiguration", "customerId") == logs_customer_id] - if location: - env_list = [e for e in env_list if e['location'] == location] - if env_list: - # TODO check how many CA in env - env_details = parse_resource_id(env_list[0]["id"]) - env.name = env_details["name"] - resource_group.name = env_details["resource_group"] - - -def _get_acr_from_image(cmd, app): - if app.image is not None and "azurecr.io" in app.image: - if app.registry_user is None or app.registry_pass is None: - logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - app.registry_server = app.image.split('/')[0] # TODO what if this conflicts with registry_server param? - parsed = urlparse(app.image) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: - app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) - app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) - except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex - - -def _get_registry_from_app(app): - containerapp_def = app.get() - if containerapp_def: - if len(safe_get(containerapp_def, "properties", "configuration", "registries")) == 1: - app.registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] - - -def _get_registry_details(cmd, app: 'ContainerApp'): - registry_rg = None - registry_name = None - if app.registry_server: - if "azurecr.io" not in app.registry_server: - raise ValidationError("Cannot supply non-Azure registry when using --source.") - if app.registry_user is None or app.registry_pass is None: - logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - parsed = urlparse(app.registry_server) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: - app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) - except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex - else: - registry_rg = app.resource_group.name - user = get_profile_username() - registry_name = "{}acr".format(app.name).replace('-','') - registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "") - app.registry_server = registry_name + ".azurecr.io" - app.should_create_acr = True - app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) - - -class ResourceGroup: - def __init__(self, cmd, name: str, location: str, exists: bool): - self.cmd = cmd - self.name = name - self.location = _get_default_containerapps_location(cmd, location) - self.exists = exists - - self.check_exists() - - def create(self, cmd): - if not self.name: - self.name = get_randomized_name(get_profile_username()) - g = create_resource_group(cmd, self.name, self.location) - self.exists = True - return g - - def _get(self): - return get_resource_group(self.cmd, self.name) - - def get(self): - r = None - try: - r = self._get(self.cmd) - except: - pass - return r - - def check_exists(self) -> bool: - self.exists = check_resource_group_exists(self.cmd, self.name) - return self.exists - - -class Resource: - def __init__(self, cmd, name: str, resource_group: 'ResourceGroup', exists: bool=None): - self.cmd = cmd - self.name = name - self.resource_group = resource_group - self.exists = exists - - self.check_exists(cmd) - - - def create(self, *args, **kwargs): - raise NotImplementedError() - - def _get(self): - raise NotImplementedError() - - def get(self): - r = None - try: - r = self._get(self.cmd) - except: - pass - return r - - def check_exists(self): - self.exists = self.get() is None - return self.exists - - -class ContainerAppEnvironment(Resource): - def __init__(self, - cmd, - name: str, - resource_group: 'ResourceGroup', - exists: bool=None, - location=None, - logs_key=None, - logs_customer_id=None): - - super().__init__(cmd, name, resource_group, exists) - if is_valid_resource_id(name): - self.name = parse_resource_id(name)["name"] - rg = parse_resource_id(name)["resource_group"] - if resource_group.name != rg: - self.resource_group = ResourceGroup(cmd, rg, location) - self.location=_get_default_containerapps_location(cmd, location) - self.logs_key=logs_key - self.logs_customer_id=logs_customer_id - - def _get(self, cmd): - return ManagedEnvironmentClient.show(cmd, self.resource_group.name, self.name) - - def create(self, cmd, app_name): - if self.name is None: - self.name = "{}-env".format(app_name).replace("_", "-") - env = create_managed_environment(cmd, - self.name, - location=self.location, - resource_group_name=self.resource_group.name, - logs_key=self.logs_key, - logs_customer_id=self.logs_customer_id, disable_warnings=True) - self.exists = True - return env - - -class AzureContainerRegistry(Resource): - def __init__(self, - name: str, - resource_group: 'ResourceGroup'): - - self.name = name - self.resource_group = resource_group - - -class ContainerApp(Resource): - def __init__(self, - cmd, - name: str, - resource_group: 'ResourceGroup', - exists: bool=None, - image=None, - env: 'ContainerAppEnvironment'=None, - target_port=None, - registry_server=None, - registry_user=None, - registry_pass=None, - env_vars=None, - ingress=None): - - super().__init__(cmd, name, resource_group, exists) - self.image=image - self.env=env - self.target_port=target_port - self.registry_server=registry_server - self.registry_user=registry_user - self.registry_pass = registry_pass - self.env_vars=env_vars - self.ingress=ingress - - self.should_create_acr = False - self.acr: 'AzureContainerRegistry' = None - - def _get(self, cmd): - return ContainerAppClient.show(cmd, self.resource_group, self.name) - - def create(self, cmd): - return create_containerapp(cmd=cmd, - name=self.name, - resource_group_name=self.resource_group.name, - image=self.image, - managed_env=self.env.name, - target_port=self.target_port, - registry_server=self.registry_server, - registry_pass=self.registry_pass, - registry_user=self.registry_user, - env_vars=self.env_vars, - ingress=self.ingress, - disable_warnings=True) - def create_acr(self): - registry_rg = self.resource_group.name - url = self.registry_server - registry_name = url[:url.rindex(".azurecr.io")] - registry_def = create_new_acr(self.cmd, registry_name, registry_rg, self.location) - self.registry_server = registry_def.login_server - - def run_acr_build(self, dockerfile): - image_name = self.image if self.image is not None else self.name - from datetime import datetime - now = datetime.now() - # Add version tag for acr image - image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) - - self.registry_rg - - self.image = self.registry_server + '/' + image_name - queue_acr_build(self.cmd, self.registry_rg, self.registry_name, image_name, self.source, dockerfile, quiet=True) - - def containerapp_up(cmd, name, resource_group_name=None, @@ -2400,82 +2002,51 @@ def containerapp_up(cmd, logs_customer_id=None, logs_key=None, repo=None, + token=None, branch=None, browse=False, context_path=None, service_principal_client_id=None, service_principal_client_secret=None, service_principal_tenant_id=None): + from ._up_utils import (_validate_up_args, _reformat_image, _get_dockerfile_content, _get_ingress_and_target_port, + ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app, + _get_registry_details, _create_github_action, _set_up_defaults) + dockerfile="Dockerfile", # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) _validate_up_args(source, image, repo) image = _reformat_image(source, repo, image) - token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"]) + token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) - dockerfile_content = _get_dockerfile_content(repo, branch, token, source, dockerfile) + dockerfile_content = _get_dockerfile_content(repo, branch, token, source, context_path, dockerfile) ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) - resource_group = ResourceGroup(name=resource_group_name, location=location) + resource_group = ResourceGroup(cmd, name=resource_group_name, location=location) env = ContainerAppEnvironment(cmd, managed_env, resource_group, location=location, logs_key=logs_key, logs_customer_id=logs_customer_id) app = ContainerApp(cmd, name, resource_group, None, image, env, target_port, registry_server, registry_user, registry_pass, env_vars, ingress) - # If no RG passed in and a singular app exists with the same name, get its env and rg - _get_app_env_and_group(cmd, name, resource_group, env) - - # If no env passed in (and not creating a new RG), then try getting an env by location / log analytics ID - _get_env_and_group_from_log_analytics(cmd, resource_group_name, env, resource_group, logs_customer_id, location) - - # get ACR details from --image, if possible - _get_acr_from_image(cmd, app) + _set_up_defaults(cmd, name, resource_group_name, logs_customer_id, location, resource_group, env, app) if source: registry_server = _get_registry_from_app(app) # if the app exists, get the registry _get_registry_details(cmd, app) # fetch ACR creds from arguments registry arguments - if not resource_group.check_exists(): - logger.warning(f"Creating resource group {resource_group.name}") - resource_group.create() - if not env.check_exists(): - logger.warning(f"Creating Containerapp environment {env.name} in resource group {env.resource_group.name}") - env.create(name) - if app._should_create_acr(): - logger.warning(f"Creating Azure Container Registry {app.acr.name} in resource group {app.acr.resource_group.name}") - app.create_acr() + resource_group.create_if_needed() + env.create_if_needed(name) + app.create_acr_if_needed() if source: app.run_acr_build(dockerfile) - # return image - logger.warning(f"Creating Containerapp {app.name} in resource group {app.resource_group.name}") app.create() if repo: - sp = _get_or_create_sp(cmd, - app.resource_group.name, - env.resource_group.name, - name, - service_principal_client_id, - service_principal_client_secret, - service_principal_tenant_id) - service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = sp - gh_action = create_or_update_github_action(cmd=cmd, - name=name, - resource_group_name=resource_group_name, - repo_url=repo, - registry_url=registry_server, - registry_username=registry_user, - registry_password=registry_pass, - branch=branch, - token=token, - login_with_github=False, - service_principal_client_id=service_principal_client_id, - service_principal_client_secret=service_principal_client_secret, - service_principal_tenant_id=service_principal_tenant_id, - image=image, - context_path=context_path) + _create_github_action(app, env, service_principal_client_id, service_principal_client_secret, + service_principal_tenant_id, branch, token, repo, context_path) if browse: - open_containerapp_in_browser(cmd, name, resource_group) + open_containerapp_in_browser(cmd, app.name, app.resource_group.name) # TODO output From 4a16e17f46abfa06a863323e379991c3ba67022f Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 00:35:27 -0700 Subject: [PATCH 145/158] fix linter issues, fix lingering exec/tail improvements --- src/containerapp/azext_containerapp/_help.py | 38 +++++++++++++++---- .../azext_containerapp/_params.py | 31 +++++++-------- .../azext_containerapp/_ssh_utils.py | 38 ++----------------- .../azext_containerapp/commands.py | 4 +- src/containerapp/azext_containerapp/custom.py | 9 ++--- 5 files changed, 53 insertions(+), 67 deletions(-) diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 09bca883ff6..d3b0bef5cb6 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -101,24 +101,48 @@ az containerapp exec -n MyContainerapp -g MyResourceGroup --command bash """ -helps['containerapp log'] = """ +helps['containerapp browse'] = """ + type: command + short-summary: Open a containerapp in the browser, if possible + examples: + - name: open a containerapp in the browser + text: | + az containerapp browse -n MyContainerapp -g MyResourceGroup +""" + +helps['containerapp up'] = """ + type: command + short-summary: Create or update a container app as well as any associated resources (ACR, resource group, container apps environment, Github Actions, etc.) + examples: + - name: Create a container app from a Github repo (setting up github actions) + text: | + az containerapp up -n MyContainerapp --repo https://github.com/myAccount/myRepo + - name: Create a container app from content in a local directory + text: | + az containerapp up -n MyContainerapp --source . + - name: Create a container app from a container in a registry + text: | + az containerapp up -n MyContainerapp --image myregistry.azurecr.io/myImage:myTag +""" + +helps['containerapp logs'] = """ type: group short-summary: Show container app logs """ -helps['containerapp log tail'] = """ +helps['containerapp logs show'] = """ type: command short-summary: Show past logs and/or print logs in real time (with the --follow parameter). Note that the logs are only taken from one revision, replica (pod), and container. examples: - - name: Fetch the past 10 lines of logs from an app and return + - name: Fetch the past 20 lines of logs from an app and return text: | - az containerapp log tail -n MyContainerapp -g MyResourceGroup - - name: Fetch 20 lines of past logs logs from an app and print logs as they come in + az containerapp logs show -n MyContainerapp -g MyResourceGroup + - name: Fetch 30 lines of past logs logs from an app and print logs as they come in text: | - az containerapp log tail -n MyContainerapp -g MyResourceGroup --follow + az containerapp logs show -n MyContainerapp -g MyResourceGroup --follow --tail 30 - name: Fetch logs for a particular revision, replica, and container text: | - az containerapp log tail -n MyContainerapp -g MyResourceGroup --replica MyReplica --revision MyRevision --container MyContainer + az containerapp logs show -n MyContainerapp -g MyResourceGroup --replica MyReplica --revision MyRevision --container MyContainer """ # Replica Commands diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index b05878ae226..2755a7bf18c 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -31,19 +31,6 @@ def load_arguments(self, _): 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') - with self.argument_context('containerapp github up') as c: - c.argument('repo', help='The GitHub repository to which the workflow file will be added. In the format: https://github.com// or /') - 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.') - 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('context_path', help='Path in the repo from which to run the docker build. Defaults to "./"') - 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.') - c.argument('image', type=str, options_list=['--image', '-i'], help="Container image name that the Github Action should use. Defaults to the Container App name.") - with self.argument_context('containerapp exec') as c: c.argument('container', help="The name of the container to ssh into") c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") @@ -52,9 +39,9 @@ def load_arguments(self, _): c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) - with self.argument_context('containerapp log tail') as c: + with self.argument_context('containerapp logs show') as c: c.argument('follow', help="Print logs in real time if present.", arg_type=get_three_state_flag()) - c.argument('tail', help="The number of past logs to print (0-300)", type=int, default=10) + c.argument('tail', help="The number of past logs to print (0-300)", type=int, default=20) c.argument('container', help="The name of the container") c.argument('output_format', options_list=["--format"], help="Log output format", arg_type=get_enum_type(["json", "text"]), default="json") c.argument('replica', help="The name of the replica (pod). List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") @@ -191,6 +178,7 @@ def load_arguments(self, _): with self.argument_context('containerapp revision copy') as c: c.argument('from_revision', type=str, help='Revision to copy from. Default: latest revision.') + c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") with self.argument_context('containerapp ingress') as c: c.argument('allow_insecure', help='Allow insecure connections for ingress traffic.') @@ -240,11 +228,18 @@ def load_arguments(self, _): c.argument('dryrun', help="Show summary of the operation instead of executing it.") c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") - - with self.argument_context('containerapp up', arg_group='Source') as c: - c.argument('dockerfile', help="Name of the dockerfile.") + c.argument('browse', help='Open the app in a web browser after creation and deployment, if possible.') with self.argument_context('containerapp up', arg_group='Log Analytics (Environment)') as c: c.argument('logs_customer_id', type=str, options_list=['--logs-workspace-id'], help='Name or resource ID of the Log Analytics workspace to send diagnostics logs to. You can use \"az monitor log-analytics workspace create\" to create one. Extra billing may apply.') c.argument('logs_key', type=str, options_list=['--logs-workspace-key'], help='Log Analytics workspace key to configure your Log Analytics workspace. You can use \"az monitor log-analytics workspace get-shared-keys\" to retrieve the key.') c.ignore('no_wait') + + with self.argument_context('containerapp up', arg_group='Github Repo') as c: + c.argument('repo', help='Create an app via Github Actions. In the format: https://github.com// or /') + 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. If missing (and using --repo), a browser page will be opened to authenticate with Github.') + c.argument('branch', options_list=['--branch', '-b'], help='The branch of the GitHub repo. Defaults to "main"') + c.argument('context_path', help='Path in the repo from which to run the docker build. Defaults to "./". Dockerfile is assumed to be named "Dockerfile" and in this directory.') + c.argument('service_principal_client_id', help='The service principal client ID. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-client-id", "--sp-cid"]) + c.argument('service_principal_client_secret', help='The service principal client secret. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-client-secret", "--sp-sec"]) + c.argument('service_principal_tenant_id', help='The service principal tenant ID. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-tenant-id", "--sp-tid"]) diff --git a/src/containerapp/azext_containerapp/_ssh_utils.py b/src/containerapp/azext_containerapp/_ssh_utils.py index cb76294c1ac..a5a77a601c0 100644 --- a/src/containerapp/azext_containerapp/_ssh_utils.py +++ b/src/containerapp/azext_containerapp/_ssh_utils.py @@ -58,17 +58,8 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, self._url = self._get_url(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, replica=replica, container=container, startup_command=startup_command) self._socket = websocket.WebSocket(enable_multithread=True) - logger.warning("Attempting to connect to %s", remove_token(self._url)) - - # TODO may be worth including some retry policy here - try: - self._socket.connect(self._url, header=[f"Authorization: Bearer {self._token}"]) - except WebSocketBadStatusException: - logger.info("Caught WebSocketBadStatusException") - url = self._get_url_no_command(cmd=cmd, resource_group_name=resource_group_name, name=name, revision=revision, - replica=replica, container=container, startup_command=startup_command) - logger.warning("Attempting to connect to %s", remove_token(url)) - self._socket.connect(url, header=[f"Authorization: Bearer {self._token}"]) + logger.warning("Attempting to connect to %s", self._url) + self._socket.connect(self._url, header=[f"Authorization: Bearer {self._token}"]) self.is_connected = True self._windows_conout_mode = None @@ -77,30 +68,15 @@ def __init__(self, cmd, resource_group_name, name, revision, replica, container, self._windows_conout_mode = _get_conout_mode() self._windows_conin_mode = _get_conin_mode() - # TODO remove once encorporated into _get_url - def _get_url_no_command(self, cmd, resource_group_name, name, revision, replica, container, startup_command): - sub = get_subscription_id(cmd.cli_ctx) - base_url = self._logstream_endpoint - proxy_api_url = base_url[:base_url.index("/subscriptions/")].replace("https://", "") - token = self._token - encoded_cmd = urllib.parse.quote_plus(startup_command) - - return (f"wss://{proxy_api_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" - f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec" - f"?token={token}&command={encoded_cmd}") - def _get_url(self, cmd, resource_group_name, name, revision, replica, container, startup_command): sub = get_subscription_id(cmd.cli_ctx) base_url = self._logstream_endpoint proxy_api_url = base_url[:base_url.index("/subscriptions/")].replace("https://", "") - token = self._token encoded_cmd = urllib.parse.quote_plus(startup_command) - # TODO remove token from URL once token is read from header - # TODO remove startup command from path return (f"wss://{proxy_api_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" - f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec/{startup_command}" - f"?token={token}&command={encoded_cmd}") + f"/revisions/{revision}/replicas/{replica}/containers/{container}/exec" + f"?command={encoded_cmd}") def disconnect(self): logger.warning("Disconnecting...") @@ -117,12 +93,6 @@ def recv(self, *args, **kwargs): return self._socket.recv(*args, **kwargs) -def remove_token(url): - if "?token=" in url: - return url[:url.index("?token=")] - return url - - def _decode_and_output_to_terminal(connection: WebSocketConnection, response, encodings): for i, encoding in enumerate(encodings): try: diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index ff659d7735e..6cbd42cd37f 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -58,8 +58,8 @@ def load_command_table(self, _): g.custom_show_command('show', 'get_replica') # TODO implement the table transformer g.custom_command('list', 'list_replicas') - with self.command_group('containerapp log', is_preview=True) as g: - g.custom_command('tail', 'stream_containerapp_logs', validator=validate_ssh) + with self.command_group('containerapp logs', is_preview=True) as g: + g.custom_command('show', 'stream_containerapp_logs', validator=validate_ssh) 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 c8272bb884f..25c7b2c8622 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -59,7 +59,7 @@ _get_name, await_github_action, repo_url_to_name) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, - SSH_BACKUP_ENCODING, remove_token) + SSH_BACKUP_ENCODING) logger = get_logger(__name__) @@ -1916,7 +1916,6 @@ def get_replica(cmd, resource_group_name, name, replica, revision=None): replica_name=replica) -# TODO token will be read from header at some point -- a PR has apparently been opened for this def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=None, replica=None, startup_command="sh"): if isinstance(startup_command, list): startup_command = startup_command[0] # CLI seems a little buggy when calling a param "--command" @@ -1943,7 +1942,6 @@ def containerapp_ssh(cmd, resource_group_name, name, container=None, revision=No conn.send(SSH_CTRL_C_MSG) -# TODO token will be in header soon (same as SSH) def stream_containerapp_logs(cmd, resource_group_name, name, container=None, revision=None, replica=None, follow=False, tail=None, output_format=None): if tail: @@ -1956,11 +1954,10 @@ def stream_containerapp_logs(cmd, resource_group_name, name, container=None, rev logstream_endpoint = token_response["properties"]["logStreamEndpoint"] base_url = logstream_endpoint[:logstream_endpoint.index("/subscriptions/")] - # TODO remove token from URL once token is read from header url = (f"{base_url}/subscriptions/{sub}/resourceGroups/{resource_group_name}/containerApps/{name}" - f"/revisions/{revision}/replicas/{replica}/containers/{container}/logstream?token={token}") + f"/revisions/{revision}/replicas/{replica}/containers/{container}/logstream") - logger.warning("connecting to : %s", remove_token(url)) + logger.warning("connecting to : %s", url) request_params = {"follow": str(follow).lower(), "output": output_format, "tailLines": tail} headers = {"Authorization": f"Bearer {token}"} resp = requests.get(url, timeout=None, stream=True, params=request_params, headers=headers) From 1e80127594035f269620f68d9a330ec1795460fd Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 00:40:43 -0700 Subject: [PATCH 146/158] update history --- src/containerapp/HISTORY.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 986b360129c..3a2c9605bd1 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -5,8 +5,9 @@ Release History 0.4.0 ++++++ +* Create or update a container app and all associated resources (container app environment, ACR, Github Actions, resource group, etc.) with 'az containerapp up' * Open an ssh-like shell in a Container App with 'az containerapp exec' -* Support for log streaming with 'az containerapp log tail' +* Support for log streaming with 'az containerapp logs show' * Replica show and list commands 0.3.1 From dd8473523d86a70363b0d72700f0d98b5662f091 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 01:02:37 -0700 Subject: [PATCH 147/158] update output --- .../azext_containerapp/_up_utils.py | 17 +++++++++++++++++ src/containerapp/azext_containerapp/custom.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 842e0fff7fe..f600856c5e3 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -464,3 +464,20 @@ def _create_github_action(app:'ContainerApp', service_principal_tenant_id=service_principal_tenant_id, image=app.image, context_path=context_path) + +def up_output(app): + url = safe_get(ContainerAppClient.show(app.cmd, app.resource_group.name, app.name), "properties", + "configuration", + "ingress", "fqdn") + if url and not url.startswith("http"): + url = f"http://{url}" + if url: + output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" + f"Browse to your container app at: {url} \n\n" + f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" + f"See full output using: az containerapp show n {app.name} -g {app.resource_group.name} \n") + else: + output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" + f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" + f"See full output using: az containerapp show n {app.name} -g {app.resource_group.name} \n") + return output diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 25c7b2c8622..962fae64dd1 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2008,7 +2008,7 @@ def containerapp_up(cmd, service_principal_tenant_id=None): from ._up_utils import (_validate_up_args, _reformat_image, _get_dockerfile_content, _get_ingress_and_target_port, ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app, - _get_registry_details, _create_github_action, _set_up_defaults) + _get_registry_details, _create_github_action, _set_up_defaults, up_output) dockerfile="Dockerfile", # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) @@ -2046,4 +2046,4 @@ def containerapp_up(cmd, if browse: open_containerapp_in_browser(cmd, app.name, app.resource_group.name) - # TODO output + print(up_output(app)) From e8c3c340bdb30a5aa5d0d137bcd8b63c29df7326 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 09:40:37 -0700 Subject: [PATCH 148/158] bug fixes for --repo --- src/containerapp/azext_containerapp/_up_utils.py | 6 ++++-- src/containerapp/azext_containerapp/_utils.py | 4 ++-- src/containerapp/azext_containerapp/commands.py | 2 +- src/containerapp/azext_containerapp/custom.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index f600856c5e3..f2285e40f01 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -238,9 +238,11 @@ def create_acr(self): registry_rg = self.resource_group.name url = self.registry_server registry_name = url[:url.rindex(".azurecr.io")] - registry_def = create_new_acr(self.cmd, registry_name, registry_rg, self.location) + registry_def = create_new_acr(self.cmd, registry_name, registry_rg, self.env.location) self.registry_server = registry_def.login_server + self.registry_user, self.registry_pass, _ = _get_acr_cred(self.cmd.cli_ctx, registry_name) + def run_acr_build(self, dockerfile): image_name = self.image if self.image is not None else self.name from datetime import datetime @@ -393,7 +395,7 @@ def _get_acr_from_image(cmd, app): def _get_registry_from_app(app): containerapp_def = app.get() if containerapp_def: - if len(safe_get(containerapp_def, "properties", "configuration", "registries")) == 1: + if len(safe_get(containerapp_def, "properties", "configuration", "registries", default=[])) == 1: app.registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index 17ba1bb2d29..e72b829acde 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -837,10 +837,10 @@ def _registry_exists(containerapp_def, registry_server): # get a value from nested dict without getting IndexError (returns None instead) # for example, model["key1"]["key2"]["key3"] would become safe_get(model, "key1", "key2", "key3") -def safe_get(model, *keys): +def safe_get(model, *keys, default=None): for k in keys[:-1]: model = model.get(k, {}) - return model.get(keys[-1]) + return model.get(keys[-1], default) def is_platform_windows(): diff --git a/src/containerapp/azext_containerapp/commands.py b/src/containerapp/azext_containerapp/commands.py index 6cbd42cd37f..57fd157ae7f 100644 --- a/src/containerapp/azext_containerapp/commands.py +++ b/src/containerapp/azext_containerapp/commands.py @@ -59,7 +59,7 @@ def load_command_table(self, _): g.custom_command('list', 'list_replicas') with self.command_group('containerapp logs', is_preview=True) as g: - g.custom_command('show', 'stream_containerapp_logs', validator=validate_ssh) + g.custom_show_command('show', 'stream_containerapp_logs', validator=validate_ssh) 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 962fae64dd1..76d804ff192 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2026,7 +2026,7 @@ def containerapp_up(cmd, _set_up_defaults(cmd, name, resource_group_name, logs_customer_id, location, resource_group, env, app) - if source: + if source or repo: registry_server = _get_registry_from_app(app) # if the app exists, get the registry _get_registry_details(cmd, app) # fetch ACR creds from arguments registry arguments From 16bf2b8b5e36c4906d853b3139c1d81f4b067f2e Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 10:07:26 -0700 Subject: [PATCH 149/158] fix --source bug --- 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 76d804ff192..68a640fb44e 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2010,7 +2010,7 @@ def containerapp_up(cmd, ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app, _get_registry_details, _create_github_action, _set_up_defaults, up_output) - dockerfile="Dockerfile", # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) + dockerfile="Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) _validate_up_args(source, image, repo) From a877444e8b2407b47da6b70137c5e4e8a4b5f1a6 Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 10:56:57 -0700 Subject: [PATCH 150/158] fix --source --- .../azext_containerapp/_up_utils.py | 26 ++++++++++--------- src/containerapp/azext_containerapp/custom.py | 5 ++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index f2285e40f01..1a4e521682e 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -209,7 +209,8 @@ def __init__(self, def _get(self): return ContainerAppClient.show(self.cmd, self.resource_group.name, self.name) - def create(self): + def create(self, no_registry=False): + # no_registry: don't pass in a registry during create even if the app has one (used for GH actions) if get_container_app_if_exists(self.cmd, self.resource_group.name, self.name): logger.warning(f"Updating Containerapp {self.name} in resource group {self.resource_group.name}") else: @@ -221,9 +222,9 @@ def create(self): image=self.image, managed_env=self.env.get_rid(), target_port=self.target_port, - registry_server=self.registry_server, - registry_pass=self.registry_pass, - registry_user=self.registry_user, + registry_server=None if no_registry else self.registry_server, + registry_pass=None if no_registry else self.registry_server, + registry_user=None if no_registry else self.registry_server, env_vars=self.env_vars, ingress=self.ingress, disable_warnings=True) @@ -235,27 +236,28 @@ def create_acr_if_needed(self): self.create_acr() def create_acr(self): - registry_rg = self.resource_group.name + registry_rg = self.resource_group url = self.registry_server registry_name = url[:url.rindex(".azurecr.io")] - registry_def = create_new_acr(self.cmd, registry_name, registry_rg, self.env.location) + registry_def = create_new_acr(self.cmd, registry_name, registry_rg.name, self.env.location) self.registry_server = registry_def.login_server + if not self.acr: + self.acr = AzureContainerRegistry() + self.acr.name = registry_name + self.acr.resource_group = registry_rg + self.registry_user, self.registry_pass, _ = _get_acr_cred(self.cmd.cli_ctx, registry_name) - def run_acr_build(self, dockerfile): + def run_acr_build(self, dockerfile, source): image_name = self.image if self.image is not None else self.name from datetime import datetime now = datetime.now() # Add version tag for acr image image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) - self.registry_rg - self.image = self.registry_server + '/' + image_name - queue_acr_build(self.cmd, self.registry_rg, self.registry_name, image_name, self.source, dockerfile, quiet=True) - -# up utils -- TODO move to their own file + queue_acr_build(self.cmd, self.acr.resource_group.name, self.acr.name, image_name, source, dockerfile, quiet=True) def _create_service_principal(cmd, resource_group_name, env_resource_group_name): logger.warning("No valid service principal provided. Creating a new service principal...") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 68a640fb44e..96ebf46951d 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2035,10 +2035,9 @@ def containerapp_up(cmd, app.create_acr_if_needed() if source: - app.run_acr_build(dockerfile) - - app.create() + app.run_acr_build(dockerfile, source) + app.create(no_registry=bool(repo)) if repo: _create_github_action(app, env, service_principal_client_id, service_principal_client_secret, service_principal_tenant_id, branch, token, repo, context_path) From 0f49ce0dc9cabf8d2c072efb5a3e23b45f9ebbda Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 13:17:36 -0700 Subject: [PATCH 151/158] minor bug fixes --- src/containerapp/azext_containerapp/_up_utils.py | 10 ++++++---- src/containerapp/azext_containerapp/custom.py | 3 +-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 1a4e521682e..f13ba3414a6 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -223,8 +223,8 @@ def create(self, no_registry=False): managed_env=self.env.get_rid(), target_port=self.target_port, registry_server=None if no_registry else self.registry_server, - registry_pass=None if no_registry else self.registry_server, - registry_user=None if no_registry else self.registry_server, + registry_pass=None if no_registry else self.registry_pass, + registry_user=None if no_registry else self.registry_user, env_vars=self.env_vars, ingress=self.ingress, disable_warnings=True) @@ -418,8 +418,9 @@ def _get_registry_details(cmd, app: 'ContainerApp'): else: registry_rg = app.resource_group.name user = get_profile_username() - registry_name = "{}acr".format(app.name).replace('-','') - registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "") + registry_name = app.name.replace('-','').lower() + registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "").replace(".", "")[:10] # cap at 15 characters total + registry_name = f"ca{registry_name}acr" # ACR names must start + end in a letter app.registry_server = registry_name + ".azurecr.io" app.should_create_acr = True app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) @@ -469,6 +470,7 @@ def _create_github_action(app:'ContainerApp', image=app.image, context_path=context_path) + def up_output(app): url = safe_get(ContainerAppClient.show(app.cmd, app.resource_group.name, app.name), "properties", "configuration", diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 96ebf46951d..082e1b2a0c8 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1130,7 +1130,7 @@ def create_or_update_github_action(cmd, 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) + registry_username, registry_password, _ = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex @@ -1725,7 +1725,6 @@ def set_secrets(cmd, name, resource_group_name, secrets, # yaml=None, no_wait=False): _validate_subscription_registered(cmd, "Microsoft.App") - # if not yaml and not secrets: # raise RequiredArgumentMissingError('Usage error: --secrets is required if not using --yaml') From e886ea4780d6a3a0a9d568b8f9c87e9c85c23eba Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 21 Apr 2022 16:24:34 -0400 Subject: [PATCH 152/158] Added API change. --- .../azext_containerapp/_clients.py | 4 +- .../azext_containerapp/_up_utils.py | 15 +-- src/containerapp/azext_containerapp/custom.py | 126 ++++++++++++++++++ 3 files changed, 135 insertions(+), 10 deletions(-) diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 7986ca00df8..40e4b188d2a 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -103,7 +103,7 @@ def create_or_update(cls, cmd, resource_group_name, name, container_app_envelope @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 = PREVIEW_API_VERSION + api_version = STABLE_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( @@ -117,7 +117,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= if no_wait: return r.json() - elif r.status_code == 201: + elif r.status_code == 202: url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.App/containerApps/{}?api-version={}" request_url = url_fmt.format( management_hostname.strip('/'), diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 1a4e521682e..cacfc8f51b7 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -26,7 +26,7 @@ _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, repo_url_to_name, get_container_app_if_exists) -from .custom import (create_managed_environment, create_containerapp, list_containerapp, +from .custom import (create_managed_environment, containerapp_up_logic, list_containerapp, list_managed_environments, create_or_update_github_action) logger = get_logger(__name__) @@ -216,18 +216,17 @@ def create(self, no_registry=False): else: logger.warning(f"Creating Containerapp {self.name} in resource group {self.resource_group.name}") - return create_containerapp(cmd=self.cmd, + return containerapp_up_logic(cmd=self.cmd, name=self.name, resource_group_name=self.resource_group.name, image=self.image, managed_env=self.env.get_rid(), target_port=self.target_port, registry_server=None if no_registry else self.registry_server, - registry_pass=None if no_registry else self.registry_server, - registry_user=None if no_registry else self.registry_server, + registry_pass=None if no_registry else self.registry_pass, + registry_user=None if no_registry else self.registry_user, env_vars=self.env_vars, - ingress=self.ingress, - disable_warnings=True) + ingress=self.ingress) def create_acr_if_needed(self): if self.should_create_acr: @@ -479,9 +478,9 @@ def up_output(app): output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" f"Browse to your container app at: {url} \n\n" f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" - f"See full output using: az containerapp show n {app.name} -g {app.resource_group.name} \n") + f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") else: output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" - f"See full output using: az containerapp show n {app.name} -g {app.resource_group.name} \n") + f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") return output diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 96ebf46951d..4624fc3968c 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2046,3 +2046,129 @@ def containerapp_up(cmd, open_containerapp_in_browser(cmd, app.name, app.resource_group.name) print(up_output(app)) + + +def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, env_vars, ingress, target_port, registry_server, registry_user, registry_pass): + containerapp_def = None + try: + containerapp_def = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) + except: + pass + + try: + location = ManagedEnvironmentClient.show(cmd, resource_group_name, managed_env.split('/')[-1])["location"] + except: + pass + + ca_exists = False + if containerapp_def: + ca_exists = True + + if not ca_exists: + containerapp_def = None + containerapp_def = ContainerAppModel + containerapp_def["location"] = location + containerapp_def["properties"]["managedEnvironmentId"] = managed_env + containerapp_def["properties"]["configuration"] = ConfigurationModel + else: + # check provisioning state here instead of secrets so no error + _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) + + + container = ContainerModel + container["image"] = image + container["name"] = name + + + if env_vars: + container["env"] = parse_env_var_flags(env_vars) + + + external_ingress = None + if ingress is not None: + if ingress.lower() == "internal": + external_ingress = False + elif ingress.lower() == "external": + external_ingress = True + + + ingress_def = None + if target_port is not None and ingress is not None: + ingress_def = IngressModel + ingress_def["external"] = external_ingress + ingress_def["targetPort"] = target_port + containerapp_def["properties"]["configuration"]["ingress"] = ingress_def + + + # handle multi-container case + if ca_exists: + existing_containers = containerapp_def["properties"]["template"]["containers"] + if len(existing_containers) == 0: + # No idea how this would ever happen, failed provisioning maybe? + containerapp_def["properties"]["template"] = TemplateModel + containerapp_def["properties"]["template"]["containers"] = [container] + if len(existing_containers) == 1: + # Assume they want it updated + existing_containers[0] = container + if len(existing_containers) > 1: + # Assume they want to update, if not existing just add it + existing_containers = [x for x in existing_containers if x['name'].lower() == name.lower()] + if len(existing_containers) == 1: + existing_containers[0] = container + else: + existing_containers.append(container) + containerapp_def["properties"]["template"]["containers"] = existing_containers + else: + containerapp_def["properties"]["template"] = TemplateModel + containerapp_def["properties"]["template"]["containers"] = [container] + + + registries_def = None + registry = None + + + if "secrets" not in containerapp_def["properties"]["configuration"] or containerapp_def["properties"]["configuration"]["secrets"] == None: + containerapp_def["properties"]["configuration"]["secrets"] = [] + + + if "registries" not in containerapp_def["properties"]["configuration"] or containerapp_def["properties"]["configuration"]["registries"] == None: + containerapp_def["properties"]["configuration"]["registries"] = [] + + + registries_def = containerapp_def["properties"]["configuration"]["registries"] + + if 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: + 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) + + if ca_exists: + return ContainerAppClient.patch_update(cmd, resource_group_name, name, containerapp_def) + else: + return ContainerAppClient.create_or_update(cmd, resource_group_name, name, containerapp_def) \ No newline at end of file From bfcace82285de2ee38f42dbdc0fc9a360bd4ab71 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Thu, 21 Apr 2022 17:28:14 -0400 Subject: [PATCH 153/158] Finished API change, added helloworld image auto ingress, checked provisioning state beforehand. --- .../azext_containerapp/_up_utils.py | 24 ++++++++----------- src/containerapp/azext_containerapp/custom.py | 14 ++++++++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index cacfc8f51b7..23d778e4e38 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -248,7 +248,7 @@ def create_acr(self): self.registry_user, self.registry_pass, _ = _get_acr_cred(self.cmd.cli_ctx, registry_name) - def run_acr_build(self, dockerfile, source): + def run_acr_build(self, dockerfile, source, quiet=False): image_name = self.image if self.image is not None else self.name from datetime import datetime now = datetime.now() @@ -256,7 +256,7 @@ def run_acr_build(self, dockerfile, source): image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) self.image = self.registry_server + '/' + image_name - queue_acr_build(self.cmd, self.acr.resource_group.name, self.acr.name, image_name, source, dockerfile, quiet=True) + queue_acr_build(self.cmd, self.acr.resource_group.name, self.acr.name, image_name, source, dockerfile, quiet) def _create_service_principal(cmd, resource_group_name, env_resource_group_name): logger.warning("No valid service principal provided. Creating a new service principal...") @@ -417,8 +417,9 @@ def _get_registry_details(cmd, app: 'ContainerApp'): else: registry_rg = app.resource_group.name user = get_profile_username() - registry_name = "{}acr".format(app.name).replace('-','') - registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "") + registry_name = app.name.replace('-','') + registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "")[:10] + registry_name = f"ca{registry_name}acr" app.registry_server = registry_name + ".azurecr.io" app.should_create_acr = True app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) @@ -474,13 +475,8 @@ def up_output(app): "ingress", "fqdn") if url and not url.startswith("http"): url = f"http://{url}" - if url: - output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" - f"Browse to your container app at: {url} \n\n" - f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" - f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") - else: - output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" - f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" - f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") - return output + + logger.warning(f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n") + url and logger.warning(f"Browse to your container app at: {url} \n") + logger.warning(f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n") + logger.warning(f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 4624fc3968c..ce9bc73a807 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2017,6 +2017,10 @@ def containerapp_up(cmd, image = _reformat_image(source, repo, image) token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token) + if image and "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest" in image.lower(): + ingress = "external" if not ingress else ingress + target_port = 80 if not target_port else target_port + dockerfile_content = _get_dockerfile_content(repo, branch, token, source, context_path, dockerfile) ingress, target_port = _get_ingress_and_target_port(ingress, target_port, dockerfile_content) @@ -2026,6 +2030,10 @@ def containerapp_up(cmd, _set_up_defaults(cmd, name, resource_group_name, logs_customer_id, location, resource_group, env, app) + if app.check_exists(): + if app.get()["properties"]["provisioningState"] == "InProgress": + raise ValidationError("Containerapp has an existing provisioning in progress. Please wait until provisioning has completed and rerun the command.") + if source or repo: registry_server = _get_registry_from_app(app) # if the app exists, get the registry _get_registry_details(cmd, app) # fetch ACR creds from arguments registry arguments @@ -2035,7 +2043,7 @@ def containerapp_up(cmd, app.create_acr_if_needed() if source: - app.run_acr_build(dockerfile, source) + app.run_acr_build(dockerfile, source, False) app.create(no_registry=bool(repo)) if repo: @@ -2045,7 +2053,7 @@ def containerapp_up(cmd, if browse: open_containerapp_in_browser(cmd, app.name, app.resource_group.name) - print(up_output(app)) + up_output(app) def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, env_vars, ingress, target_port, registry_server, registry_user, registry_pass): @@ -2169,6 +2177,6 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en registries_def.append(registry) if ca_exists: - return ContainerAppClient.patch_update(cmd, resource_group_name, name, containerapp_def) + return ContainerAppClient.update(cmd, resource_group_name, name, containerapp_def) else: return ContainerAppClient.create_or_update(cmd, resource_group_name, name, containerapp_def) \ No newline at end of file From b3aae19ace335ee9df9ed8e4f4bb8348096b36cd Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 14:32:14 -0700 Subject: [PATCH 154/158] fixes for sisira's comments --- src/containerapp/HISTORY.rst | 2 +- src/containerapp/azext_containerapp/_clients.py | 6 +++--- src/containerapp/azext_containerapp/_help.py | 10 +++++----- src/containerapp/azext_containerapp/_params.py | 8 +++----- src/containerapp/azext_containerapp/_utils.py | 6 +++--- src/containerapp/setup.py | 2 +- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 3a2c9605bd1..0e0e05ee905 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,7 +3,7 @@ Release History =============== -0.4.0 +0.3.2 ++++++ * Create or update a container app and all associated resources (container app environment, ACR, Github Actions, resource group, etc.) with 'az containerapp up' * Open an ssh-like shell in a Container App with 'az containerapp exec' diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 7986ca00df8..0034b61bd85 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -367,7 +367,7 @@ def list_replicas(cls, cmd, resource_group_name, container_app_name, revision_na resource_group_name, container_app_name, revision_name, - PREVIEW_API_VERSION) + STABLE_API_VERSION) r = send_raw_request(cmd.cli_ctx, "GET", request_url) j = r.json() @@ -395,7 +395,7 @@ def get_replica(cls, cmd, resource_group_name, container_app_name, revision_name container_app_name, revision_name, replica_name, - PREVIEW_API_VERSION) + STABLE_API_VERSION) r = send_raw_request(cmd.cli_ctx, "GET", request_url) return r.json() @@ -410,7 +410,7 @@ def get_auth_token(cls, cmd, resource_group_name, name): sub_id, resource_group_name, name, - PREVIEW_API_VERSION) + STABLE_API_VERSION) r = send_raw_request(cmd.cli_ctx, "POST", request_url) return r.json() diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index d3b0bef5cb6..6ee0f6274c8 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -88,7 +88,7 @@ helps['containerapp exec'] = """ type: command - short-summary: Open an SSH-like interactive shell within a container app replica (pod) + short-summary: Open an SSH-like interactive shell within a container app replica examples: - name: exec into a container app text: | @@ -132,7 +132,7 @@ helps['containerapp logs show'] = """ type: command - short-summary: Show past logs and/or print logs in real time (with the --follow parameter). Note that the logs are only taken from one revision, replica (pod), and container. + short-summary: Show past logs and/or print logs in real time (with the --follow parameter). Note that the logs are only taken from one revision, replica, and container. examples: - name: Fetch the past 20 lines of logs from an app and return text: | @@ -148,12 +148,12 @@ # Replica Commands helps['containerapp replica'] = """ type: group - short-summary: Manage container app replicas (pods) + short-summary: Manage container app replicas """ helps['containerapp replica list'] = """ type: command - short-summary: List a container app revision's replicas (pods) + short-summary: List a container app revision's replica examples: - name: List a container app's replicas in the latest revision text: | @@ -165,7 +165,7 @@ helps['containerapp replica show'] = """ type: command - short-summary: Show a container app replica (pod) + short-summary: Show a container app replica examples: - name: Show a replica from the latest revision text: | diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 2755a7bf18c..01c6a4afa05 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -33,7 +33,7 @@ def load_arguments(self, _): with self.argument_context('containerapp exec') as c: c.argument('container', help="The name of the container to ssh into") - c.argument('replica', help="The name of the replica (pod) to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") + c.argument('replica', help="The name of the replica to ssh into. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") c.argument('revision', help="The name of the container app revision to ssh into. Defaults to the latest revision.") c.argument('startup_command', options_list=["--command"], help="The startup command (bash, zsh, sh, etc.).") c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") @@ -44,21 +44,20 @@ def load_arguments(self, _): c.argument('tail', help="The number of past logs to print (0-300)", type=int, default=20) c.argument('container', help="The name of the container") c.argument('output_format', options_list=["--format"], help="Log output format", arg_type=get_enum_type(["json", "text"]), default="json") - c.argument('replica', help="The name of the replica (pod). List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") + c.argument('replica', help="The name of the replica. List replicas with 'az containerapp replica list'. A replica may not exist if there is not traffic to your app.") c.argument('revision', help="The name of the container app revision. Defaults to the latest revision.") c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) # Replica with self.argument_context('containerapp replica') as c: - c.argument('replica', help="The name of the replica (pod). ") + c.argument('replica', help="The name of the replica. ") c.argument('revision', help="The name of the container app revision. Defaults to the latest revision.") c.argument('name', name_type, id_part=None, help="The name of the Containerapp.") c.argument('resource_group_name', arg_type=resource_group_name_type, id_part=None) # Container with self.argument_context('containerapp', arg_group='Container') as c: - # c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") c.argument('container_name', type=str, help="Name of the container.") c.argument('cpu', type=float, validator=validate_cpu, help="Required CPU in cores from 0.25 - 2.0, e.g. 0.5") c.argument('memory', type=str, validator=validate_memory, help="Required memory from 0.5 - 4.0 ending with \"Gi\", e.g. 1.0Gi") @@ -225,7 +224,6 @@ def load_arguments(self, _): c.argument('name', configured_default='name', id_part=None) c.argument('managed_env', configured_default='managed_env') c.argument('registry_server', configured_default='registry_server') - c.argument('dryrun', help="Show summary of the operation instead of executing it.") c.argument('source', type=str, help='Local directory path to upload to Azure container registry.') c.argument('image', type=str, options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.") c.argument('browse', help='Open the app in a web browser after creation and deployment, if possible.') diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index e72b829acde..d7091624cb9 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -156,7 +156,7 @@ def is_int(s): return False -def await_github_action(cmd, token, repo, branch, name, resource_group_name, timeout=300): +def await_github_action(cmd, token, repo, branch, name, resource_group_name, timeout_secs=300): from .custom import show_github_action from github import Github from time import sleep @@ -187,7 +187,7 @@ def await_github_action(cmd, token, repo, branch, name, resource_group_name, tim sleep(1) animation.tick() - if (datetime.utcnow() - start).seconds >= timeout: + if (datetime.utcnow() - start).seconds >= timeout_secs: raise CLIInternalError("Timed out while waiting for the Github action to start.") animation.flush() @@ -202,7 +202,7 @@ def await_github_action(cmd, token, repo, branch, name, resource_group_name, tim animation.tick() status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] animation.flush() - if (datetime.utcnow() - start).seconds >= timeout: + if (datetime.utcnow() - start).seconds >= timeout_secs: raise CLIInternalError("Timed out while waiting for the Github action to start.") if status != "completed": diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 3276459f468..d0f615849f3 100644 --- a/src/containerapp/setup.py +++ b/src/containerapp/setup.py @@ -17,7 +17,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '0.4.0' +VERSION = '0.3.2' # The full list of classifiers is available at From 78b1c50e25ad8a37b84f66e8a660ef939e65b53a Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 15:16:53 -0700 Subject: [PATCH 155/158] fix minor typo --- src/containerapp/azext_containerapp/_up_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index f13ba3414a6..084399ed77a 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -481,9 +481,9 @@ def up_output(app): output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" f"Browse to your container app at: {url} \n\n" f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" - f"See full output using: az containerapp show n {app.name} -g {app.resource_group.name} \n") + f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") else: output = (f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n\n" f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n\n" - f"See full output using: az containerapp show n {app.name} -g {app.resource_group.name} \n") + f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") return output From 0ba71853bd89b31d35dd90d8dbc88f8da47d73bc Mon Sep 17 00:00:00 2001 From: Silas Strawn Date: Thu, 21 Apr 2022 18:47:50 -0700 Subject: [PATCH 156/158] bug fix where commands fail if providing registry creds --- src/containerapp/azext_containerapp/.flake8 | 4 +++ .../azext_containerapp/_up_utils.py | 35 +++++++++++++------ src/containerapp/azext_containerapp/custom.py | 4 ++- 3 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 src/containerapp/azext_containerapp/.flake8 diff --git a/src/containerapp/azext_containerapp/.flake8 b/src/containerapp/azext_containerapp/.flake8 new file mode 100644 index 00000000000..777d0ca9ecd --- /dev/null +++ b/src/containerapp/azext_containerapp/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = + W503 # line break before binary operator, not compliant with PEP 8 + E203 # whitespace before ':', not compliant with PEP 8 \ No newline at end of file diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index 084399ed77a..44f6665158c 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.py @@ -15,6 +15,9 @@ MutuallyExclusiveArgumentError) from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.command_modules.appservice._create_util import check_resource_group_exists +from azure.cli.command_modules.acr.custom import acr_show +from azure.cli.core.commands.client_factory import get_mgmt_service_client +from azure.mgmt.containerregistry import ContainerRegistryManagementClient from knack.log import get_logger from msrestazure.tools import parse_resource_id, is_valid_resource_id, resource_id @@ -382,16 +385,19 @@ def _get_env_and_group_from_log_analytics(cmd, resource_group_name, env:'Contain def _get_acr_from_image(cmd, app): if app.image is not None and "azurecr.io" in app.image: + app.registry_server = app.image.split('/')[0] # TODO what if this conflicts with registry_server param? + parsed = urlparse(app.image) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] if app.registry_user is None or app.registry_pass is None: logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - app.registry_server = app.image.split('/')[0] # TODO what if this conflicts with registry_server param? - parsed = urlparse(app.image) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] - try: - app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) - app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) - except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + try: + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) + except Exception as ex: + raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + else: + acr_rg = _get_acr_rg(app) + app.acr = AzureContainerRegistry(name=registry_name, resource_group=ResourceGroup(app.cmd, acr_rg, None, None)) def _get_registry_from_app(app): @@ -401,20 +407,27 @@ def _get_registry_from_app(app): app.registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] +def _get_acr_rg(app): + registry_name = app.registry_server[:app.registry_server.rindex(".azurecr.io")] + client = get_mgmt_service_client(app.cmd.cli_ctx, ContainerRegistryManagementClient).registries + return parse_resource_id(acr_show(app.cmd, client, registry_name).id)["resource_group"] + def _get_registry_details(cmd, app: 'ContainerApp'): registry_rg = None registry_name = None if app.registry_server: if "azurecr.io" not in app.registry_server: raise ValidationError("Cannot supply non-Azure registry when using --source.") + parsed = urlparse(app.registry_server) + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] if app.registry_user is None or app.registry_pass is None: logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') - parsed = urlparse(app.registry_server) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] try: app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) except Exception as ex: raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + else: + registry_rg = _get_acr_rg(app) else: registry_rg = app.resource_group.name user = get_profile_username() @@ -423,6 +436,7 @@ def _get_registry_details(cmd, app: 'ContainerApp'): registry_name = f"ca{registry_name}acr" # ACR names must start + end in a letter app.registry_server = registry_name + ".azurecr.io" app.should_create_acr = True + app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) @@ -438,6 +452,7 @@ def _set_up_defaults(cmd, name, resource_group_name, logs_customer_id, location, # get ACR details from --image, if possible _get_acr_from_image(cmd, app) + def _create_github_action(app:'ContainerApp', env:'ContainerAppEnvironment', service_principal_client_id, service_principal_client_secret, service_principal_tenant_id, diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 082e1b2a0c8..a3515ca44df 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -2007,7 +2007,7 @@ def containerapp_up(cmd, service_principal_tenant_id=None): from ._up_utils import (_validate_up_args, _reformat_image, _get_dockerfile_content, _get_ingress_and_target_port, ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app, - _get_registry_details, _create_github_action, _set_up_defaults, up_output) + _get_registry_details, _create_github_action, _set_up_defaults, up_output, AzureContainerRegistry) dockerfile="Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) @@ -2033,6 +2033,8 @@ def containerapp_up(cmd, env.create_if_needed(name) app.create_acr_if_needed() + + if source: app.run_acr_build(dockerfile, source) From c7e07db814eb37c215d55c3cdf09e2f20ea05fa8 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 22 Apr 2022 16:22:14 -0400 Subject: [PATCH 157/158] Fixed style issues. --- .../azext_containerapp/_archive_utils.py | 13 +- .../azext_containerapp/_github_oauth.py | 2 +- .../azext_containerapp/_params.py | 2 +- .../azext_containerapp/_up_utils.py | 619 ++++++++++++------ src/containerapp/azext_containerapp/_utils.py | 28 +- src/containerapp/azext_containerapp/custom.py | 34 +- 6 files changed, 447 insertions(+), 251 deletions(-) diff --git a/src/containerapp/azext_containerapp/_archive_utils.py b/src/containerapp/azext_containerapp/_archive_utils.py index a32e380ad69..9130e6ab4f9 100644 --- a/src/containerapp/azext_containerapp/_archive_utils.py +++ b/src/containerapp/azext_containerapp/_archive_utils.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, consider-using-with, no-member import tarfile import os @@ -10,8 +11,8 @@ from io import open import requests from knack.log import get_logger -from knack.util import CLIError from msrestazure.azure_exceptions import CloudError +from azure.cli.core.azclierror import (CLIInternalError) from azure.cli.core.profiles import ResourceType, get_sdk from azure.cli.command_modules.acr._azure_utils import get_blob_info from azure.cli.command_modules.acr._constants import TASK_VALID_VSTS_URLS @@ -48,10 +49,10 @@ def upload_source_code(cmd, client, upload_url = source_upload_location.upload_url relative_path = source_upload_location.relative_path except (AttributeError, CloudError) as e: - raise CLIError("Failed to get a SAS URL to upload context. Error: {}".format(e.message)) + raise CLIInternalError("Failed to get a SAS URL to upload context. Error: {}".format(e.message)) from e if not upload_url: - raise CLIError("Failed to get a SAS URL to upload context.") + raise CLIInternalError("Failed to get a SAS URL to upload context.") account_name, endpoint_suffix, container_name, blob_name, sas_token = get_blob_info(upload_url) BlockBlobService = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE, 'blob#BlockBlobService') @@ -192,7 +193,7 @@ def _archive_file_recursively(tar, name, arcname, parent_ignored, parent_matchin tarinfo = tar.gettarinfo(name, arcname) if tarinfo is None: - raise CLIError("tarfile: unsupported type {}".format(name)) + raise CLIInternalError("tarfile: unsupported type {}".format(name)) # check if the file/dir is ignored ignored, matching_rule_index = ignore_check( @@ -234,9 +235,9 @@ def check_remote_source_code(source_location): # Others are tarball if requests.head(source_location).status_code < 400: return source_location - raise CLIError("'{}' doesn't exist.".format(source_location)) + raise CLIInternalError("'{}' doesn't exist.".format(source_location)) # oci if lower_source_location.startswith("oci://"): return source_location - raise CLIError("'{}' doesn't exist.".format(source_location)) + raise CLIInternalError("'{}' doesn't exist.".format(source_location)) diff --git a/src/containerapp/azext_containerapp/_github_oauth.py b/src/containerapp/azext_containerapp/_github_oauth.py index 41fe984be0d..96144b2d929 100644 --- a/src/containerapp/azext_containerapp/_github_oauth.py +++ b/src/containerapp/azext_containerapp/_github_oauth.py @@ -4,9 +4,9 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=consider-using-f-string +from azure.cli.core.util import open_page_in_browser from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) from knack.log import get_logger -from azure.cli.core.util import open_page_in_browser logger = get_logger(__name__) diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 01c6a4afa05..a1f6b3e2471 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -233,7 +233,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.') c.ignore('no_wait') - with self.argument_context('containerapp up', arg_group='Github Repo') as c: + with self.argument_context('containerapp up', arg_group='Github Repo') as c: c.argument('repo', help='Create an app via Github Actions. In the format: https://github.com// or /') 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. If missing (and using --repo), a browser page will be opened to authenticate with Github.') c.argument('branch', options_list=['--branch', '-b'], help='The branch of the GitHub repo. Defaults to "main"') diff --git a/src/containerapp/azext_containerapp/_up_utils.py b/src/containerapp/azext_containerapp/_up_utils.py index dfeae4135fd..9c8567a89e3 100644 --- a/src/containerapp/azext_containerapp/_up_utils.py +++ b/src/containerapp/azext_containerapp/_up_utils.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, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals +# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals, logging-fstring-interpolation, arguments-differ, abstract-method, logging-format-interpolation from urllib.parse import urlparse @@ -12,9 +12,12 @@ RequiredArgumentMissingError, ValidationError, InvalidArgumentValueError, - MutuallyExclusiveArgumentError) + MutuallyExclusiveArgumentError, +) from azure.cli.core.commands.client_factory import get_subscription_id -from azure.cli.command_modules.appservice._create_util import check_resource_group_exists +from azure.cli.command_modules.appservice._create_util import ( + check_resource_group_exists, +) from azure.cli.command_modules.acr.custom import acr_show from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.containerregistry import ContainerRegistryManagementClient @@ -24,18 +27,35 @@ from ._clients import ManagedEnvironmentClient, ContainerAppClient, GitHubActionClient -from ._utils import (get_randomized_name, get_profile_username, create_resource_group, - get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, - _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, - repo_url_to_name, get_container_app_if_exists) - -from .custom import (create_managed_environment, containerapp_up_logic, list_containerapp, - list_managed_environments, create_or_update_github_action) +from ._utils import ( + get_randomized_name, + get_profile_username, + create_resource_group, + get_resource_group, + queue_acr_build, + _get_acr_cred, + create_new_acr, + _get_default_containerapps_location, + safe_get, + is_int, + create_service_principal_for_rbac, + repo_url_to_name, + get_container_app_if_exists, +) + +from .custom import ( + create_managed_environment, + containerapp_up_logic, + list_containerapp, + list_managed_environments, + create_or_update_github_action, +) logger = get_logger(__name__) + class ResourceGroup: - def __init__(self, cmd, name: str, location: str, exists: bool=None): + def __init__(self, cmd, name: str, location: str, exists: bool = None): self.cmd = cmd self.name = name self.location = _get_default_containerapps_location(cmd, location) @@ -56,8 +76,8 @@ def _get(self): def get(self): r = None try: - r = self._get(self.cmd) - except: + r = self._get() + except: # pylint: disable=bare-except pass return r @@ -77,7 +97,9 @@ def create_if_needed(self): class Resource: - def __init__(self, cmd, name: str, resource_group: 'ResourceGroup', exists: bool=None): + def __init__( + self, cmd, name: str, resource_group: "ResourceGroup", exists: bool = None + ): self.cmd = cmd self.name = name self.resource_group = resource_group @@ -85,7 +107,6 @@ def __init__(self, cmd, name: str, resource_group: 'ResourceGroup', exists: bool self.check_exists() - def create(self, *args, **kwargs): raise NotImplementedError() @@ -96,7 +117,7 @@ def get(self): r = None try: r = self._get() - except: + except: # pylint: disable=bare-except pass return r @@ -109,21 +130,27 @@ def check_exists(self): def create_if_needed(self, *args, **kwargs): if not self.check_exists(): - logger.warning(f"Creating {type(self).__name__} '{self.name}' in resource group {self.resource_group.name}") + logger.warning( + f"Creating {type(self).__name__} '{self.name}' in resource group {self.resource_group.name}" + ) self.create(*args, **kwargs) else: - logger.warning(f"Using {type(self).__name__} '{self.name}' in resource group {self.resource_group.name}") # TODO use .info() + logger.warning( + f"Using {type(self).__name__} '{self.name}' in resource group {self.resource_group.name}" + ) # TODO use .info() class ContainerAppEnvironment(Resource): - def __init__(self, - cmd, - name: str, - resource_group: 'ResourceGroup', - exists: bool=None, - location=None, - logs_key=None, - logs_customer_id=None): + def __init__( + self, + cmd, + name: str, + resource_group: "ResourceGroup", + exists: bool = None, + location=None, + logs_key=None, + logs_customer_id=None, + ): super().__init__(cmd, name, resource_group, exists) if is_valid_resource_id(name): @@ -131,83 +158,92 @@ def __init__(self, rg = parse_resource_id(name)["resource_group"] if resource_group.name != rg: self.resource_group = ResourceGroup(cmd, rg, location) - self.location=_get_default_containerapps_location(cmd, location) - self.logs_key=logs_key - self.logs_customer_id=logs_customer_id + self.location = _get_default_containerapps_location(cmd, location) + self.logs_key = logs_key + self.logs_customer_id = logs_customer_id def set_name(self, name_or_rid): if is_valid_resource_id(name_or_rid): self.name = parse_resource_id(name_or_rid)["name"] rg = parse_resource_id(name_or_rid)["resource_group"] if self.resource_group.name != rg: - self.resource_group = ResourceGroup(self.cmd, rg, _get_default_containerapps_location(self.cmd, - self.location)) + self.resource_group = ResourceGroup( + self.cmd, + rg, + _get_default_containerapps_location(self.cmd, self.location), + ) else: self.name = name_or_rid - def _get(self): - return ManagedEnvironmentClient.show(self.cmd, self.resource_group.name, self.name) + return ManagedEnvironmentClient.show( + self.cmd, self.resource_group.name, self.name + ) def create(self, app_name): if self.name is None: self.name = "{}-env".format(app_name).replace("_", "-") - env = create_managed_environment(self.cmd, - self.name, - location=self.location, - resource_group_name=self.resource_group.name, - logs_key=self.logs_key, - logs_customer_id=self.logs_customer_id, disable_warnings=True) + env = create_managed_environment( + self.cmd, + self.name, + location=self.location, + resource_group_name=self.resource_group.name, + logs_key=self.logs_key, + logs_customer_id=self.logs_customer_id, + disable_warnings=True, + ) self.exists = True return env def get_rid(self): rid = self.name if not is_valid_resource_id(self.name): - rid = resource_id(subscription=get_subscription_id(self.cmd.cli_ctx), - resource_group=self.resource_group.name, - namespace='Microsoft.App', - type='managedEnvironments', - name=self.name) + rid = resource_id( + subscription=get_subscription_id(self.cmd.cli_ctx), + resource_group=self.resource_group.name, + namespace="Microsoft.App", + type="managedEnvironments", + name=self.name, + ) return rid class AzureContainerRegistry(Resource): - def __init__(self, - name: str, - resource_group: 'ResourceGroup'): + def __init__(self, name: str, resource_group: "ResourceGroup"): # pylint: disable=super-init-not-called self.name = name self.resource_group = resource_group -class ContainerApp(Resource): - def __init__(self, - cmd, - name: str, - resource_group: 'ResourceGroup', - exists: bool=None, - image=None, - env: 'ContainerAppEnvironment'=None, - target_port=None, - registry_server=None, - registry_user=None, - registry_pass=None, - env_vars=None, - ingress=None): +class ContainerApp(Resource): # pylint: disable=too-many-instance-attributes + def __init__( + self, + cmd, + name: str, + resource_group: "ResourceGroup", + exists: bool = None, + image=None, + env: "ContainerAppEnvironment" = None, + target_port=None, + registry_server=None, + registry_user=None, + registry_pass=None, + env_vars=None, + ingress=None, + ): super().__init__(cmd, name, resource_group, exists) - self.image=image - self.env=env - self.target_port=target_port - self.registry_server=registry_server - self.registry_user=registry_user + self.image = image + self.env = env + self.target_port = target_port + self.registry_server = registry_server + self.registry_user = registry_user self.registry_pass = registry_pass - self.env_vars=env_vars - self.ingress=ingress + self.env_vars = env_vars + self.ingress = ingress self.should_create_acr = False - self.acr: 'AzureContainerRegistry' = None + self.acr: "AzureContainerRegistry" = None def _get(self): return ContainerAppClient.show(self.cmd, self.resource_group.name, self.name) @@ -215,57 +251,88 @@ def _get(self): def create(self, no_registry=False): # no_registry: don't pass in a registry during create even if the app has one (used for GH actions) if get_container_app_if_exists(self.cmd, self.resource_group.name, self.name): - logger.warning(f"Updating Containerapp {self.name} in resource group {self.resource_group.name}") + logger.warning( + f"Updating Containerapp {self.name} in resource group {self.resource_group.name}" + ) else: - logger.warning(f"Creating Containerapp {self.name} in resource group {self.resource_group.name}") - - return containerapp_up_logic(cmd=self.cmd, - name=self.name, - resource_group_name=self.resource_group.name, - image=self.image, - managed_env=self.env.get_rid(), - target_port=self.target_port, - registry_server=None if no_registry else self.registry_server, - registry_pass=None if no_registry else self.registry_pass, - registry_user=None if no_registry else self.registry_user, - env_vars=self.env_vars, - ingress=self.ingress) + logger.warning( + f"Creating Containerapp {self.name} in resource group {self.resource_group.name}" + ) + + return containerapp_up_logic( + cmd=self.cmd, + name=self.name, + resource_group_name=self.resource_group.name, + image=self.image, + managed_env=self.env.get_rid(), + target_port=self.target_port, + registry_server=None if no_registry else self.registry_server, + registry_pass=None if no_registry else self.registry_pass, + registry_user=None if no_registry else self.registry_user, + env_vars=self.env_vars, + ingress=self.ingress, + ) def create_acr_if_needed(self): if self.should_create_acr: - logger.warning(f"Creating Azure Container Registry {self.acr.name} in resource group " - f"{self.acr.resource_group.name}") + logger.warning( + f"Creating Azure Container Registry {self.acr.name} in resource group " + f"{self.acr.resource_group.name}" + ) self.create_acr() def create_acr(self): registry_rg = self.resource_group url = self.registry_server - registry_name = url[:url.rindex(".azurecr.io")] - registry_def = create_new_acr(self.cmd, registry_name, registry_rg.name, self.env.location) + registry_name = url[: url.rindex(".azurecr.io")] + registry_def = create_new_acr( + self.cmd, registry_name, registry_rg.name, self.env.location + ) self.registry_server = registry_def.login_server if not self.acr: - self.acr = AzureContainerRegistry() - self.acr.name = registry_name - self.acr.resource_group = registry_rg + self.acr = AzureContainerRegistry(registry_name, registry_rg) - self.registry_user, self.registry_pass, _ = _get_acr_cred(self.cmd.cli_ctx, registry_name) + self.registry_user, self.registry_pass, _ = _get_acr_cred( + self.cmd.cli_ctx, registry_name + ) def run_acr_build(self, dockerfile, source, quiet=False): image_name = self.image if self.image is not None else self.name from datetime import datetime + now = datetime.now() # Add version tag for acr image - image_name += ":{}".format(str(now).replace(' ', '').replace('-', '').replace('.', '').replace(':', '')) + image_name += ":{}".format( + str(now).replace(" ", "").replace("-", "").replace(".", "").replace(":", "") + ) + + self.image = self.registry_server + "/" + image_name + queue_acr_build( + self.cmd, + self.acr.resource_group.name, + self.acr.name, + image_name, + source, + dockerfile, + quiet, + ) - self.image = self.registry_server + '/' + image_name - queue_acr_build(self.cmd, self.acr.resource_group.name, self.acr.name, image_name, source, dockerfile, quiet) def _create_service_principal(cmd, resource_group_name, env_resource_group_name): - logger.warning("No valid service principal provided. Creating a new service principal...") - scopes = [f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}"] - if env_resource_group_name is not None and env_resource_group_name != resource_group_name: - scopes.append(f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{env_resource_group_name}") + logger.warning( + "No valid service principal provided. Creating a new service principal..." + ) + scopes = [ + f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{resource_group_name}" + ] + if ( + env_resource_group_name is not None + and env_resource_group_name != resource_group_name + ): + scopes.append( + f"/subscriptions/{get_subscription_id(cmd.cli_ctx)}/resourceGroups/{env_resource_group_name}" + ) sp = create_service_principal_for_rbac(cmd, scopes=scopes, role="contributor") logger.info(f"Created service principal: {sp['displayName']}") @@ -273,24 +340,42 @@ def _create_service_principal(cmd, resource_group_name, env_resource_group_name) return sp["appId"], sp["password"], sp["tenant"] -def _get_or_create_sp(cmd, resource_group_name, env_resource_group_name, name, service_principal_client_id, - service_principal_client_secret, service_principal_tenant_id): +def _get_or_create_sp( # pylint: disable=inconsistent-return-statements + cmd, + resource_group_name, + env_resource_group_name, + name, + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id, +): try: - GitHubActionClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - return service_principal_client_id, service_principal_client_secret, service_principal_tenant_id - except: + GitHubActionClient.show( + cmd=cmd, resource_group_name=resource_group_name, name=name + ) + return ( + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id, + ) + except: # pylint: disable=bare-except service_principal = None # TODO if possible, search for SPs with the right credentials # I haven't found a way to get SP creds + secrets yet from the API if not service_principal: - return _create_service_principal(cmd, resource_group_name, env_resource_group_name) + return _create_service_principal( + cmd, resource_group_name, env_resource_group_name + ) # return client_id, secret, tenant_id -def _get_dockerfile_content_from_repo(repo_url, branch, token, context_path, dockerfile): +def _get_dockerfile_content_from_repo( # pylint: disable=inconsistent-return-statements + repo_url, branch, token, context_path, dockerfile +): from github import Github + g = Github(token) context_path = context_path or "." repo = repo_url_to_name(repo_url) @@ -303,45 +388,63 @@ def _get_dockerfile_content_from_repo(repo_url, branch, token, context_path, doc return resp.content.decode("utf-8").split("\n") -def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: 'list[str]'): - if not target_port and not ingress and dockerfile_content is not None: +def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: "list[str]"): + if not target_port and not ingress and dockerfile_content is not None: # pylint: disable=too-many-nested-blocks for line in dockerfile_content: if line: - line = line.upper().strip().replace("/TCP", "").replace("/UDP", "").replace("\n","") + line = ( + line.upper() + .strip() + .replace("/TCP", "") + .replace("/UDP", "") + .replace("\n", "") + ) if line and line[0] != "#": if "EXPOSE" in line: - parts = line.split(" ") - for i, p in enumerate(parts[:-1]): - if "EXPOSE" in p and is_int(parts[i+1]): - target_port = parts[i+1] - ingress = "external" - logger.warning("Adding external ingress port {} based on dockerfile expose.".format(target_port)) + parts = line.split(" ") + for i, p in enumerate(parts[:-1]): + if "EXPOSE" in p and is_int(parts[i + 1]): + target_port = parts[i + 1] + ingress = "external" + logger.warning( + "Adding external ingress port {} based on dockerfile expose.".format( + target_port + ) + ) ingress = "external" if target_port and not ingress else ingress return ingress, target_port def _validate_up_args(source, image, repo): if not source and not image and not repo: - raise RequiredArgumentMissingError("You must specify either --source, --repo, or --image") + raise RequiredArgumentMissingError( + "You must specify either --source, --repo, or --image" + ) if source and repo: - raise MutuallyExclusiveArgumentError("Cannot use --source and --repo togther. " - "Can either deploy from a local directory or a Github repo") + raise MutuallyExclusiveArgumentError( + "Cannot use --source and --repo togther. " + "Can either deploy from a local directory or a Github repo" + ) + def _reformat_image(source, repo, image): if source and (image or repo): - image = image.split('/')[-1] # if link is given - image = image.replace(':', '') + image = image.split("/")[-1] # if link is given + image = image.replace(":", "") return image + def _get_dockerfile_content_local(source, dockerfile): lines = [] if source: dockerfile_location = f"{source}/{dockerfile}" try: - with open(dockerfile_location, 'r') as fh: - lines = [line for line in fh] - except: - raise InvalidArgumentValueError("Cannot open specified Dockerfile. Check dockerfile name, path, and permissions.") + with open(dockerfile_location, "r") as fh: # pylint: disable=unspecified-encoding + lines = list(fh) + except Exception as e: + raise InvalidArgumentValueError( + "Cannot open specified Dockerfile. Check dockerfile name, path, and permissions." + ) from e return lines @@ -349,32 +452,66 @@ def _get_dockerfile_content(repo, branch, token, source, context_path, dockerfil if source: return _get_dockerfile_content_local(source, dockerfile) elif repo: - return _get_dockerfile_content_from_repo(repo, branch, token, context_path, dockerfile) + return _get_dockerfile_content_from_repo( + repo, branch, token, context_path, dockerfile + ) return [] -def _get_app_env_and_group(cmd, name, resource_group: 'ResourceGroup', env: 'ContainerAppEnvironment'): +def _get_app_env_and_group( + cmd, name, resource_group: "ResourceGroup", env: "ContainerAppEnvironment" +): if not resource_group.name and not resource_group.exists: - matched_apps = [c for c in list_containerapp(cmd) if c['name'].lower() == name.lower()] + matched_apps = [ + c for c in list_containerapp(cmd) if c["name"].lower() == name.lower() + ] if len(matched_apps) == 1: - if env.name: - logger.warning("User passed custom environment name for an existing containerapp. Using existing environment.") - resource_group.name = parse_resource_id(matched_apps[0]["id"])["resource_group"] - env.set_name(matched_apps[0]["properties"]["managedEnvironmentId"]) + if env.name: + logger.warning( + "User passed custom environment name for an existing containerapp. Using existing environment." + ) + resource_group.name = parse_resource_id(matched_apps[0]["id"])[ + "resource_group" + ] + env.set_name(matched_apps[0]["properties"]["managedEnvironmentId"]) elif len(matched_apps) > 1: - raise ValidationError(f"There are multiple containerapps with name {name} on the subscription. " - "Please specify which resource group your Containerapp is in.") - - -def _get_env_and_group_from_log_analytics(cmd, resource_group_name, env:'ContainerAppEnvironment', resource_group:'ResourceGroup', logs_customer_id, location): + raise ValidationError( + f"There are multiple containerapps with name {name} on the subscription. " + "Please specify which resource group your Containerapp is in." + ) + + +def _get_env_and_group_from_log_analytics( + cmd, + resource_group_name, + env: "ContainerAppEnvironment", + resource_group: "ResourceGroup", + logs_customer_id, + location, +): # resource_group_name is the value the user passed in (if present) if not env.name: - if (resource_group_name == resource_group.name and resource_group.exists) or (not resource_group_name): - env_list = list_managed_environments(cmd=cmd, resource_group_name=resource_group_name) + if (resource_group_name == resource_group.name and resource_group.exists) or ( + not resource_group_name + ): + env_list = list_managed_environments( + cmd=cmd, resource_group_name=resource_group_name + ) if logs_customer_id: - env_list = [e for e in env_list if safe_get(e, "properties", "appLogsConfiguration", "logAnalyticsConfiguration", "customerId") == logs_customer_id] + env_list = [ + e + for e in env_list + if safe_get( + e, + "properties", + "appLogsConfiguration", + "logAnalyticsConfiguration", + "customerId", + ) + == logs_customer_id + ] if location: - env_list = [e for e in env_list if e['location'] == location] + env_list = [e for e in env_list if e["location"] == location] if env_list: # TODO check how many CA in env env_details = parse_resource_id(env_list[0]["id"]) @@ -384,115 +521,195 @@ def _get_env_and_group_from_log_analytics(cmd, resource_group_name, env:'Contain def _get_acr_from_image(cmd, app): if app.image is not None and "azurecr.io" in app.image: - app.registry_server = app.image.split('/')[0] # TODO what if this conflicts with registry_server param? + app.registry_server = app.image.split("/")[ + 0 + ] # TODO what if this conflicts with registry_server param? parsed = urlparse(app.image) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split(".")[0] if app.registry_user is None or app.registry_pass is None: - logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') + logger.info( + "No credential was provided to access Azure Container Registry. Trying to look up..." + ) try: - app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) - app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred( + cmd.cli_ctx, registry_name + ) + app.acr = AzureContainerRegistry( + registry_name, ResourceGroup(cmd, registry_rg, None, None) + ) except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + raise RequiredArgumentMissingError( + "Failed to retrieve credentials for container registry. Please provide the registry username and password" + ) from ex else: acr_rg = _get_acr_rg(app) - app.acr = AzureContainerRegistry(name=registry_name, resource_group=ResourceGroup(app.cmd, acr_rg, None, None)) + app.acr = AzureContainerRegistry( + name=registry_name, + resource_group=ResourceGroup(app.cmd, acr_rg, None, None), + ) def _get_registry_from_app(app): containerapp_def = app.get() if containerapp_def: - if len(safe_get(containerapp_def, "properties", "configuration", "registries", default=[])) == 1: - app.registry_server = containerapp_def["properties"]["configuration"]["registries"][0]["server"] + if ( + len( + safe_get( + containerapp_def, + "properties", + "configuration", + "registries", + default=[], + ) + ) + == 1 + ): + app.registry_server = containerapp_def["properties"]["configuration"][ + "registries" + ][0]["server"] def _get_acr_rg(app): - registry_name = app.registry_server[:app.registry_server.rindex(".azurecr.io")] - client = get_mgmt_service_client(app.cmd.cli_ctx, ContainerRegistryManagementClient).registries - return parse_resource_id(acr_show(app.cmd, client, registry_name).id)["resource_group"] + registry_name = app.registry_server[: app.registry_server.rindex(".azurecr.io")] + client = get_mgmt_service_client( + app.cmd.cli_ctx, ContainerRegistryManagementClient + ).registries + return parse_resource_id(acr_show(app.cmd, client, registry_name).id)[ + "resource_group" + ] + -def _get_registry_details(cmd, app: 'ContainerApp'): +def _get_registry_details(cmd, app: "ContainerApp"): registry_rg = None registry_name = None if app.registry_server: if "azurecr.io" not in app.registry_server: - raise ValidationError("Cannot supply non-Azure registry when using --source.") + raise ValidationError( + "Cannot supply non-Azure registry when using --source." + ) parsed = urlparse(app.registry_server) - registry_name = (parsed.netloc if parsed.scheme else parsed.path).split('.')[0] + registry_name = (parsed.netloc if parsed.scheme else parsed.path).split(".")[0] if app.registry_user is None or app.registry_pass is None: - logger.info('No credential was provided to access Azure Container Registry. Trying to look up...') + logger.info( + "No credential was provided to access Azure Container Registry. Trying to look up..." + ) try: - app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(cmd.cli_ctx, registry_name) + app.registry_user, app.registry_pass, registry_rg = _get_acr_cred( + cmd.cli_ctx, registry_name + ) except Exception as ex: - raise RequiredArgumentMissingError('Failed to retrieve credentials for container registry. Please provide the registry username and password') from ex + raise RequiredArgumentMissingError( + "Failed to retrieve credentials for container registry. Please provide the registry username and password" + ) from ex else: registry_rg = _get_acr_rg(app) else: registry_rg = app.resource_group.name user = get_profile_username() - registry_name = app.name.replace('-','').lower() - registry_name = registry_name + str(hash((registry_rg, user, app.name))).replace("-", "").replace(".", "")[:10] # cap at 15 characters total - registry_name = f"ca{registry_name}acr" # ACR names must start + end in a letter + registry_name = app.name.replace("-", "").lower() + registry_name = ( + registry_name + + str(hash((registry_rg, user, app.name))) + .replace("-", "") + .replace(".", "")[:10] + ) # cap at 15 characters total + registry_name = ( + f"ca{registry_name}acr" # ACR names must start + end in a letter + ) app.registry_server = registry_name + ".azurecr.io" app.should_create_acr = True - app.acr = AzureContainerRegistry(registry_name, ResourceGroup(cmd, registry_rg, None, None)) + app.acr = AzureContainerRegistry( + registry_name, ResourceGroup(cmd, registry_rg, None, None) + ) # attempt to populate defaults for managed env, RG, ACR, etc -def _set_up_defaults(cmd, name, resource_group_name, logs_customer_id, location, - resource_group: 'ResourceGroup', env:'ContainerAppEnvironment', app:'ContainerApp'): +def _set_up_defaults( + cmd, + name, + resource_group_name, + logs_customer_id, + location, + resource_group: "ResourceGroup", + env: "ContainerAppEnvironment", + app: "ContainerApp", +): # If no RG passed in and a singular app exists with the same name, get its env and rg _get_app_env_and_group(cmd, name, resource_group, env) # If no env passed in (and not creating a new RG), then try getting an env by location / log analytics ID - _get_env_and_group_from_log_analytics(cmd, resource_group_name, env, resource_group, logs_customer_id, location) + _get_env_and_group_from_log_analytics( + cmd, resource_group_name, env, resource_group, logs_customer_id, location + ) # get ACR details from --image, if possible _get_acr_from_image(cmd, app) -def _create_github_action(app:'ContainerApp', - env:'ContainerAppEnvironment', - service_principal_client_id, service_principal_client_secret, service_principal_tenant_id, - branch, - token, - repo, - context_path): - - sp = _get_or_create_sp(app.cmd, - app.resource_group.name, - env.resource_group.name, - app.name, - service_principal_client_id, - service_principal_client_secret, - service_principal_tenant_id) - service_principal_client_id, service_principal_client_secret, service_principal_tenant_id = sp - gh_action = create_or_update_github_action(cmd=app.cmd, - name=app.name, - resource_group_name=app.resource_group.name, - repo_url=repo, - registry_url=app.registry_server, - registry_username=app.registry_user, - registry_password=app.registry_pass, - branch=branch, - token=token, - login_with_github=False, - service_principal_client_id=service_principal_client_id, - service_principal_client_secret=service_principal_client_secret, - service_principal_tenant_id=service_principal_tenant_id, - image=app.image, - context_path=context_path) +def _create_github_action( + app: "ContainerApp", + env: "ContainerAppEnvironment", + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id, + branch, + token, + repo, + context_path, +): + + sp = _get_or_create_sp( + app.cmd, + app.resource_group.name, + env.resource_group.name, + app.name, + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id, + ) + ( + service_principal_client_id, + service_principal_client_secret, + service_principal_tenant_id, + ) = sp + create_or_update_github_action( + cmd=app.cmd, + name=app.name, + resource_group_name=app.resource_group.name, + repo_url=repo, + registry_url=app.registry_server, + registry_username=app.registry_user, + registry_password=app.registry_pass, + branch=branch, + token=token, + login_with_github=False, + service_principal_client_id=service_principal_client_id, + service_principal_client_secret=service_principal_client_secret, + service_principal_tenant_id=service_principal_tenant_id, + image=app.image, + context_path=context_path, + ) def up_output(app): - url = safe_get(ContainerAppClient.show(app.cmd, app.resource_group.name, app.name), "properties", - "configuration", - "ingress", "fqdn") + url = safe_get( + ContainerAppClient.show(app.cmd, app.resource_group.name, app.name), + "properties", + "configuration", + "ingress", + "fqdn", + ) if url and not url.startswith("http"): url = f"http://{url}" - logger.warning(f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n") + logger.warning( + f"\nYour container app ({app.name}) has been created a deployed! Congrats! \n" + ) url and logger.warning(f"Browse to your container app at: {url} \n") - logger.warning(f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n") - logger.warning(f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n") + logger.warning( + f"Stream logs for your container with: az containerapp logs -n {app.name} -g {app.resource_group.name} \n" + ) + logger.warning( + f"See full output using: az containerapp show -n {app.name} -g {app.resource_group.name} \n" + ) diff --git a/src/containerapp/azext_containerapp/_utils.py b/src/containerapp/azext_containerapp/_utils.py index d7091624cb9..0429f1dd1ba 100644 --- a/src/containerapp/azext_containerapp/_utils.py +++ b/src/containerapp/azext_containerapp/_utils.py @@ -2,19 +2,20 @@ # 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, expression-not-assigned, too-many-locals +# pylint: disable=line-too-long, consider-using-f-string, no-else-return, duplicate-string-formatting-argument, expression-not-assigned, too-many-locals, logging-fstring-interpolation import time import json -import datetime -from dateutil.relativedelta import relativedelta import platform + from urllib.parse import urlparse +from datetime import datetime +from dateutil.relativedelta import relativedelta from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError, ResourceNotFoundError, ArgumentUsageError) from azure.cli.core.commands.client_factory import get_subscription_id from knack.log import get_logger -from msrestazure.tools import parse_resource_id, is_valid_resource_id +from msrestazure.tools import parse_resource_id, is_valid_resource_id, resource_id from ._clients import ContainerAppClient from ._client_factory import handle_raw_exception, providers_client_factory, cf_resource_groups, log_analytics_client_factory, log_analytics_shared_key_client_factory @@ -22,19 +23,16 @@ logger = get_logger(__name__) - # original implementation at azure.cli.command_modules.role.custom.create_service_principal_for_rbac # reimplemented to remove unnecessary warning statements -def create_service_principal_for_rbac( - # pylint:disable=too-many-statements,too-many-locals, too-many-branches, unused-argument +def create_service_principal_for_rbac( # pylint:disable=too-many-statements,too-many-locals, too-many-branches, unused-argument, inconsistent-return-statements cmd, name=None, years=None, create_cert=False, cert=None, scopes=None, role=None, show_auth_for_sdk=None, skip_assignment=False, keyvault=None): from azure.cli.command_modules.role.custom import (_graph_client_factory, TZ_UTC, _process_service_principal_creds, - _validate_app_dates, create_application, + _validate_app_dates, create_application, _create_service_principal, _create_role_assignment, _error_caused_by_role_assignment_exists) - if role and not scopes or not role and scopes: raise ArgumentUsageError("Usage error: To create role assignments, specify both --role and --scopes.") @@ -161,7 +159,6 @@ def await_github_action(cmd, token, repo, branch, name, resource_group_name, tim from github import Github from time import sleep from ._clients import PollingAnimation - from datetime import datetime start = datetime.utcnow() @@ -171,7 +168,6 @@ def await_github_action(cmd, token, repo, branch, name, resource_group_name, tim github_repo = g.get_repo(repo) - workflow = None while workflow is None: workflows = github_repo.get_workflows() @@ -191,13 +187,14 @@ def await_github_action(cmd, token, repo, branch, name, resource_group_name, tim raise CLIInternalError("Timed out while waiting for the Github action to start.") animation.flush() - animation.tick(); animation.flush() + animation.tick() + animation.flush() run = workflow.get_runs()[0] logger.warning(f"Github action run: https://github.com/{repo}/actions/runs/{run.id}") logger.warning("Waiting for deployment to complete...") run_id = run.id status = run.status - while status == "queued" or status == "in_progress": + while status in ('queued', 'in_progress'): sleep(3) animation.tick() status = [wf.status for wf in workflow.get_runs() if wf.id == run_id][0] @@ -410,7 +407,7 @@ def get_container_app_if_exists(cmd, resource_group_name, name): app = None try: app = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name) - except: + except: # pylint: disable=bare-except pass return app @@ -554,7 +551,6 @@ def _get_existing_secrets(cmd, resource_group_name, name, containerapp_def): 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 @@ -676,8 +672,6 @@ def _add_or_update_tags(containerapp_def, tags): def _object_to_dict(obj): - import json - import datetime def default_handler(x): if isinstance(x, datetime.datetime): diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 6649c86f0ef..00a47081484 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -52,11 +52,8 @@ _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, get_randomized_name, _set_webapp_up_default_args, get_profile_username, create_resource_group, - get_resource_group, queue_acr_build, _get_acr_cred, create_new_acr, _get_log_analytics_workspace_name, - _get_default_containerapps_location, safe_get, is_int, create_service_principal_for_rbac, - _get_name, await_github_action, repo_url_to_name) + _ensure_identity_resource_id, _remove_dapr_readonly_attributes, _remove_env_vars, + _update_revision_env_secretrefs, _get_acr_cred, safe_get, await_github_action, repo_url_to_name) from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG, SSH_BACKUP_ENCODING) @@ -1056,8 +1053,8 @@ def _validate_github(repo, branch, token): 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']: @@ -2009,7 +2006,7 @@ def containerapp_up(cmd, ResourceGroup, ContainerAppEnvironment, ContainerApp, _get_registry_from_app, _get_registry_details, _create_github_action, _set_up_defaults, up_output, AzureContainerRegistry) - dockerfile="Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) + dockerfile = "Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated) _validate_up_args(source, image, repo) @@ -2034,15 +2031,13 @@ def containerapp_up(cmd, raise ValidationError("Containerapp has an existing provisioning in progress. Please wait until provisioning has completed and rerun the command.") if source or repo: - registry_server = _get_registry_from_app(app) # if the app exists, get the registry + _get_registry_from_app(app) # if the app exists, get the registry _get_registry_details(cmd, app) # fetch ACR creds from arguments registry arguments resource_group.create_if_needed() env.create_if_needed(name) app.create_acr_if_needed() - - if source: app.run_acr_build(dockerfile, source, False) @@ -2083,16 +2078,13 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en # check provisioning state here instead of secrets so no error _get_existing_secrets(cmd, resource_group_name, name, containerapp_def) - container = ContainerModel container["image"] = image container["name"] = name - if env_vars: container["env"] = parse_env_var_flags(env_vars) - external_ingress = None if ingress is not None: if ingress.lower() == "internal": @@ -2100,7 +2092,6 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en elif ingress.lower() == "external": external_ingress = True - ingress_def = None if target_port is not None and ingress is not None: ingress_def = IngressModel @@ -2108,7 +2099,6 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en ingress_def["targetPort"] = target_port containerapp_def["properties"]["configuration"]["ingress"] = ingress_def - # handle multi-container case if ca_exists: existing_containers = containerapp_def["properties"]["template"]["containers"] @@ -2131,19 +2121,15 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en containerapp_def["properties"]["template"] = TemplateModel containerapp_def["properties"]["template"]["containers"] = [container] - registries_def = None registry = None - - if "secrets" not in containerapp_def["properties"]["configuration"] or containerapp_def["properties"]["configuration"]["secrets"] == None: + if "secrets" not in containerapp_def["properties"]["configuration"] or containerapp_def["properties"]["configuration"]["secrets"] is None: containerapp_def["properties"]["configuration"]["secrets"] = [] - - if "registries" not in containerapp_def["properties"]["configuration"] or containerapp_def["properties"]["configuration"]["registries"] == None: + if "registries" not in containerapp_def["properties"]["configuration"] or containerapp_def["properties"]["configuration"]["registries"] is None: containerapp_def["properties"]["configuration"]["registries"] = [] - registries_def = containerapp_def["properties"]["configuration"]["registries"] if registry_server: @@ -2162,7 +2148,6 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en registry_pass, update_existing_secret=True) - # If not updating existing registry, add as new registry if not updating_existing_registry: registry = RegistryCredentialsModel @@ -2179,5 +2164,4 @@ def containerapp_up_logic(cmd, resource_group_name, name, managed_env, image, en if ca_exists: return ContainerAppClient.update(cmd, resource_group_name, name, containerapp_def) - else: - return ContainerAppClient.create_or_update(cmd, resource_group_name, name, containerapp_def) \ No newline at end of file + return ContainerAppClient.create_or_update(cmd, resource_group_name, name, containerapp_def) From ba1d01e5e8d4099b9f27d738927c5939682dd6d1 Mon Sep 17 00:00:00 2001 From: Haroon Feisal Date: Fri, 22 Apr 2022 16:32:06 -0400 Subject: [PATCH 158/158] Updated help and version text. --- src/containerapp/HISTORY.rst | 2 +- src/containerapp/azext_containerapp/_help.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 0e0e05ee905..70ffa6993c4 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -5,7 +5,7 @@ Release History 0.3.2 ++++++ -* Create or update a container app and all associated resources (container app environment, ACR, Github Actions, resource group, etc.) with 'az containerapp up' +* Added 'az containerapp up' to create or update a container app and all associated resources (container app environment, ACR, Github Actions, resource group, etc.) * Open an ssh-like shell in a Container App with 'az containerapp exec' * Support for log streaming with 'az containerapp logs show' * Replica show and list commands diff --git a/src/containerapp/azext_containerapp/_help.py b/src/containerapp/azext_containerapp/_help.py index 6ee0f6274c8..ab4cde705eb 100644 --- a/src/containerapp/azext_containerapp/_help.py +++ b/src/containerapp/azext_containerapp/_help.py @@ -114,15 +114,18 @@ type: command short-summary: Create or update a container app as well as any associated resources (ACR, resource group, container apps environment, Github Actions, etc.) examples: - - name: Create a container app from a Github repo (setting up github actions) + - name: Create a container app from a dockerfile in a Github repo (setting up github actions) text: | az containerapp up -n MyContainerapp --repo https://github.com/myAccount/myRepo - - name: Create a container app from content in a local directory + - name: Create a container app from a dockerfile in a local directory text: | az containerapp up -n MyContainerapp --source . - - name: Create a container app from a container in a registry + - name: Create a container app from an image in a registry text: | az containerapp up -n MyContainerapp --image myregistry.azurecr.io/myImage:myTag + - name: Create a container app from an image in a registry with ingress enabled and a specified environment + text: | + az containerapp up -n MyContainerapp --image myregistry.azurecr.io/myImage:myTag --ingress external --target-port 80 --environment MyEnv """ helps['containerapp logs'] = """