diff --git a/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/AbstractMojoBase.java b/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/AbstractMojoBase.java index c3f67935c..154d48a04 100644 --- a/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/AbstractMojoBase.java +++ b/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/AbstractMojoBase.java @@ -61,6 +61,10 @@ public abstract class AbstractMojoBase extends AbstractAzureMojo { @Parameter(property = "registry") protected ContainerRegistryConfig registry; + @Getter + @Parameter(property = "identity") + protected String identity; + @Getter @Parameter(property = "containers") protected List containers; diff --git a/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/parser/ConfigParser.java b/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/parser/ConfigParser.java index 04b9f258c..a1d92c69e 100644 --- a/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/parser/ConfigParser.java +++ b/azure-container-apps-maven-plugin/src/main/java/com/microsoft/azure/maven/containerapps/parser/ConfigParser.java @@ -81,6 +81,9 @@ public ContainerAppDraft.ImageConfig getImageConfigFromContainers(ContainerAppCo buildImageConfig.setSourceBuildEnv(sourceBuildEnv); imageConfig.setBuildImageConfig(buildImageConfig); } + if (!StringUtils.isEmpty(mojo.getIdentity())) { + imageConfig.setIdentity(mojo.getIdentity()); + } return imageConfig; } diff --git a/azure-toolkit-libs/azure-toolkit-containerapps-lib/pom.xml b/azure-toolkit-libs/azure-toolkit-containerapps-lib/pom.xml index ccd5a9458..0bcb27293 100644 --- a/azure-toolkit-libs/azure-toolkit-containerapps-lib/pom.xml +++ b/azure-toolkit-libs/azure-toolkit-containerapps-lib/pom.xml @@ -26,6 +26,10 @@ com.microsoft.azure azure-toolkit-auth-lib + + com.microsoft.azure + azure-toolkit-identity-lib + com.microsoft.azure azure-toolkit-containerregistry-lib diff --git a/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerApp.java b/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerApp.java index c9ae2f5bc..99316267b 100644 --- a/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerApp.java +++ b/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerApp.java @@ -10,6 +10,7 @@ import com.azure.resourcemanager.appcontainers.models.Configuration; import com.azure.resourcemanager.appcontainers.models.Container; import com.azure.resourcemanager.appcontainers.models.Ingress; +import com.azure.resourcemanager.appcontainers.models.ManagedServiceIdentity; import com.azure.resourcemanager.appcontainers.models.Template; import com.azure.resourcemanager.appcontainers.models.Volume; import com.azure.resourcemanager.resources.fluentcore.arm.ResourceId; @@ -304,4 +305,9 @@ public ResourceConfiguration getResourceConfiguration() { .findFirst().orElse(null); return ResourceConfiguration.builder().workloadProfile(workloadProfile).build(); } + + @Nullable + public ManagedServiceIdentity getIdentity() { + return Optional.ofNullable(getRemote()).map(com.azure.resourcemanager.appcontainers.models.ContainerApp::identity).orElse(null); + } } diff --git a/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerAppDraft.java b/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerAppDraft.java index ce0dfd33e..10d561261 100644 --- a/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerAppDraft.java +++ b/azure-toolkit-libs/azure-toolkit-containerapps-lib/src/main/java/com/microsoft/azure/toolkit/lib/containerapps/containerapp/ContainerAppDraft.java @@ -5,6 +5,9 @@ package com.microsoft.azure.toolkit.lib.containerapps.containerapp; +import com.azure.core.management.serializer.SerializerFactory; +import com.azure.core.util.serializer.SerializerAdapter; +import com.azure.core.util.serializer.SerializerEncoding; import com.azure.resourcemanager.appcontainers.implementation.ContainerAppImpl; import com.azure.resourcemanager.appcontainers.models.ActiveRevisionsMode; import com.azure.resourcemanager.appcontainers.models.Configuration; @@ -12,10 +15,15 @@ import com.azure.resourcemanager.appcontainers.models.ContainerApps; import com.azure.resourcemanager.appcontainers.models.ContainerResources; import com.azure.resourcemanager.appcontainers.models.EnvironmentVar; +import com.azure.resourcemanager.appcontainers.models.ManagedServiceIdentity; +import com.azure.resourcemanager.appcontainers.models.ManagedServiceIdentityType; import com.azure.resourcemanager.appcontainers.models.RegistryCredentials; import com.azure.resourcemanager.appcontainers.models.Scale; import com.azure.resourcemanager.appcontainers.models.Secret; import com.azure.resourcemanager.appcontainers.models.Template; +import com.azure.resourcemanager.appcontainers.models.UserAssignedIdentity; +import com.azure.resourcemanager.authorization.AuthorizationManager; +import com.azure.resourcemanager.authorization.models.RoleAssignment; import com.azure.resourcemanager.containerregistry.models.OverridingArgument; import com.azure.resourcemanager.containerregistry.models.RegistryTaskRun; import com.google.common.collect.Sets; @@ -45,6 +53,8 @@ import com.microsoft.azure.toolkit.lib.containerregistry.ContainerRegistry; import com.microsoft.azure.toolkit.lib.containerregistry.ContainerRegistryDraft; import com.microsoft.azure.toolkit.lib.containerregistry.model.Sku; +import com.microsoft.azure.toolkit.lib.identities.AzureManagedIdentity; +import com.microsoft.azure.toolkit.lib.identities.Identity; import com.microsoft.azure.toolkit.lib.resource.ResourceGroup; import lombok.AllArgsConstructor; import lombok.Builder; @@ -71,6 +81,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import static com.microsoft.azure.toolkit.lib.containerregistry.ContainerRegistry.ACR_IMAGE_SUFFIX; @@ -78,6 +89,7 @@ public class ContainerAppDraft extends ContainerApp implements AzResource.Draft { private static final String sourceDockerFilePath = "template/aca/source-dockerfile"; private static final String artifactDockerFilePath = "template/aca/artifact-dockerfile"; + public static final String ACR_PULL_ROLE_ID = "7f951dda-4ed3-4680-a7ca-43fe172d538d"; @Getter @Nullable @@ -135,6 +147,7 @@ public com.azure.resourcemanager.appcontainers.models.ContainerApp createResourc .withConfiguration(configuration) .withTemplate(template) .withWorkloadProfileName(workloadProfile) + .withIdentity(ensureMIAndACRPermission(imageConfig)) .create(); final Action updateImage = Optional.ofNullable(AzureActionManager.getInstance().getAction(ContainerApp.UPDATE_IMAGE)) .map(action -> action.bind(this)) @@ -192,6 +205,10 @@ public com.azure.resourcemanager.appcontainers.models.ContainerApp updateResourc } } update.withConfiguration(configuration); + ManagedServiceIdentity identity = ensureMIAndACRPermission(imageConfig); + if (Objects.nonNull(identity)) { + update.withIdentity(identity); + } messager.progress(AzureString.format("Updating Container App({0})...", getName())); final com.azure.resourcemanager.appcontainers.models.ContainerApp result = update.apply(); final Action browse = Optional.ofNullable(AzureActionManager.getInstance().getAction(ContainerApp.BROWSE)) @@ -402,10 +419,12 @@ private ContainerRegistry getOrCreateRegistry(final ImageConfig config) { private static Secret getSecret(final ImageConfig config) { final ContainerRegistry registry = config.getContainerRegistry(); if (Objects.nonNull(registry)) { - final String password = Optional.ofNullable(registry.getPrimaryCredential()).orElseGet(registry::getSecondaryCredential); - final String passwordKey = Objects.equals(password, registry.getPrimaryCredential()) ? "password" : "password2"; - final String passwordName = String.format("%s-%s", registry.getName().toLowerCase(), passwordKey); - return new Secret().withName(passwordName).withValue(password); + if (StringUtils.isEmpty(config.identity)) { + final String password = Optional.ofNullable(registry.getPrimaryCredential()).orElseGet(registry::getSecondaryCredential); + final String passwordKey = Objects.equals(password, registry.getPrimaryCredential()) ? "password" : "password2"; + final String passwordName = String.format("%s-%s", registry.getName().toLowerCase(), passwordKey); + return new Secret().withName(passwordName).withValue(password); + } } return null; } @@ -414,15 +433,74 @@ private static Secret getSecret(final ImageConfig config) { private static RegistryCredentials getRegistryCredential(final ImageConfig config) { final ContainerRegistry registry = config.getContainerRegistry(); if (Objects.nonNull(registry)) { - final String username = registry.getUserName(); - final String password = Optional.ofNullable(registry.getPrimaryCredential()).orElseGet(registry::getSecondaryCredential); - final String passwordKey = Objects.equals(password, registry.getPrimaryCredential()) ? "password" : "password2"; - final String passwordName = String.format("%s-%s", registry.getName().toLowerCase(), passwordKey); - return new RegistryCredentials().withServer(registry.getLoginServerUrl()).withUsername(username).withPasswordSecretRef(passwordName); + if (StringUtils.isEmpty(config.identity)) { + final String username = registry.getUserName(); + final String password = Optional.ofNullable(registry.getPrimaryCredential()).orElseGet(registry::getSecondaryCredential); + final String passwordKey = Objects.equals(password, registry.getPrimaryCredential()) ? "password" : "password2"; + final String passwordName = String.format("%s-%s", registry.getName().toLowerCase(), passwordKey); + return new RegistryCredentials().withServer(registry.getLoginServerUrl()).withUsername(username).withPasswordSecretRef(passwordName); + } else if (StringUtils.equalsIgnoreCase(config.identity, "system")) { + return new RegistryCredentials().withServer(registry.getLoginServerUrl()).withIdentity("system"); + } else { + return new RegistryCredentials().withServer(registry.getLoginServerUrl()).withIdentity(config.identity); + } } return null; } + // Only user assigned identity will be returned, and it will be added to the container app. + // System assigned identity should be enabled before using it to pull acr image. So no need to return it here. + @Nullable + private ManagedServiceIdentity ensureMIAndACRPermission(ImageConfig imageConfig) { + if (StringUtils.isBlank(imageConfig.getIdentity())) { + return null; + } + if (StringUtils.equalsIgnoreCase(imageConfig.getIdentity(), "system")) { + String principalId = Optional.ofNullable(this.origin) + .map(ContainerApp::getIdentity) + .filter(identity -> identity.type().equals(ManagedServiceIdentityType.SYSTEM_ASSIGNED) || identity.type().equals(ManagedServiceIdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED)) + .map(identity -> identity.principalId().toString()) + .orElseThrow(() -> new AzureToolkitRuntimeException("System managed identity should be enabled before using to pull acr image.")); + grantACRPullPermissionToIdentity(imageConfig, principalId); + return null; + } + try { + Identity identity = Azure.az(AzureManagedIdentity.class).getById(imageConfig.getIdentity()); + grantACRPullPermissionToIdentity(imageConfig, identity.getPrincipalId()); + return new ManagedServiceIdentity().withType(ManagedServiceIdentityType.USER_ASSIGNED).withUserAssignedIdentities(Collections.singletonMap(identity.getId(), new UserAssignedIdentity())); + } catch (Exception e) { + throw new AzureToolkitRuntimeException("Failed to get Registry Identity.", e); + } + + } + + private void grantACRPullPermissionToIdentity(ImageConfig imageConfig, String identityPrincipalId) { + final String scope = imageConfig.getContainerRegistry().getId(); + final RoleAssignment existingAssignment = getExistingRoleAssignment(identityPrincipalId, scope); + final String roleDefinitionId = String.format("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", getSubscriptionId(), ACR_PULL_ROLE_ID); + if (Objects.nonNull(existingAssignment)) { + AzureMessager.getMessager().info("ACR pull permission already granted to the identity."); + return; + } + final AuthorizationManager authorizationManager = this.getAuthorizationManager(); + final String roleAssignmentName = UUID.randomUUID().toString(); + authorizationManager.roleAssignments().define(roleAssignmentName) + .forObjectId(identityPrincipalId) + .withRoleDefinition(roleDefinitionId) + .withScope(scope).create(); + AzureMessager.getMessager().info("ACR pull permission granted to the identity."); + } + + private RoleAssignment getExistingRoleAssignment(final String identityId, final String scope) { + final AuthorizationManager authorizationManager = this.getAuthorizationManager(); + final String roleDefinitionId = String.format("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", getSubscriptionId(), ACR_PULL_ROLE_ID); + return authorizationManager.roleAssignments() + .listByScope(scope).stream() + .filter(assignment -> StringUtils.equalsIgnoreCase(assignment.principalId(), identityId) && + StringUtils.equalsIgnoreCase(assignment.roleDefinitionId(), roleDefinitionId)) + .findFirst().orElse(null); + } + @Nonnull private synchronized Config ensureConfig() { this.config = Optional.ofNullable(this.config).orElseGet(Config::new); @@ -516,6 +594,8 @@ public static class ImageConfig { private List environmentVariables = new ArrayList<>(); @Nullable private BuildImageConfig buildImageConfig; + @Nullable + private String identity; public ImageConfig(@Nonnull String fullImageName) { this.fullImageName = fullImageName;