Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 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
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
711bbd4
Added FileShare commands. (#84)
runefa May 4, 2022
65cb30c
Fixed bug. (#86)
runefa May 4, 2022
13f217a
register log analytics resource provider if not registered when creat…
StrawnSC May 4, 2022
e86e089
fix style
StrawnSC May 4, 2022
8960d75
Fixed linter issue.
May 4, 2022
a8737a0
fix linter issues
StrawnSC May 5, 2022
1165dc9
update history file
StrawnSC May 5, 2022
a4590f4
Moved constant to constants.py.
May 5, 2022
1b8c7e0
add timeout to container app ping for ssh/logstream
StrawnSC May 5, 2022
40ca5b9
Various tests (Ingress, Traffic, Dapr, Env) (#87)
runefa May 5, 2022
4e7c369
Revert "Various tests (Ingress, Traffic, Dapr, Env) (#87)" (#88)
runefa May 5, 2022
0b15002
Reverted fileshare. (#90)
runefa May 5, 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
4 changes: 4 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Release History
===============

0.3.3
++++++
* Improved 'az containerapp up' handling of environment locations
* Added 'az containerapp env storage' to manage Container App environment file shares

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
4 changes: 4 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@

SHORT_POLLING_INTERVAL_SECS = 3
LONG_POLLING_INTERVAL_SECS = 10

LOG_ANALYTICS_RP = "Microsoft.OperationalInsights"

MAX_ENV_PER_LOCATION = 2
11 changes: 7 additions & 4 deletions src/containerapp/azext_containerapp/_ssh_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ 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._url)
logger.info("Attempting to connect to %s", self._url)
self._socket.connect(self._url, header=[f"Authorization: Bearer {self._token}"])

self.is_connected = True
Expand Down Expand Up @@ -160,9 +160,12 @@ def _getch_windows():
def ping_container_app(app):
site = safe_get(app, "properties", "configuration", "ingress", "fqdn")
if site:
resp = requests.get(f'https://{site}')
if not resp.ok:
logger.info(f"Got bad status pinging app: {resp.status_code}")
try:
resp = requests.get(f'https://{site}', timeout=30)
if not resp.ok:
logger.info(f"Got bad status pinging app: {resp.status_code}")
except requests.exceptions.ReadTimeout:
logger.info("Timed out while pinging app external URL")
else:
logger.info("Could not fetch site external URL")

Expand Down
95 changes: 89 additions & 6 deletions src/containerapp/azext_containerapp/_up_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@
create_service_principal_for_rbac,
repo_url_to_name,
get_container_app_if_exists,
trigger_workflow
trigger_workflow,
_ensure_location_allowed,
_is_resource_provider_registered,
_register_resource_provider
)

from ._constants import MAXIMUM_SECRET_LENGTH
from ._constants import MAXIMUM_SECRET_LENGTH, LOG_ANALYTICS_RP

from .custom import (
create_managed_environment,
Expand All @@ -62,6 +65,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"
Comment on lines +68 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we modify the location entered by users, do we need to inform the users through the log to make users aware?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is specifically for "up" & our test canary region ("northcentralusstage") since ACR resources up creates is not supported on this region, hence this change - so user doesn't need to create an ACR separately from the command & up will re-use an existing ACR in EastUS or create a new one East US which is a supported PROD region for ACR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks!

self.exists = exists

self.check_exists()
Expand Down Expand Up @@ -151,7 +156,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 +169,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 +193,9 @@ def create_if_needed(self, app_name):
) # TODO use .info()

def create(self):
self.location = validate_environment_location(self.cmd, self.location)
if not _is_resource_provider_registered(self.cmd, LOG_ANALYTICS_RP):
_register_resource_provider(self.cmd, LOG_ANALYTICS_RP)
env = create_managed_environment(
self.cmd,
self.name,
Expand Down Expand Up @@ -290,8 +298,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 +446,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 +799,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):
from ._constants import MAX_ENV_PER_LOCATION
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))
69 changes: 53 additions & 16 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

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
from ._constants import MAXIMUM_CONTAINER_APP_NAME_LENGTH, SHORT_POLLING_INTERVAL_SECS, LONG_POLLING_INTERVAL_SECS
from ._constants import (MAXIMUM_CONTAINER_APP_NAME_LENGTH, SHORT_POLLING_INTERVAL_SECS, LONG_POLLING_INTERVAL_SECS,
LOG_ANALYTICS_RP)

logger = get_logger(__name__)

Expand Down Expand Up @@ -177,8 +178,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 Expand Up @@ -254,22 +256,56 @@ def _get_location_from_resource_group(cli_ctx, resource_group_name):
return group.location


def _validate_subscription_registered(cmd, resource_provider, subscription_id=None):
providers_client = None
def _register_resource_provider(cmd, resource_provider):
from azure.mgmt.resource.resources.models import ProviderRegistrationRequest, ProviderConsentDefinition

logger.warning(f"Registering resource provider {resource_provider} ...")
properties = ProviderRegistrationRequest(third_party_provider_consent=ProviderConsentDefinition(consent_to_authorization=True))

client = providers_client_factory(cmd.cli_ctx)
try:
client.register(resource_provider, properties=properties)
# wait for registration to finish
timeout_secs = 120
registration = _is_resource_provider_registered(cmd, resource_provider)
start = datetime.utcnow()
while not registration:
registration = _is_resource_provider_registered(cmd, resource_provider)
time.sleep(SHORT_POLLING_INTERVAL_SECS)
if (datetime.utcnow() - start).seconds >= timeout_secs:
raise CLIInternalError(f"Timed out while waiting for the {resource_provider} resource provider to be registered.")

except Exception as e:
msg = ("This operation requires requires registering the resource provider {0}. "
"We were unable to perform that registration on your behalf: "
"Server responded with error message -- {1} . "
"Please check with your admin on permissions, "
"or try running registration manually with: az provider register --wait --namespace {0}")
raise ValidationError(resource_provider, msg.format(e.args)) from e


def _is_resource_provider_registered(cmd, resource_provider, subscription_id=None):
registered = None
if not subscription_id:
subscription_id = get_subscription_id(cmd.cli_ctx)

try:
providers_client = providers_client_factory(cmd.cli_ctx, subscription_id)
registration_state = getattr(providers_client.get(resource_provider), 'registration_state', "NotRegistered")

if not (registration_state and registration_state.lower() == 'registered'):
raise ValidationError('Subscription {} is not registered for the {} resource provider. Please run \"az provider register -n {} --wait\" to register your subscription.'.format(
subscription_id, resource_provider, resource_provider))
except ValidationError as ex:
raise ex
registered = (registration_state and registration_state.lower() == 'registered')
except Exception: # pylint: disable=broad-except
pass
return registered


def _validate_subscription_registered(cmd, resource_provider, subscription_id=None):
if not subscription_id:
subscription_id = get_subscription_id(cmd.cli_ctx)
registered = _is_resource_provider_registered(cmd, resource_provider, subscription_id)
if registered is False:
raise ValidationError(f'Subscription {subscription_id} is not registered for the {resource_provider} '
f'resource provider. Please run "az provider register -n {resource_provider} --wait" '
'to register your subscription.')


def _ensure_location_allowed(cmd, location, resource_provider, resource_type):
Expand Down Expand Up @@ -421,7 +457,7 @@ def _get_default_log_analytics_location(cmd):
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', [])
resource_types = getattr(providers_client.get(LOG_ANALYTICS_RP), 'resource_types', [])
res_locations = []
for res in resource_types:
if res and getattr(res, 'resource_type', "") == "workspaces":
Expand Down Expand Up @@ -514,14 +550,14 @@ def _get_log_analytics_workspace_name(cmd, logs_customer_id, 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.")
_validate_subscription_registered(cmd, LOG_ANALYTICS_RP)
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")
_ensure_location_allowed(cmd, log_analytics_location, LOG_ANALYTICS_RP, "workspaces")
except Exception: # pylint: disable=broad-except
log_analytics_location = _get_default_log_analytics_location(cmd)

Expand Down Expand Up @@ -819,8 +855,9 @@ def _update_traffic_weights(containerapp_def, list_weights):

if not is_existing:
containerapp_def["properties"]["configuration"]["ingress"]["traffic"].append({
"revisionName": key_val[0],
"weight": int(key_val[1])
"revisionName": key_val[0] if key_val[0].lower() != "latest" else None,
"weight": int(key_val[1]),
"latestRevision": key_val[0].lower() == "latest"
})


Expand Down
8 changes: 5 additions & 3 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1964,7 +1964,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")

logger.warning("connecting to : %s", url)
logger.info("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)
Expand Down 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
Loading