Skip to content

Commit

Permalink
Support managed identity to pull image from ACR (#2517)
Browse files Browse the repository at this point in the history
* print more info after deploying

* init managed identity support

* fix

* merge develop

* fix comments

* fix user mi issue
  • Loading branch information
RuoyuWang-MS authored Dec 16, 2024
1 parent 2691b3d commit bddb7b4
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppContainerMavenConfig> containers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions azure-toolkit-libs/azure-toolkit-containerapps-lib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-toolkit-auth-lib</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-toolkit-identity-lib</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-toolkit-containerregistry-lib</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@

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;
import com.azure.resourcemanager.appcontainers.models.Container;
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;
Expand Down Expand Up @@ -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;
Expand All @@ -71,13 +81,15 @@
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;

public class ContainerAppDraft extends ContainerApp implements AzResource.Draft<ContainerApp, com.azure.resourcemanager.appcontainers.models.ContainerApp> {
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
Expand Down Expand Up @@ -135,6 +147,7 @@ public com.azure.resourcemanager.appcontainers.models.ContainerApp createResourc
.withConfiguration(configuration)
.withTemplate(template)
.withWorkloadProfileName(workloadProfile)
.withIdentity(ensureMIAndACRPermission(imageConfig))
.create();
final Action<ContainerApp> updateImage = Optional.ofNullable(AzureActionManager.getInstance().getAction(ContainerApp.UPDATE_IMAGE))
.map(action -> action.bind(this))
Expand Down Expand Up @@ -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<ContainerApp> browse = Optional.ofNullable(AzureActionManager.getInstance().getAction(ContainerApp.BROWSE))
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -516,6 +594,8 @@ public static class ImageConfig {
private List<EnvironmentVar> environmentVariables = new ArrayList<>();
@Nullable
private BuildImageConfig buildImageConfig;
@Nullable
private String identity;

public ImageConfig(@Nonnull String fullImageName) {
this.fullImageName = fullImageName;
Expand Down

0 comments on commit bddb7b4

Please sign in to comment.