Skip to content

Commit

Permalink
Merge pull request #133 from StrawnSC/registry_identity_for_create
Browse files Browse the repository at this point in the history
Registry Identities for `az containerapp create`
  • Loading branch information
StrawnSC authored Aug 11, 2022
2 parents d289220 + 02e3a07 commit 49e1fc7
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 20 deletions.
1 change: 1 addition & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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

Expand Down
2 changes: 2 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@

NAME_INVALID = "Invalid"
NAME_ALREADY_EXISTS = "AlreadyExists"

HELLO_WORLD_IMAGE = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
1 change: 1 addition & 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 Down
35 changes: 34 additions & 1 deletion 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 @@ -1431,3 +1436,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"
19 changes: 16 additions & 3 deletions src/containerapp/azext_containerapp/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@
# --------------------------------------------------------------------------------------------
# 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:
Expand Down Expand Up @@ -49,7 +62,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
60 changes: 46 additions & 14 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.rindex("/")]
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
1 change: 0 additions & 1 deletion src/containerapp/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

VERSION = '0.3.9'


# The full list of classifiers is available at
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
CLASSIFIERS = [
Expand Down

0 comments on commit 49e1fc7

Please sign in to comment.