Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bfaaaf1
start containerapp version 0.3.8
StrawnSC Jun 21, 2022
a18092a
add msi registry support for 'az containerapp create'
StrawnSC Jun 21, 2022
2b8a53a
update history
StrawnSC Jun 21, 2022
4b4099e
fix CI
StrawnSC Jul 1, 2022
55aaae6
Revert "fix CI"
StrawnSC Jul 1, 2022
9f3bae1
bump version
StrawnSC Aug 1, 2022
8a4d5a7
Merge branch 'containerapp-0.3.9' into registry_identity_for_create
StrawnSC Aug 2, 2022
bbde76c
error out for system identities
StrawnSC Aug 3, 2022
9618daa
Increased polling time, gave better message when timeout occurs for e…
runefa Aug 9, 2022
d289220
Added show_secrets to show. (#142)
runefa Aug 9, 2022
ea3978c
az containerapp create: take registry server from --image if not prov…
StrawnSC Aug 10, 2022
02e3a07
Merge branch 'containerapp-0.3.9' into registry_identity_for_create
StrawnSC Aug 11, 2022
49e1fc7
Merge pull request #133 from StrawnSC/registry_identity_for_create
StrawnSC Aug 11, 2022
ed5d5ba
fix cert upload failure issue (#141)
lil131 Aug 11, 2022
7cb8226
fix style
StrawnSC Aug 12, 2022
28a8e0c
Update src/containerapp/HISTORY.rst
StrawnSC Aug 15, 2022
86740b6
resolve review comments
StrawnSC Aug 15, 2022
19323a4
Merge branch 'containerapp-0.3.9' of github.com:calvinsid/azure-cli-e…
StrawnSC Aug 15, 2022
a0164d2
Merge branch 'main' into containerapp-0.3.9
StrawnSC Aug 15, 2022
e0f4cef
rerecord failed tests
StrawnSC Aug 15, 2022
2edaf81
Update src/containerapp/HISTORY.rst
StrawnSC Aug 16, 2022
4b1c7aa
Update src/containerapp/HISTORY.rst
zhoxing-ms Aug 16, 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
8 changes: 8 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
Release History
===============

0.3.9
++++++
* 'az containerapp create': Allow authenticating with managed identity (MSI) instead of ACR username & password
* 'az containerapp show': Add parameter --show-secrets to show secret values
* 'az containerapp env create': Add better message when polling times out
* 'az containerapp env certificate upload': Fix bug where certificate uploading failed with error "Certificate must contain one private key."
* 'az containerapp env certificate upload': Fix bug where replacing invalid character in certificate name failed"

0.3.8
++++++
* 'az containerapp update': Fix bug where --yaml would error out due to secret values
Expand Down
2 changes: 1 addition & 1 deletion src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
logger = get_logger(__name__)

STABLE_API_VERSION = "2022-03-01"
POLLING_TIMEOUT = 60 # how many seconds before exiting
POLLING_TIMEOUT = 600 # how many seconds before exiting
POLLING_SECONDS = 2 # how many seconds between requests


Expand Down
4 changes: 3 additions & 1 deletion src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
LOG_ANALYTICS_RP = "Microsoft.OperationalInsights"
CONTAINER_APPS_RP = "Microsoft.App"

MAX_ENV_PER_LOCATION = 2
MAX_ENV_PER_LOCATION = 5

MICROSOFT_SECRET_SETTING_NAME = "microsoft-provider-authentication-secret"
FACEBOOK_SECRET_SETTING_NAME = "facebook-provider-authentication-secret"
Expand All @@ -29,3 +29,5 @@

NAME_INVALID = "Invalid"
NAME_ALREADY_EXISTS = "AlreadyExists"

HELLO_WORLD_IMAGE = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
4 changes: 4 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def load_arguments(self, _):
c.argument('registry_pass', 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', 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.")
c.argument('registry_identity', help="A Managed Identity to authenticate with the registry server instead of username/password. Use a resource ID or 'system' for user-defined and system-defined identities, respectively. The registry must be an ACR. If possible, an 'acrpull' role assignemnt will be created for the identity automatically.")

# Ingress
with self.argument_context('containerapp', arg_group='Ingress') as c:
Expand All @@ -111,6 +112,9 @@ def load_arguments(self, _):
with self.argument_context('containerapp create', arg_group='Container') as c:
c.argument('image', options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.")

with self.argument_context('containerapp show') as c:
c.argument('show_secrets', help="Show Containerapp secrets.", action='store_true')

with self.argument_context('containerapp update', arg_group='Container') as c:
c.argument('image', options_list=['--image', '-i'], help="Container image, e.g. publisher/image-name:tag.")

Expand Down
40 changes: 36 additions & 4 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from azure.cli.core.azclierror import (ValidationError, RequiredArgumentMissingError, CLIInternalError,
ResourceNotFoundError, FileOperationError, CLIError)
ResourceNotFoundError, FileOperationError, CLIError, InvalidArgumentValueError, UnauthorizedError)
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.command_modules.appservice.utils import _normalize_location
from azure.cli.command_modules.network._client_factory import network_client_factory
from azure.cli.command_modules.role.custom import create_role_assignment
from azure.cli.command_modules.acr.custom import acr_show
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.cli.core.profiles import ResourceType
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
Expand Down Expand Up @@ -1046,7 +1051,7 @@ def generate_randomized_cert_name(thumbprint, prefix, initial="rg"):
cert_name = "{}-{}-{}-{:04}".format(prefix[:14], initial[:14], thumbprint[:4].lower(), randint(0, 9999))
for c in cert_name:
if not (c.isalnum() or c == '-' or c == '.'):
cert_name.replace(c, '-')
cert_name = cert_name.replace(c, '-')
return cert_name.lower()


Expand Down Expand Up @@ -1307,8 +1312,7 @@ def load_cert_file(file_path, cert_password=None):
x509 = p12.get_certificate()
digest_algorithm = 'sha256'
thumbprint = x509.digest(digest_algorithm).decode("utf-8").replace(':', '')
pem_data = crypto.dump_certificate(crypto.FILETYPE_PEM, x509)
blob = b64encode(pem_data).decode("utf-8")
blob = b64encode(cert_data).decode("utf-8")
else:
raise FileOperationError('Not a valid file type. Only .PFX and .PEM files are supported.')
except Exception as e:
Expand Down Expand Up @@ -1431,3 +1435,31 @@ def set_managed_identity(cmd, resource_group_name, containerapp_def, system_assi

if not isExisting:
containerapp_def["identity"]["userAssignedIdentities"][r] = {}


def create_acrpull_role_assignment(cmd, registry_server, registry_identity=None, service_principal=None, skip_error=False):
if registry_identity:
registry_identity_parsed = parse_resource_id(registry_identity)
registry_identity_name, registry_identity_rg = registry_identity_parsed.get("name"), registry_identity_parsed.get("resource_group")
sp_id = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_MSI).user_assigned_identities.get(resource_name=registry_identity_name, resource_group_name=registry_identity_rg).principal_id
else:
sp_id = service_principal

client = get_mgmt_service_client(cmd.cli_ctx, ContainerRegistryManagementClient).registries
acr_id = acr_show(cmd, client, registry_server[: registry_server.rindex(ACR_IMAGE_SUFFIX)]).id
try:
create_role_assignment(cmd, role="acrpull", assignee=sp_id, scope=acr_id)
except Exception as e:
message = (f"Role assignment failed with error message: \"{' '.join(e.args)}\". \n"
f"To add the role assignment manually, please run 'az role assignment create --assignee {sp_id} --scope {acr_id} --role acrpull'. \n"
"You may have to restart the containerapp with 'az containerapp revision restart'.")
if skip_error:
logger.error(message)
else:
raise UnauthorizedError(message)


def is_registry_msi_system(identity):
if identity is None:
return False
return identity.lower() == "system"
20 changes: 17 additions & 3 deletions src/containerapp/azext_containerapp/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@
# --------------------------------------------------------------------------------------------
# pylint: disable=line-too-long

from azure.cli.core.azclierror import (ValidationError, ResourceNotFoundError)
from azure.cli.core.azclierror import (ValidationError, ResourceNotFoundError, InvalidArgumentValueError,
MutuallyExclusiveArgumentError)
from msrestazure.tools import is_valid_resource_id
from knack.log import get_logger

from ._clients import ContainerAppClient
from ._ssh_utils import ping_container_app
from ._utils import safe_get
from ._utils import safe_get, is_registry_msi_system
from ._constants import ACR_IMAGE_SUFFIX


logger = get_logger(__name__)


# called directly from custom method bc otherwise it disrupts the --environment auto RID functionality
def validate_create(registry_identity, registry_pass, registry_user, registry_server, no_wait):
if registry_identity and (registry_pass or registry_user):
raise MutuallyExclusiveArgumentError("Cannot provide both registry identity and username/password")
if is_registry_msi_system(registry_identity) and no_wait:
raise MutuallyExclusiveArgumentError("--no-wait is not supported with system registry identity")
if registry_identity and not is_valid_resource_id(registry_identity) and not is_registry_msi_system(registry_identity):
raise InvalidArgumentValueError("--registry-identity must be an identity resource ID or 'system'")
if registry_identity and ACR_IMAGE_SUFFIX not in (registry_server or ""):
raise InvalidArgumentValueError("--registry-identity: expected an ACR registry (*.azurecr.io) for --registry-server")


def _is_number(s):
try:
float(s)
Expand Down Expand Up @@ -49,7 +63,7 @@ def validate_cpu(namespace):

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
from msrestazure.tools import resource_id

if namespace.managed_env:
if not is_valid_resource_id(namespace.managed_env):
Expand Down
2 changes: 1 addition & 1 deletion src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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
from ._validators import validate_ssh, validate_create


def transform_containerapp_output(app):
Expand Down
70 changes: 53 additions & 17 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,13 @@
validate_container_app_name, _update_weights, get_vnet_location, register_provider_if_needed,
generate_randomized_cert_name, _get_name, load_cert_file, check_cert_name_availability,
validate_hostname, patch_new_custom_domain, get_custom_domains, _validate_revision_name, set_managed_identity,
clean_null_values, _populate_secret_values)


create_acrpull_role_assignment, is_registry_msi_system, clean_null_values, _populate_secret_values)
from ._validators import validate_create
from ._ssh_utils import (SSH_DEFAULT_ENCODING, WebSocketConnection, read_ssh, get_stdin_writer, SSH_CTRL_C_MSG,
SSH_BACKUP_ENCODING)
from ._constants import (MAXIMUM_SECRET_LENGTH, MICROSOFT_SECRET_SETTING_NAME, FACEBOOK_SECRET_SETTING_NAME, GITHUB_SECRET_SETTING_NAME,
GOOGLE_SECRET_SETTING_NAME, TWITTER_SECRET_SETTING_NAME, APPLE_SECRET_SETTING_NAME, CONTAINER_APPS_RP,
NAME_INVALID, NAME_ALREADY_EXISTS, ACR_IMAGE_SUFFIX)
NAME_INVALID, NAME_ALREADY_EXISTS, ACR_IMAGE_SUFFIX, HELLO_WORLD_IMAGE)

logger = get_logger(__name__)

Expand Down Expand Up @@ -328,9 +327,17 @@ def create_containerapp(cmd,
no_wait=False,
system_assigned=False,
disable_warnings=False,
user_assigned=None):
user_assigned=None,
registry_identity=None):
if image and "/" in image and not registry_server:
registry_server = image[:image.index("/")]
register_provider_if_needed(cmd, CONTAINER_APPS_RP)
validate_container_app_name(name)
validate_create(registry_identity, registry_pass, registry_user, registry_server, no_wait)

if registry_identity and not is_registry_msi_system(registry_identity):
logger.info("Creating an acrpull role assignment for the registry identity")
create_acrpull_role_assignment(cmd, registry_server, registry_identity, skip_error=True)

if yaml:
if image or managed_env or min_replicas or max_replicas or target_port or ingress or\
Expand All @@ -341,7 +348,7 @@ def create_containerapp(cmd,
return create_containerapp_yaml(cmd=cmd, name=name, resource_group_name=resource_group_name, file_name=yaml, no_wait=no_wait)

if not image:
image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
image = HELLO_WORLD_IMAGE

if managed_env is None:
raise RequiredArgumentMissingError('Usage error: --environment is required if not using --yaml')
Expand Down Expand Up @@ -382,19 +389,22 @@ def create_containerapp(cmd,
secrets_def = parse_secret_flags(secrets)

registries_def = None
if registry_server is not None:
if registry_server is not None and not is_registry_msi_system(registry_identity):
registries_def = RegistryCredentialsModel
registries_def["server"] = registry_server

# 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 registry_identity is None:
registry_user, registry_pass = _infer_acr_credentials(cmd, registry_server, disable_warnings)

registries_def["server"] = registry_server
registries_def["username"] = registry_user
if not registry_identity:
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, disable_warnings=disable_warnings)
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, disable_warnings=disable_warnings)
else:
registries_def["identity"] = registry_identity

dapr_def = None
if dapr_enabled:
Expand Down Expand Up @@ -450,7 +460,7 @@ def create_containerapp(cmd,

container_def = ContainerModel
container_def["name"] = container_name if container_name else name
container_def["image"] = image
container_def["image"] = image if not is_registry_msi_system(registry_identity) else HELLO_WORLD_IMAGE
if env_vars is not None:
container_def["env"] = parse_env_var_flags(env_vars)
if startup_command is not None:
Expand All @@ -475,10 +485,32 @@ def create_containerapp(cmd,
containerapp_def["properties"]["template"] = template_def
containerapp_def["tags"] = tags

if registry_identity:
if is_registry_msi_system(registry_identity):
set_managed_identity(cmd, resource_group_name, containerapp_def, system_assigned=True)
else:
set_managed_identity(cmd, resource_group_name, containerapp_def, user_assigned=[registry_identity])

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 is_registry_msi_system(registry_identity):
while r["properties"]["provisioningState"] == "InProgress":
r = ContainerAppClient.show(cmd, resource_group_name, name)
time.sleep(10)
logger.info("Creating an acrpull role assignment for the system identity")
system_sp = r["identity"]["principalId"]
create_acrpull_role_assignment(cmd, registry_server, registry_identity=None, service_principal=system_sp)
container_def["image"] = image

registries_def = RegistryCredentialsModel
registries_def["server"] = registry_server
registries_def["identity"] = registry_identity
config_def["registries"] = [registries_def]

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:
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))

Expand Down Expand Up @@ -813,11 +845,14 @@ def update_containerapp(cmd,
no_wait)


def show_containerapp(cmd, name, resource_group_name):
def show_containerapp(cmd, name, resource_group_name, show_secrets=False):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

try:
return ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
r = ContainerAppClient.show(cmd=cmd, resource_group_name=resource_group_name, name=name)
if show_secrets:
_get_existing_secrets(cmd, resource_group_name, name, r)
return r
except CLIError as e:
handle_raw_exception(e)

Expand Down Expand Up @@ -938,7 +973,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:
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))

not disable_warnings and logger.warning("\nContainer Apps environment created. To deploy a container app, use: az containerapp create --help\n")
if "properties" in r and "provisioningState" in r["properties"] and r["properties"]["provisioningState"].lower() == "succeeded":
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:
Expand Down
Loading