diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3468bafeffd..9c5297afae4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -228,6 +228,7 @@ /src/quota/ @kairu-ms @ZengTaoxu -/src/containerapp/ @calvinsID @haroonf @panchagnula +/src/containerapp/ @calvinsID @haroonf @panchagnula @StrawnSC /src/scvmm/ @nascarsayan + diff --git a/src/containerapp/HISTORY.rst b/src/containerapp/HISTORY.rst index 2abbd24f08a..f05316e2680 100644 --- a/src/containerapp/HISTORY.rst +++ b/src/containerapp/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +0.3.1 +++++++ +* Update "az containerapp github-action add" parameters: replace --docker-file-path with --context-path, add --image. + 0.3.0 ++++++ * Subgroup commands for managed identities: az containerapp identity diff --git a/src/containerapp/azext_containerapp/_clients.py b/src/containerapp/azext_containerapp/_clients.py index 77cf596c8bf..b4690cde26c 100644 --- a/src/containerapp/azext_containerapp/_clients.py +++ b/src/containerapp/azext_containerapp/_clients.py @@ -15,7 +15,8 @@ logger = get_logger(__name__) API_VERSION = "2021-03-01" -NEW_API_VERSION = "2022-01-01-preview" +PREVIEW_API_VERSION = "2022-01-01-preview" +STABLE_API_VERSION = "2022-03-01" POLLING_TIMEOUT = 60 # how many seconds before exiting POLLING_SECONDS = 2 # how many seconds between requests @@ -73,7 +74,7 @@ class ContainerAppClient(): @classmethod def create_or_update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -102,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 = NEW_API_VERSION + api_version = PREVIEW_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( @@ -131,7 +132,7 @@ def update(cls, cmd, resource_group_name, name, container_app_envelope, no_wait= @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 + api_version = PREVIEW_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( @@ -165,7 +166,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -183,7 +184,7 @@ 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 + api_version = PREVIEW_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) request_url = "{}/subscriptions/{}/providers/Microsoft.App/containerApps?api-version={}".format( management_hostname.strip('/'), @@ -211,7 +212,7 @@ 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 + api_version = PREVIEW_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( @@ -240,7 +241,7 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) def list_secrets(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -259,7 +260,7 @@ 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 + api_version = PREVIEW_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( @@ -288,7 +289,7 @@ def list_revisions(cls, cmd, resource_group_name, name, formatter=lambda x: x): @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 + api_version = PREVIEW_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( @@ -305,7 +306,7 @@ def show_revision(cls, cmd, resource_group_name, container_app_name, name): @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 + api_version = PREVIEW_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( @@ -322,7 +323,7 @@ def restart_revision(cls, cmd, resource_group_name, container_app_name, name): @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 + api_version = PREVIEW_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( @@ -339,7 +340,7 @@ def activate_revision(cls, cmd, resource_group_name, container_app_name, name): @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 + api_version = PREVIEW_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( @@ -358,7 +359,7 @@ class ManagedEnvironmentClient(): @classmethod def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -387,7 +388,7 @@ def create(cls, cmd, resource_group_name, name, managed_environment_envelope, no @classmethod def update(cls, cmd, resource_group_name, name, managed_environment_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -416,7 +417,7 @@ def update(cls, cmd, resource_group_name, name, managed_environment_envelope, no @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 + api_version = PREVIEW_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( @@ -451,7 +452,7 @@ def delete(cls, cmd, resource_group_name, name, no_wait=False): @classmethod def show(cls, cmd, resource_group_name, name): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -469,7 +470,7 @@ def list_by_subscription(cls, cmd, formatter=lambda x: x): env_list = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_API_VERSION sub_id = get_subscription_id(cmd.cli_ctx) request_url = "{}/subscriptions/{}/providers/Microsoft.App/managedEnvironments?api-version={}".format( management_hostname.strip('/'), @@ -497,7 +498,7 @@ def list_by_resource_group(cls, cmd, resource_group_name, formatter=lambda x: x) env_list = [] management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -527,7 +528,7 @@ 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 + api_version = STABLE_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( @@ -556,7 +557,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 = NEW_API_VERSION + api_version = STABLE_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( @@ -572,7 +573,7 @@ def show(cls, cmd, resource_group_name, name): @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 + api_version = PREVIEW_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( @@ -610,7 +611,7 @@ class DaprComponentClient(): def create_or_update(cls, cmd, resource_group_name, environment_name, name, dapr_component_envelope, no_wait=False): management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager - api_version = NEW_API_VERSION + api_version = PREVIEW_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( @@ -641,7 +642,7 @@ def create_or_update(cls, cmd, resource_group_name, environment_name, name, dapr @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 + api_version = PREVIEW_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( @@ -678,7 +679,7 @@ def delete(cls, cmd, resource_group_name, environment_name, name, no_wait=False) @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 + api_version = PREVIEW_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( @@ -697,7 +698,7 @@ 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 + api_version = PREVIEW_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('/'), diff --git a/src/containerapp/azext_containerapp/_models.py b/src/containerapp/azext_containerapp/_models.py index d4b26d94b32..6a474f89267 100644 --- a/src/containerapp/azext_containerapp/_models.py +++ b/src/containerapp/azext_containerapp/_models.py @@ -211,7 +211,8 @@ GitHubActionConfiguration = { "registryInfo": None, # [RegistryInfo] "azureCredentials": None, # [AzureCredentials] - "dockerfilePath": None, # str + "image": None, # str + "contextPath": None, # str "publishType": None, # str "os": None, # str "runtimeStack": None, # str diff --git a/src/containerapp/azext_containerapp/_params.py b/src/containerapp/azext_containerapp/_params.py index 4c958cd0077..51d2d6c5739 100644 --- a/src/containerapp/azext_containerapp/_params.py +++ b/src/containerapp/azext_containerapp/_params.py @@ -131,10 +131,11 @@ def load_arguments(self, _): c.argument('registry_url', help='The container registry server, e.g. myregistry.azurecr.io') c.argument('registry_username', help='The username of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied') c.argument('registry_password', help='The password of the registry. If using Azure Container Registry, we will try to infer the credentials if not supplied') - c.argument('docker_file_path', help='The dockerfile location, e.g. ./Dockerfile') + c.argument('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', 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 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') diff --git a/src/containerapp/azext_containerapp/custom.py b/src/containerapp/azext_containerapp/custom.py index 06ce16ad26d..f990c5f6ff9 100644 --- a/src/containerapp/azext_containerapp/custom.py +++ b/src/containerapp/azext_containerapp/custom.py @@ -1031,7 +1031,8 @@ def create_or_update_github_action(cmd, branch=None, token=None, login_with_github=False, - docker_file_path=None, + image=None, + context_path=None, service_principal_client_id=None, service_principal_client_secret=None, service_principal_tenant_id=None): @@ -1131,7 +1132,8 @@ def create_or_update_github_action(cmd, github_action_configuration = GitHubActionConfiguration github_action_configuration["registryInfo"] = registry_info github_action_configuration["azureCredentials"] = azure_credentials - github_action_configuration["dockerfilePath"] = docker_file_path + github_action_configuration["contextPath"] = context_path + github_action_configuration["image"] = image source_control_info["properties"]["githubActionConfiguration"] = github_action_configuration diff --git a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py new file mode 100644 index 00000000000..3f57809fbfa --- /dev/null +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_commands.py @@ -0,0 +1,149 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import time +import unittest + +from azure.cli.testsdk.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, JMESPathCheck, live_only) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + +@live_only() +class ContainerappIdentityTests(ScenarioTest): + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="eastus2") + def test_containerapp_identity_e2e(self, resource_group): + env_name = self.create_random_name(prefix='containerapp-env', length=24) + ca_name = self.create_random_name(prefix='containerapp', length=24) + user_identity_name = self.create_random_name(prefix='containerapp', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + self.cmd('containerapp create -g {} -n {} --environment {}'.format(resource_group, ca_name, env_name)) + + self.cmd('containerapp identity assign --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('identity create -g {} -n {}'.format(resource_group, user_identity_name)) + + self.cmd('containerapp identity assign --user-assigned {} -g {} -n {}'.format(user_identity_name, resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned, UserAssigned'), + ]) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned, UserAssigned'), + ]) + + self.cmd('containerapp identity remove --user-assigned {} -g {} -n {}'.format(user_identity_name, resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity remove --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'None'), + ]) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'None'), + ]) + + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="eastus") + def test_containerapp_identity_system(self, resource_group): + env_name = self.create_random_name(prefix='containerapp-env', length=24) + ca_name = self.create_random_name(prefix='containerapp', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + self.cmd('containerapp create -g {} -n {} --environment {} --system-assigned'.format(resource_group, ca_name, env_name)) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity remove --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'None'), + ]) + + self.cmd('containerapp identity assign --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity remove --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'None'), + ]) + + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="eastus2") + def test_containerapp_identity_user(self, resource_group): + env_name = self.create_random_name(prefix='containerapp-env', length=24) + ca_name = self.create_random_name(prefix='containerapp', length=24) + user_identity_name1 = self.create_random_name(prefix='containerapp-user1', length=24) + user_identity_name2 = self.create_random_name(prefix='containerapp-user2', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + self.cmd('containerapp create -g {} -n {} --environment {}'.format(resource_group, ca_name, env_name)) + + self.cmd('identity create -g {} -n {}'.format(resource_group, user_identity_name1)) + + self.cmd('identity create -g {} -n {}'.format(resource_group, user_identity_name2)) + + self.cmd('containerapp identity assign --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity assign --user-assigned {} {} -g {} -n {}'.format(user_identity_name1, user_identity_name2, resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned, UserAssigned'), + ]) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned, UserAssigned'), + ]) + + self.cmd('containerapp identity remove --user-assigned {} -g {} -n {}'.format(user_identity_name1, resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned, UserAssigned'), + ]) + + self.cmd('containerapp identity remove --user-assigned {} -g {} -n {}'.format(user_identity_name2, resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'SystemAssigned'), + ]) + + self.cmd('containerapp identity remove --system-assigned -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'None'), + ]) + + self.cmd('containerapp identity show -g {} -n {}'.format(resource_group, ca_name), checks=[ + JMESPathCheck('type', 'None'), + ]) 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..bc6d1133a1f 100644 --- a/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py +++ b/src/containerapp/azext_containerapp/tests/latest/test_containerapp_scenario.py @@ -8,35 +8,176 @@ import unittest 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) +from knack.util import CLIError TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) -@unittest.skip("Managed environment flaky") +@live_only() class ContainerappScenarioTest(ScenarioTest): @AllowLargeResponse(8192) - @ResourceGroupPreparer(location="centraluseuap") + @ResourceGroupPreparer(location="eastus2") 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), - ]) + # Ensure environment is completed + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) + + # Create basic Container App with default image 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) + JMESPathCheck('name', containerapp_name), + ]) + + self.cmd('containerapp list -g {}'.format(resource_group), checks=[ + JMESPathCheck('length(@)', 1), + JMESPathCheck('[0].name', containerapp_name), + ]) + + # Create Container App with image, resource and replica limits + create_string = "containerapp create -g {} -n {} --environment {} --image nginx --cpu 0.5 --memory 1.0Gi --min-replicas 2 --max-replicas 4".format(resource_group, containerapp_name, env_name) + self.cmd(create_string, checks=[ + JMESPathCheck('name', containerapp_name), + JMESPathCheck('properties.template.containers[0].image', 'nginx'), + JMESPathCheck('properties.template.containers[0].resources.cpu', '0.5'), + JMESPathCheck('properties.template.containers[0].resources.memory', '1Gi'), + JMESPathCheck('properties.template.scale.minReplicas', '2'), + JMESPathCheck('properties.template.scale.maxReplicas', '4') + ]) + + self.cmd('containerapp create -g {} -n {} --environment {} --ingress external --target-port 8080'.format(resource_group, containerapp_name, env_name), checks=[ + JMESPathCheck('properties.configuration.ingress.external', True), + JMESPathCheck('properties.configuration.ingress.targetPort', 8080) + ]) + + # Container App with ingress should fail unless target port is specified + with self.assertRaises(CLIError): + self.cmd('containerapp create -g {} -n {} --environment {} --ingress external'.format(resource_group, containerapp_name, env_name)) + + # Create Container App with secrets and environment variables + containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) + create_string = 'containerapp create -g {} -n {} --environment {} --secrets mysecret=secretvalue1 anothersecret="secret value 2" --env-vars GREETING="Hello, world" SECRETENV=secretref:anothersecret'.format( + resource_group, containerapp_name, env_name) + self.cmd(create_string, checks=[ + JMESPathCheck('name', containerapp_name), + JMESPathCheck('length(properties.template.containers[0].env)', 2), + JMESPathCheck('length(properties.configuration.secrets)', 2) + ]) + + + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="eastus2") + def test_container_acr(self, resource_group): + env_name = self.create_random_name(prefix='containerapp-e2e-env', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + # Ensure environment is completed + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) + registry_name = self.create_random_name(prefix='containerapp', length=24) + + # Create ACR + acr = self.cmd('acr create -g {} -n {} --sku Basic --admin-enabled'.format(resource_group, registry_name)).get_output_in_json() + registry_server = acr["loginServer"] + + acr_credentials = self.cmd('acr credential show -g {} -n {}'.format(resource_group, registry_name)).get_output_in_json() + registry_username = acr_credentials["username"] + registry_password = acr_credentials["passwords"][0]["value"] + + # Create Container App with ACR + containerapp_name = self.create_random_name(prefix='containerapp-e2e', length=24) + create_string = 'containerapp create -g {} -n {} --environment {} --registry-username {} --registry-server {} --registry-password {}'.format( + resource_group, containerapp_name, env_name, registry_username, registry_server, registry_password) + self.cmd(create_string, checks=[ + JMESPathCheck('name', containerapp_name), + JMESPathCheck('properties.configuration.registries[0].server', registry_server), + JMESPathCheck('properties.configuration.registries[0].username', registry_username), + JMESPathCheck('length(properties.configuration.secrets)', 1), + ]) + + + @AllowLargeResponse(8192) + @ResourceGroupPreparer(location="eastus") + def test_containerapp_update(self, resource_group): + env_name = self.create_random_name(prefix='containerapp-e2e-env', length=24) + + self.cmd('containerapp env create -g {} -n {}'.format(resource_group, env_name)) + + # Ensure environment is completed + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + while containerapp_env["properties"]["provisioningState"].lower() == "waiting": + time.sleep(5) + containerapp_env = self.cmd('containerapp env show -g {} -n {}'.format(resource_group, env_name)).get_output_in_json() + + # Create basic Container App with default image + containerapp_name = self.create_random_name(prefix='containerapp-update', length=24) + self.cmd('containerapp create -g {} -n {} --environment {}'.format(resource_group, containerapp_name, env_name), checks=[ + JMESPathCheck('name', containerapp_name), + JMESPathCheck('length(properties.template.containers)', 1), + JMESPathCheck('properties.template.containers[0].name', containerapp_name) + ]) + + # Update existing Container App that has a single container + + update_string = 'containerapp update -g {} -n {} --image {} --cpu 0.5 --memory 1.0Gi --args mycommand mycommand2 --command "mycommand" --revision-suffix suffix --min-replicas 2 --max-replicas 4'.format( + resource_group, containerapp_name, 'nginx') + self.cmd(update_string, checks=[ + JMESPathCheck('name', containerapp_name), + JMESPathCheck('length(properties.template.containers)', 1), + JMESPathCheck('properties.template.containers[0].name', containerapp_name), + JMESPathCheck('properties.template.containers[0].image', 'nginx'), + JMESPathCheck('properties.template.containers[0].resources.cpu', '0.5'), + JMESPathCheck('properties.template.containers[0].resources.memory', '1Gi'), + JMESPathCheck('properties.template.scale.minReplicas', '2'), + JMESPathCheck('properties.template.scale.maxReplicas', '4'), + JMESPathCheck('properties.template.containers[0].command[0]', "mycommand"), + JMESPathCheck('length(properties.template.containers[0].args)', 2) + ]) + + # Add new container to existing Container App + update_string = 'containerapp update -g {} -n {} --container-name {} --image {}'.format( + resource_group, containerapp_name, "newcontainer", "nginx") + self.cmd(update_string, checks=[ + JMESPathCheck('name', containerapp_name), + JMESPathCheck('length(properties.template.containers)', 2) + ]) + + # Updating container properties in a Container App with multiple containers, without providing container name should error + update_string = 'containerapp update -g {} -n {} --cpu {} --memory {}'.format( + resource_group, containerapp_name, '1.0', '2.0Gi') + with self.assertRaises(CLIError): + self.cmd(update_string) + + # Updating container properties in a Container App with multiple containers, should work when container name provided + update_string = 'containerapp update -g {} -n {} --container-name {} --cpu {} --memory {}'.format( + resource_group, containerapp_name, 'newcontainer', '0.75', '1.5Gi') + self.cmd(update_string) + + update_string = 'containerapp update -g {} -n {} --container-name {} --cpu {} --memory {}'.format( + resource_group, containerapp_name, containerapp_name, '0.75', '1.5Gi') + self.cmd(update_string, checks=[ + JMESPathCheck('properties.template.containers[0].resources.cpu', '0.75'), + JMESPathCheck('properties.template.containers[0].resources.memory', '1.5Gi'), + JMESPathCheck('properties.template.containers[1].resources.cpu', '0.75'), + JMESPathCheck('properties.template.containers[1].resources.memory', '1.5Gi'), ]) diff --git a/src/containerapp/setup.py b/src/containerapp/setup.py index 490525870da..15717458301 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.3.0' +VERSION = '0.3.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers