Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Up Tests #81

Open
wants to merge 68 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
1de51ee
Marchp1s and add back Identity (#57)
runefa Apr 7, 2022
7735595
Fix help for linter
calvinsID Apr 9, 2022
2e1be89
various fixes, helptext (#59)
calvinsID Apr 11, 2022
5fcd785
Fixes (#60)
calvinsID Apr 11, 2022
a1929c8
Updated managed identity + help. (#61)
runefa Apr 11, 2022
df5cc99
Added user-assigned and system-assigned to containerapp create. (#62)
runefa Apr 11, 2022
0dedf19
Bump version to 0.1.1 (#63)
calvinsID Apr 11, 2022
1474d69
Added more specific MSI help text. (#64)
runefa Apr 11, 2022
5afc2b1
Bump to 0.3.0 (#65)
calvinsID Apr 11, 2022
77dcaa0
Merge branch 'main' into calcha-main
calvinsID Apr 12, 2022
5cc0aa8
Container App Test suite (#67)
calvinsID Apr 15, 2022
1f1b31a
use new GH actions API
StrawnSC Apr 17, 2022
7f70e2f
remove live only recordings
StrawnSC Apr 17, 2022
fa539a8
update CODEOWNERS
StrawnSC Apr 17, 2022
f935df8
fix API version naming
StrawnSC Apr 18, 2022
7da2e9a
Merge branch 'main' into main
StrawnSC Apr 18, 2022
fcc3d87
Managed Identity Tests (#69)
runefa Apr 18, 2022
cb6820e
resolve review comments
StrawnSC Apr 18, 2022
f6efbd2
Managed Identity Fixes (#71)
runefa Apr 18, 2022
4e805bf
Update src/containerapp/azext_containerapp/_params.py
panchagnula Apr 19, 2022
7f7f1fe
4/26 release: Up with --repo/--browse, exec (ssh) command, replica co…
StrawnSC Apr 22, 2022
3d794b3
Merge branch 'main' into main
runefa Apr 22, 2022
e87accc
Fixed small issue with test.
Apr 22, 2022
552850f
Removed flake exclusions and removed type=str from params.
Apr 25, 2022
b86d217
Fixed repo bug when searching for dockerfile, increased timeout on gi…
Apr 25, 2022
31002f8
Added env var changes.
Apr 25, 2022
c292ac9
Assume port if ingress is provided with image and port is not.
Apr 25, 2022
b9fce25
Fixed small helloworld error.
Apr 25, 2022
077bf20
Fixed logger typo.
Apr 25, 2022
c100f1f
Search for acr before creating one.
Apr 26, 2022
44faaf7
Fixed bug where only --environment is passed. Changed hash on acr nam…
Apr 26, 2022
197913a
error out if dockerfile not found (--repo)
StrawnSC Apr 26, 2022
595cdf5
Fixed bug with --image. Changed logger warning output. Disabled warni…
Apr 26, 2022
272146b
Disabled no_wait. Added better error handling for up API calls. Updat…
Apr 26, 2022
988d901
fix ACR length cap; enforce name/secret limits; trigger GH action if …
StrawnSC Apr 26, 2022
e935bf2
Merge branch 'main' into findacr2
StrawnSC Apr 26, 2022
50617e1
Merge pull request #77 from haroonf/findacr2
StrawnSC Apr 26, 2022
757dcf4
force exact match for ACR retrieval (prevents secrets issues for --repo)
StrawnSC Apr 27, 2022
cbeac7a
fix hashing and add GH validations
StrawnSC Apr 27, 2022
44aa7ff
don't retrieve a registry if one provided; take RG from env if possible
StrawnSC Apr 27, 2022
a016f35
Fixed --registry-server with --image bug. (#78)
runefa Apr 27, 2022
c59d257
use SP creds if provided
StrawnSC Apr 27, 2022
bd2cc8b
Merge branch 'main' of github.com:calvinsid/azure-cli-extensions into…
StrawnSC Apr 27, 2022
2b58d27
fix github actions (less polling)
StrawnSC Apr 27, 2022
668b7d9
Added prototype for env check.
Apr 27, 2022
25843f1
Honor location and environment passed to create new containerapp (eve…
runefa Apr 27, 2022
612d1a5
print created SP name/id; prevent using ACR names longer than 20 char…
StrawnSC Apr 27, 2022
900e7b6
fix style; add license header
StrawnSC Apr 27, 2022
6710636
Finished core logic.
Apr 28, 2022
50e0754
add max core cli version 2.36.0
StrawnSC Apr 28, 2022
d53550b
make ACR name more unique (must be globally unique)
StrawnSC Apr 28, 2022
66878c7
Finished logic.
Apr 28, 2022
37627de
sort workflows by date before selecting one
StrawnSC Apr 28, 2022
5f5308e
log workflow
StrawnSC Apr 29, 2022
7b0bd7d
Added error message with eligible locations if users pass uneligible …
Apr 29, 2022
c534455
Added function to check if env already exists so we don't try to upda…
Apr 29, 2022
3e576de
Added error handling for location northcentralusstage. Added list of …
Apr 29, 2022
724249a
merge main
StrawnSC Apr 29, 2022
a1ce4dd
Small fixes, implemented check_env_name_on_rg.
Apr 29, 2022
4fc7267
Merge pull request #80 from haroonf/managedenvcheck
StrawnSC May 2, 2022
a53bffa
add source tests (WIP); location bug fix
StrawnSC May 2, 2022
a8a6ac1
location bug fix
StrawnSC May 2, 2022
5570ad9
fix style
StrawnSC May 2, 2022
2ccbce0
bump version number
StrawnSC May 2, 2022
34aabca
Updates to tests (#82)
calvinsID May 3, 2022
6b3468a
Remove recordings (#83)
calvinsID May 3, 2022
53ddc45
prevent using --only-show-errors, --output, -o in up
StrawnSC May 3, 2022
6d6d2d8
Merge branch 'calmain' into up-tests
StrawnSC May 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Release History
===============

0.3.3
++++++
* Improved 'az containerapp up' handling of environment locations

0.3.2
++++++
Expand Down
3 changes: 1 addition & 2 deletions src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

logger = get_logger(__name__)

API_VERSION = "2021-03-01"
PREVIEW_API_VERSION = "2022-01-01-preview"
STABLE_API_VERSION = "2022-03-01"
POLLING_TIMEOUT = 60 # how many seconds before exiting
Expand Down Expand Up @@ -74,7 +73,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 = 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(
Expand Down
89 changes: 84 additions & 5 deletions src/containerapp/azext_containerapp/_up_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
create_service_principal_for_rbac,
repo_url_to_name,
get_container_app_if_exists,
trigger_workflow
trigger_workflow,
_ensure_location_allowed
)

from ._constants import MAXIMUM_SECRET_LENGTH
Expand All @@ -62,6 +63,8 @@ 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)
if self.location.lower() == "northcentralusstage":
self.location = "eastus"
self.exists = exists

self.check_exists()
Expand Down Expand Up @@ -151,7 +154,7 @@ def __init__(
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.location = location
self.logs_key = logs_key
self.logs_customer_id = logs_customer_id

Expand All @@ -164,7 +167,7 @@ def set_name(self, name_or_rid):
self.resource_group = ResourceGroup(
self.cmd,
rg,
_get_default_containerapps_location(self.cmd, self.location),
self.location,
)
else:
self.name = name_or_rid
Expand All @@ -188,6 +191,7 @@ def create_if_needed(self, app_name):
) # TODO use .info()

def create(self):
self.location = validate_environment_location(self.cmd, self.location)
env = create_managed_environment(
self.cmd,
self.name,
Expand Down Expand Up @@ -290,8 +294,11 @@ def create_acr(self):
registry_rg = self.resource_group
url = self.registry_server
registry_name = url[: url.rindex(".azurecr.io")]
location = "eastus"
if self.env.location and self.env.location.lower() != "northcentralusstage":
location = self.env.location
registry_def = create_new_acr(
self.cmd, registry_name, registry_rg.name, self.env.location
self.cmd, registry_name, registry_rg.name, location
)
self.registry_server = registry_def.login_server

Expand Down Expand Up @@ -435,7 +442,13 @@ def _get_ingress_and_target_port(ingress, target_port, dockerfile_content: "list
return ingress, target_port


def _validate_up_args(source, image, repo, registry_server):
def _validate_up_args(cmd, source, image, repo, registry_server):
disallowed_params = ["--only-show-errors", "--output", "-o"]
command_args = cmd.cli_ctx.data.get("safe_params", [])
for a in disallowed_params:
if a in command_args:
raise ValidationError(f"Argument {a} is not allowed for 'az containerapp up'")

if not source and not image and not repo:
raise RequiredArgumentMissingError(
"You must specify either --source, --repo, or --image"
Expand Down Expand Up @@ -782,3 +795,69 @@ def find_existing_acr(cmd, app: "ContainerApp"):
app.should_create_acr = False
return acr.name, parse_resource_id(acr.id)["resource_group"]
return None, None


def validate_environment_location(cmd, location):
MAX_ENV_PER_LOCATION = 2
env_list = list_managed_environments(cmd)

locations = [l["location"] for l in env_list]
locations = list(set(locations)) # remove duplicates

location_count = {}
for loc in locations:
location_count[loc] = len([e for e in env_list if e["location"] == loc])

disallowed_locations = []
for _, value in enumerate(location_count):
if location_count[value] > MAX_ENV_PER_LOCATION - 1:
disallowed_locations.append(value)

res_locations = list_environment_locations(cmd)
res_locations = [l for l in res_locations if l not in disallowed_locations]

allowed_locs = ", ".join(res_locations)

if location:
try:
_ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments")
except Exception: # pylint: disable=broad-except
raise ValidationError("You cannot create a Containerapp environment in location {}. List of eligible locations: {}.".format(location, allowed_locs))

if len(res_locations) > 0:
if not location:
logger.warning("Creating environment on location {}.".format(res_locations[0]))
return res_locations[0]
if location in disallowed_locations:
raise ValidationError("You have more than {} environments in location {}. List of eligible locations: {}.".format(MAX_ENV_PER_LOCATION, location, allowed_locs))
return location
else:
raise ValidationError("You cannot create any more environments. Environments are limited to {} per location in a subscription. Please specify an existing environment using --environment.".format(MAX_ENV_PER_LOCATION))


def list_environment_locations(cmd):
from ._utils import providers_client_factory
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', "") == "managedEnvironments":
res_locations = getattr(res, 'locations', [])

res_locations = [res_loc.lower().replace(" ", "").replace("(", "").replace(")", "") for res_loc in res_locations if res_loc.strip()]

return res_locations


def check_env_name_on_rg(cmd, managed_env, resource_group_name, location):
if location:
_ensure_location_allowed(cmd, location, "Microsoft.App", "managedEnvironments")
if managed_env and resource_group_name and location:
env_def = None
try:
env_def = ManagedEnvironmentClient.show(cmd, resource_group_name, parse_resource_id(managed_env)["name"])
except:
pass
if env_def:
if location != env_def["location"]:
raise ValidationError("Environment {} already exists in resource group {} on location {}, cannot change location of existing environment to {}.".format(parse_resource_id(managed_env)["name"], resource_group_name, env_def["location"], location))
5 changes: 3 additions & 2 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ def get_workflow(github_repo, name): # pylint: disable=inconsistent-return-stat


def trigger_workflow(token, repo, name, branch):
logger.warning("Triggering Github Action")
get_workflow(get_github_repo(token, repo), name).create_dispatch(branch)
wf = get_workflow(get_github_repo(token, repo), name)
logger.warning(f"Triggering Github Action: {wf.path}")
wf.create_dispatch(branch)


def await_github_action(cmd, token, repo, branch, name, resource_group_name, timeout_secs=1200):
Expand Down
6 changes: 4 additions & 2 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2015,12 +2015,14 @@ 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, AzureContainerRegistry)
_get_registry_details, _create_github_action, _set_up_defaults, up_output, AzureContainerRegistry,
check_env_name_on_rg)
HELLOWORLD = "mcr.microsoft.com/azuredocs/containerapps-helloworld"
dockerfile = "Dockerfile" # for now the dockerfile name must be "Dockerfile" (until GH actions API is updated)

_validate_up_args(source, image, repo, registry_server)
_validate_up_args(cmd, source, image, repo, registry_server)
validate_container_app_name(name)
check_env_name_on_rg(cmd, managed_env, resource_group_name, location)

image = _reformat_image(source, repo, image)
token = None if not repo else get_github_access_token(cmd, ["admin:repo_hook", "repo", "workflow"], token)
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import platform
from unittest import mock
import time
import unittest
from azext_containerapp.custom import containerapp_ssh

from azure.cli.testsdk.reverse_dependency import get_dummy_cli
Expand All @@ -18,7 +19,7 @@
TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..'))


@live_only()
@live_only() # Containerapp tests can only be run live due to log analytics name being randomly generated every time
class ContainerappScenarioTest(ScenarioTest):
@AllowLargeResponse(8192)
@ResourceGroupPreparer(location="eastus2")
Expand Down Expand Up @@ -218,6 +219,30 @@ def test_container_acr(self, resource_group):
JMESPathCheck('length(properties.configuration.secrets)', 1),
])

# Update Container App with ACR
update_string = 'containerapp update -g {} -n {} --min-replicas 0 --max-replicas 1 --set-env-vars testenv=testing'.format(
resource_group, containerapp_name)
self.cmd(update_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),
JMESPathCheck('properties.template.scale.minReplicas', '0'),
JMESPathCheck('properties.template.scale.maxReplicas', '1'),
JMESPathCheck('length(properties.template.containers[0].env)', 1),
])

# Add secrets to Container App with ACR
containerapp_secret = self.cmd('containerapp secret list -g {} -n {}'.format(resource_group, containerapp_name)).get_output_in_json()
secret_name = containerapp_secret[0]["name"]
secret_string = 'containerapp secret set -g {} -n {} --secrets newsecret=test'.format(resource_group, containerapp_name)
self.cmd(secret_string, checks=[
JMESPathCheck('length(@)', 2),
])

with self.assertRaises(CLIError):
# Removing ACR password should fail since it is needed for ACR
self.cmd('containerapp secret remove -g {} -n {} --secret-names {}'.format(resource_group, containerapp_name, secret_name))

@AllowLargeResponse(8192)
@ResourceGroupPreparer(location="eastus")
Expand Down Expand Up @@ -286,6 +311,7 @@ def test_containerapp_update(self, resource_group):
JMESPathCheck('properties.template.containers[1].resources.memory', '1.5Gi'),
])

@unittest.skip("API only on stage currently")
@live_only() # VCR.py can't seem to handle websockets (only --live works)
# @ResourceGroupPreparer(location="centraluseuap")
@mock.patch("azext_containerapp._ssh_utils._resize_terminal")
Expand Down Expand Up @@ -358,4 +384,4 @@ def test_containerapp_logstream(self, resource_group):
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}')
self.cmd(f'containerapp logs show -n {containerapp_name} -g {resource_group}')
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,121 @@ def test_containerapp_up_image_e2e(self, resource_group):
url = url if url.startswith("http") else f"http://{url}"
resp = requests.get(url)
self.assertTrue(resp.ok)

@ResourceGroupPreparer(location="eastus2")
def test_containerapp_up_image_no_env(self, resource_group):
image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
app_name = self.create_random_name(prefix='containerapp', length=24)
self.cmd(f"containerapp up --image {image} -g {resource_group} -n {app_name}")

app = self.cmd(f"containerapp show -g {resource_group} -n {app_name}").get_output_in_json()
url = app["properties"]["configuration"]["ingress"]["fqdn"]
url = url if url.startswith("http") else f"http://{url}"
resp = requests.get(url)
self.assertTrue(resp.ok)


@live_only()
class ContainerAppUpSourceTest(ScenarioTest):
# env and RG already created
@ResourceGroupPreparer(location="eastus2")
def test_containerapp_up_source_e2e(self, resource_group):
zip_file_name = os.path.join(TEST_DIR, 'demo-flask-containerapp.zip')

# create a temp directory and unzip the code to this folder
import zipfile
import tempfile
temp_dir = tempfile.mkdtemp()
zip_ref = zipfile.ZipFile(zip_file_name, 'r')
zip_ref.extractall(temp_dir)
current_working_dir = os.getcwd()

# change the working dir to the dir where the code has been extracted to
os.chdir(temp_dir)

env_name = self.create_random_name(prefix='env', length=24)
self.cmd(f'containerapp env create -g {resource_group} -n {env_name}')
app_name = self.create_random_name(prefix='containerapp', length=24)
self.cmd(f"containerapp up --source . --environment {env_name} -g {resource_group} -n {app_name}")

app = self.cmd(f"containerapp show -g {resource_group} -n {app_name}").get_output_in_json()
url = app["properties"]["configuration"]["ingress"]["fqdn"]
url = url if url.startswith("http") else f"http://{url}"
resp = requests.get(url)
self.assertTrue(resp.ok)

# cleanup
# switch back the working dir
os.chdir(current_working_dir)
# delete temp_dir
import shutil
shutil.rmtree(temp_dir)

# Only RG already created
@ResourceGroupPreparer(location="eastus2")
def test_containerapp_up_source_create_env(self, resource_group):
zip_file_name = os.path.join(TEST_DIR, 'demo-flask-containerapp.zip')

# create a temp directory and unzip the code to this folder
import zipfile
import tempfile
temp_dir = tempfile.mkdtemp()
zip_ref = zipfile.ZipFile(zip_file_name, 'r')
zip_ref.extractall(temp_dir)
current_working_dir = os.getcwd()

# change the working dir to the dir where the code has been extracted to
os.chdir(temp_dir)

env_name = self.create_random_name(prefix='env', length=24)
app_name = self.create_random_name(prefix='containerapp', length=24)
self.cmd(f"containerapp up --source . --environment {env_name} -g {resource_group} -n {app_name}")

app = self.cmd(f"containerapp show -g {resource_group} -n {app_name}").get_output_in_json()
url = app["properties"]["configuration"]["ingress"]["fqdn"]
url = url if url.startswith("http") else f"http://{url}"
resp = requests.get(url)
self.assertTrue(resp.ok)

# cleanup
# switch back the working dir
os.chdir(current_working_dir)
# delete temp_dir
import shutil
shutil.rmtree(temp_dir)


# RG and ACR already created
@ResourceGroupPreparer(location="eastus2")
def test_containerapp_up_source_acr(self, resource_group):
zip_file_name = os.path.join(TEST_DIR, 'demo-flask-containerapp.zip')

# create a temp directory and unzip the code to this folder
import zipfile
import tempfile
temp_dir = tempfile.mkdtemp()
zip_ref = zipfile.ZipFile(zip_file_name, 'r')
zip_ref.extractall(temp_dir)
current_working_dir = os.getcwd()

# change the working dir to the dir where the code has been extracted to
os.chdir(temp_dir)

acr_name = f"{self.create_random_name(prefix='ca', length=10)}acr"
self.cmd(f"acr create --sku basic -n {acr_name} -g {resource_group}")
env_name = self.create_random_name(prefix='env', length=24)
app_name = self.create_random_name(prefix='containerapp', length=24)
self.cmd(f"containerapp up --source . --environment {env_name} -g {resource_group} -n {app_name} --registry-server {acr_name}.azurecr.io")

app = self.cmd(f"containerapp show -g {resource_group} -n {app_name}").get_output_in_json()
url = app["properties"]["configuration"]["ingress"]["fqdn"]
url = url if url.startswith("http") else f"http://{url}"
resp = requests.get(url)
self.assertTrue(resp.ok)

# cleanup
# switch back the working dir
os.chdir(current_working_dir)
# delete temp_dir
import shutil
shutil.rmtree(temp_dir)
2 changes: 1 addition & 1 deletion src/containerapp/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# TODO: Confirm this is the right version number you want and it matches your
# HISTORY.rst entry.

VERSION = '0.3.2'
VERSION = '0.3.3'


# The full list of classifiers is available at
Expand Down