Skip to content

Commit

Permalink
[Containerapp] az containerapp create/up: --registry-server and `…
Browse files Browse the repository at this point in the history
…--source` use managed identity for image pull by default (#7972)
  • Loading branch information
Greedygre authored Oct 8, 2024
1 parent a8f9b24 commit 3005636
Show file tree
Hide file tree
Showing 51 changed files with 473,349 additions and 66,448 deletions.
3 changes: 3 additions & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Release History
===============
upcoming
++++++
* 'az containerapp up': Support `--registry-identity`, `--system-assigned`, `--user-assigned`
* 'az containerapp containerapp create/up': `--registry-server` and `--source` use managed identity for image pull by default
* 'az containerapp containerapp create': `--registry-server` use managed identity for image pull by default. `--no-wait` will not take effect with system registry identity.

1.0.0b3
++++++
Expand Down
5 changes: 2 additions & 3 deletions src/containerapp/azext_containerapp/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ def _polish_bad_errors(ex):
import json
try:
content = json.loads(ex.response.content)
detail = None
if 'message' in content:
detail = content['message']
ex = CLIInternalError(detail)
elif 'Message' in content:
detail = content['Message']

ex = CLIInternalError(detail)
ex = CLIInternalError(detail)
except Exception: # pylint: disable=broad-except
pass
if no_throw:
Expand Down
4 changes: 4 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ def load_arguments(self, _):
c.argument('service_principal_client_secret', help='The service principal client secret. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-client-secret", "--sp-sec"])
c.argument('service_principal_tenant_id', help='The service principal tenant ID. Used by Github Actions to authenticate with Azure.', options_list=["--service-principal-tenant-id", "--sp-tid"])

with self.argument_context('containerapp up', arg_group='Identity') as c:
c.argument('user_assigned', nargs='+', help="Space-separated user identities to be assigned.")
c.argument('system_assigned', help="Boolean indicating whether to assign system-assigned identity.", action='store_true')

with self.argument_context('containerapp env workload-profile set') as c:
c.argument('workload_profile_type', help="The type of workload profile to add or update. Run 'az containerapp env workload-profile list-supported -l <region>' to check the options for your region.")
c.argument('min_nodes', help="The minimum node count for the workload profile")
Expand Down
73 changes: 46 additions & 27 deletions src/containerapp/azext_containerapp/_up_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,10 @@ def __init__(
env_vars=None,
workload_profile_name=None,
ingress=None,
force_single_container_updates=None
force_single_container_updates=None,
registry_identity=None,
user_assigned=None,
system_assigned=None,
):

super().__init__(cmd, name, resource_group, exists)
Expand All @@ -419,13 +422,17 @@ def __init__(
self.registry_server = registry_server
self.registry_user = registry_user
self.registry_pass = registry_pass
self.registry_identity = registry_identity
self.user_assigned = user_assigned
self.system_assigned = system_assigned
self.env_vars = env_vars
self.ingress = ingress
self.workload_profile_name = workload_profile_name
self.force_single_container_updates = force_single_container_updates

self.should_create_acr = False
self.acr: "AzureContainerRegistry" = None
self.get_acr_creds = True

def _get(self):
return ContainerAppPreviewClient.show(self.cmd, self.resource_group.name, self.name)
Expand Down Expand Up @@ -455,7 +462,10 @@ def create(self, no_registry=False): # pylint: disable=arguments-differ
workload_profile_name=self.workload_profile_name,
ingress=self.ingress,
environment_type=CONNECTED_ENVIRONMENT_TYPE if self.env.is_connected_environment() else MANAGED_ENVIRONMENT_TYPE,
force_single_container_updates=self.force_single_container_updates
force_single_container_updates=self.force_single_container_updates,
registry_identity=self.registry_identity,
system_assigned=self.system_assigned,
user_assigned=self.user_assigned
)

def set_force_single_container_updates(self, force_single_container_updates):
Expand Down Expand Up @@ -483,10 +493,10 @@ def create_acr(self):

if not self.acr:
self.acr = AzureContainerRegistry(registry_name, registry_rg)

self.registry_user, self.registry_pass, _ = _get_acr_cred(
self.cmd.cli_ctx, registry_name
)
if self.get_acr_creds:
self.registry_user, self.registry_pass, _ = _get_acr_cred(
self.cmd.cli_ctx, registry_name
)

def _docker_push_to_container_registry(self, image_name, forced_acr_login=False):
from azure.cli.command_modules.acr.custom import acr_login
Expand Down Expand Up @@ -1246,29 +1256,14 @@ def _get_acr_from_image(cmd, app):
app.registry_server = app.image.split("/")[
0
] # TODO what if this conflicts with registry_server param?

parsed = urlparse(app.image)
registry_name = (parsed.netloc if parsed.scheme else parsed.path).split(".")[0]
if app.registry_user is None or app.registry_pass is None:
logger.info(
"No credential was provided to access Azure Container Registry. Trying to look up..."
)
try:
app.registry_user, app.registry_pass, registry_rg = _get_acr_cred(
cmd.cli_ctx, registry_name
)
app.acr = AzureContainerRegistry(
registry_name, ResourceGroup(cmd, registry_rg, None, None)
)
except Exception as ex:
raise RequiredArgumentMissingError(
"Failed to retrieve credentials for container registry. Please provide the registry username and password"
) from ex
else:
acr_rg = _get_acr_rg(app)
app.acr = AzureContainerRegistry(
name=registry_name,
resource_group=ResourceGroup(app.cmd, acr_rg, None, None),
)
acr_rg = _get_acr_rg(app)
app.acr = AzureContainerRegistry(
name=registry_name,
resource_group=ResourceGroup(cmd, acr_rg, None, None),
)


def _get_registry_from_app(app, source):
Expand Down Expand Up @@ -1349,6 +1344,30 @@ def _get_registry_details(cmd, app: "ContainerApp", source):
)


def _get_registry_details_without_get_creds(cmd, app: "ContainerApp", source):
if app.registry_server:
if "azurecr.io" not in app.registry_server and source:
raise ValidationError(
"Cannot supply non-Azure registry when using --source."
)
parsed = urlparse(app.registry_server)
registry_name = (parsed.netloc if parsed.scheme else parsed.path).split(".")[0]
registry_rg = _get_acr_rg(app)
else:
registry_name, registry_rg = find_existing_acr(cmd, app)
if registry_name and registry_rg:
app.registry_server = registry_name + ACR_IMAGE_SUFFIX
else:
registry_rg = app.resource_group.name
registry_name = _get_default_registry_name(app)
app.registry_server = registry_name + ACR_IMAGE_SUFFIX
app.should_create_acr = True

app.acr = AzureContainerRegistry(
registry_name, ResourceGroup(cmd, registry_rg, None, None)
)


# attempt to populate defaults for managed env, RG, ACR, etc
def _set_up_defaults(
cmd,
Expand Down
55 changes: 54 additions & 1 deletion src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from enum import Enum
from urllib.request import urlopen

from azure.cli.command_modules.acr.custom import acr_show
from azure.cli.command_modules.containerapp._utils import safe_get, _ensure_location_allowed, \
_generate_log_analytics_if_not_provided
from azure.cli.command_modules.containerapp._client_factory import handle_raw_exception
Expand All @@ -39,7 +40,7 @@
from ._constants import (CONTAINER_APP_EXTENSION_TYPE,
CONNECTED_ENV_CHECK_CERTIFICATE_NAME_AVAILABILITY_TYPE, DEV_SERVICE_LIST,
MANAGED_ENVIRONMENT_RESOURCE_TYPE, CONTAINER_APPS_RP, CONNECTED_CLUSTER_TYPE,
DEFAULT_CONNECTED_CLUSTER_EXTENSION_NAMESPACE)
DEFAULT_CONNECTED_CLUSTER_EXTENSION_NAMESPACE, ACR_IMAGE_SUFFIX)

logger = get_logger(__name__)

Expand Down Expand Up @@ -775,3 +776,55 @@ def env_has_managed_identity(cmd, resource_group_name, env_name, identity):
result = True
break
return result


def create_acrpull_role_assignment_if_needed(cmd, registry_server, registry_identity=None, service_principal=None, skip_error=False):
import time
from azure.cli.command_modules.acr._utils import ResourceNotFound
from azure.cli.core.profiles import ResourceType
from azure.mgmt.containerregistry import ContainerRegistryManagementClient
from azure.cli.command_modules.role.custom import list_role_assignments, create_role_assignment
from azure.cli.core.azclierror import UnauthorizedError

if registry_identity:
registry_identity_parsed = parse_resource_id(registry_identity)
registry_identity_name, registry_identity_rg, registry_identity_sub = registry_identity_parsed.get("name"), registry_identity_parsed.get("resource_group"), registry_identity_parsed.get("subscription")
sp_id = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_MSI, subscription_id=registry_identity_sub).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
try:
acr_id = acr_show(cmd, client, registry_server[: registry_server.rindex(ACR_IMAGE_SUFFIX)]).id
except ResourceNotFound 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 <container-registry-resource-id> --role acrpull'. \n"
"You may have to restart the containerapp with 'az containerapp revision restart'.")
logger.warning(message)
return

role_assignments = None
# Always assign role assignment even if list role assignments throw error
try:
role_assignments = list_role_assignments(cmd, assignee=sp_id, role="acrpull", scope=acr_id)
except: # pylint: disable=bare-except
pass
if not role_assignments:
logger.warning("Creating an acrpull role assignment for the registry identity")
retries = 10
while retries > 0:
try:
create_role_assignment(cmd, role="acrpull", assignee=sp_id, scope=acr_id)
return
except Exception as e:
retries -= 1
if retries <= 0:
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) from e
else:
time.sleep(5)
Loading

0 comments on commit 3005636

Please sign in to comment.