
com.microsoft.azure.toolkit.lib.containerapps.containerapp.ContainerAppDraft Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
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.Runtime;
import com.azure.resourcemanager.appcontainers.models.RuntimeJava;
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;
import com.microsoft.azure.toolkit.lib.Azure;
import com.microsoft.azure.toolkit.lib.common.action.Action;
import com.microsoft.azure.toolkit.lib.common.action.AzureActionManager;
import com.microsoft.azure.toolkit.lib.common.bundle.AzureString;
import com.microsoft.azure.toolkit.lib.common.exception.AzureToolkitRuntimeException;
import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager;
import com.microsoft.azure.toolkit.lib.common.messager.IAzureMessager;
import com.microsoft.azure.toolkit.lib.common.model.AzResource;
import com.microsoft.azure.toolkit.lib.common.model.Region;
import com.microsoft.azure.toolkit.lib.common.model.Subscription;
import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation;
import com.microsoft.azure.toolkit.lib.common.operation.OperationContext;
import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager;
import com.microsoft.azure.toolkit.lib.common.utils.Utils;
import com.microsoft.azure.toolkit.lib.containerapps.environment.ContainerAppsEnvironment;
import com.microsoft.azure.toolkit.lib.containerapps.environment.ContainerAppsEnvironmentDraft;
import com.microsoft.azure.toolkit.lib.containerapps.model.EnvironmentType;
import com.microsoft.azure.toolkit.lib.containerapps.model.IngressConfig;
import com.microsoft.azure.toolkit.lib.containerapps.model.ResourceConfiguration;
import com.microsoft.azure.toolkit.lib.containerapps.model.RevisionMode;
import com.microsoft.azure.toolkit.lib.containerapps.model.WorkloadProfile;
import com.microsoft.azure.toolkit.lib.containerregistry.AzureContainerRegistry;
import com.microsoft.azure.toolkit.lib.containerregistry.AzureContainerRegistryModule;
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;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
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 {
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
private final ContainerApp origin;
@Getter
@Setter
private Config config;
protected ContainerAppDraft(@Nonnull String name, @Nonnull String resourceGroupName, @Nonnull ContainerAppModule module) {
super(name, resourceGroupName, module);
this.origin = null;
}
protected ContainerAppDraft(@Nonnull ContainerApp origin) {
super(origin);
this.origin = origin;
}
@Override
public void reset() {
this.config = null;
}
@Nonnull
@Override
@AzureOperation(name = "azure/containerapps.create_app.app", params = {"this.getName()"})
public com.azure.resourcemanager.appcontainers.models.ContainerApp createResourceInAzure() {
final ContainerApps client = Objects.requireNonNull(((ContainerAppModule) getModule()).getClient());
final ContainerAppsEnvironment containerAppsEnvironment = Objects.requireNonNull(ensureConfig().getEnvironment(),
"Environment is required to create Container app.");
if (containerAppsEnvironment.isDraftForCreating()) {
((ContainerAppsEnvironmentDraft) containerAppsEnvironment).commit();
}
final ImageConfig imageConfig = Objects.requireNonNull(this.getImageConfig(), "Image is required to create Container app.");
buildImageIfNeeded(imageConfig);
final Configuration configuration = new Configuration();
Optional.ofNullable(ensureConfig().getRevisionMode()).ifPresent(mode ->
configuration.withActiveRevisionsMode(ActiveRevisionsMode.fromString(ensureConfig().getRevisionMode().getValue())));
configuration.withSecrets(Optional.ofNullable(getSecret(imageConfig)).map(Collections::singletonList).orElse(Collections.emptyList()));
configuration.withRegistries(Optional.ofNullable(getRegistryCredential(imageConfig)).map(Collections::singletonList).orElse(Collections.emptyList()));
configuration.withIngress(Optional.ofNullable(ensureConfig().getIngressConfig()).map(IngressConfig::toIngress).orElse(null));
configuration.withRuntime(new Runtime().withJava(new RuntimeJava().withEnableMetrics(true)));
final ResourceConfiguration resourceConfiguration = ensureConfig().getResourceConfiguration();
final Template template = new Template()
.withContainers(ImageConfig.toContainers(imageConfig, resourceConfiguration))
.withScale(ScaleConfig.toScale(this.getScaleConfig()));
AzureMessager.getMessager().progress(AzureString.format("Creating Azure Container App({0})...", this.getName()));
final String workloadProfile = containerAppsEnvironment.getEnvironmentType() == EnvironmentType.ConsumptionOnly ? null :
Optional.ofNullable(getResourceConfiguration()).map(ResourceConfiguration::getWorkloadProfile).map(WorkloadProfile::getName).orElse(WorkloadProfile.CONSUMPTION);
final com.azure.resourcemanager.appcontainers.models.ContainerApp result = client.define(ensureConfig().getName())
.withRegion(com.azure.core.management.Region.fromName(containerAppsEnvironment.getRegion().getName()))
.withExistingResourceGroup(Objects.requireNonNull(ensureConfig().getResourceGroup(), "Resource Group is required to create Container app.").getResourceGroupName())
.withManagedEnvironmentId(containerAppsEnvironment.getId())
.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))
.orElse(null);
final Action browse = Optional.ofNullable(AzureActionManager.getInstance().getAction(ContainerApp.BROWSE))
.map(action -> action.bind(this))
.orElse(null);
AzureMessager.getMessager().success(AzureString.format("Azure Container App({0}) is successfully created.", this.getName()), browse, updateImage);
printSuccessMessages();
return result;
}
// todo: support update workload profile properties
@Nonnull
@Override
@AzureOperation(name = "azure/containerapps.update_app.app", params = {"this.getName()"})
public com.azure.resourcemanager.appcontainers.models.ContainerApp updateResourceInAzure(@Nonnull com.azure.resourcemanager.appcontainers.models.ContainerApp origin) {
final IAzureMessager messager = AzureMessager.getMessager();
final Config config = ensureConfig();
final ImageConfig imageConfig = config.getImageConfig();
final IngressConfig ingressConfig = config.getIngressConfig();
final RevisionMode revisionMode = config.getRevisionMode();
final ScaleConfig scaleConfig = config.getScaleConfig();
final boolean isImageModified = Objects.nonNull(imageConfig) && !Objects.equals(imageConfig, super.getImageConfig());
final boolean isIngressConfigModified = Objects.nonNull(ingressConfig) && !Objects.equals(ingressConfig, super.getIngressConfig());
final boolean isRevisionModeModified = !Objects.equals(revisionMode, super.getRevisionMode());
final boolean isScaleModified = !Objects.equals(scaleConfig, super.getScaleConfig());
final boolean isModified = isImageModified || isIngressConfigModified || isRevisionModeModified || isScaleModified;
if (!isModified) {
return origin;
}
buildImageIfNeeded(imageConfig);
final ContainerAppImpl update = (ContainerAppImpl) (isImageModified ? this.updateImage(origin) : origin.update());
final Configuration configuration = update.configuration();
if (!isImageModified) {
// anytime you want to update the container app, you need to include the secrets but that is not retrieved by default
final List secrets = origin.listSecrets().value().stream().map(s -> new Secret().withName(s.name()).withValue(s.value())).collect(Collectors.toList());
final List registries = Optional.ofNullable(origin.configuration().registries()).map(ArrayList::new).orElseGet(ArrayList::new);
configuration.withRegistries(registries).withSecrets(secrets);
}
// ["properties"]["template"]["containers"]
if (isIngressConfigModified) {
configuration.withIngress(ingressConfig.toIngress());
}
if (isRevisionModeModified) {
configuration.withActiveRevisionsMode(revisionMode.toActiveRevisionMode());
}
if (isScaleModified) {
if (isImageModified) {
update.withTemplate(update.template().withScale(ScaleConfig.toScale(scaleConfig)));
} else {
update.withTemplate(new Template().withScale(ScaleConfig.toScale(scaleConfig)));
}
}
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))
.map(action -> action.bind(this))
.orElse(null);
messager.success(AzureString.format("Container App({0}) is successfully updated.", getName()), browse);
printSuccessMessages();
if (isImageModified) {
AzureTaskManager.getInstance().runOnPooledThread(() -> this.getRevisionModule().refresh());
}
return result;
}
private void printSuccessMessages() {
final Action learnMore = Optional.ofNullable(AzureActionManager.getInstance().getAction(Action.OPEN_URL).withLabel("Learn More"))
.map(action -> action.bind("https://aka.ms/azuretools-aca-stack"))
.orElse(null);
final Action openPortal = Optional.ofNullable(AzureActionManager.getInstance().getAction(Action.OPEN_URL).withLabel("Open Portal"))
.map(action -> action.bind(this.getPortalUrl()))
.orElse(null);
final Action openApp = Optional.ofNullable(this.getIngressFqdn())
.filter(fqdn -> !StringUtils.isEmpty(fqdn))
.flatMap(fqdn -> Optional.ofNullable(AzureActionManager.getInstance()
.getAction(Action.OPEN_URL)
.withLabel("Open Application"))
.map(action -> action.bind(String.format("https://%s", fqdn))))
.orElse(null);
AzureMessager.getMessager().info("To take advantage of the Java-optimized feature, please set your development stack to `Java` in the portal.", learnMore, openPortal, openApp);
}
@Nonnull
private com.azure.resourcemanager.appcontainers.models.ContainerApp.Update updateImage(@Nonnull com.azure.resourcemanager.appcontainers.models.ContainerApp origin) {
final ImageConfig config = Objects.requireNonNull(this.getConfig().getImageConfig(), "image config is null.");
final com.azure.resourcemanager.appcontainers.models.ContainerApp.Update update = origin.update();
final ContainerRegistry registry = config.getContainerRegistry();
final List secrets = origin.listSecrets().value().stream().map(s -> new Secret().withName(s.name()).withValue(s.value())).collect(Collectors.toList());
final List registries = Optional.ofNullable(origin.configuration().registries()).map(ArrayList::new).orElseGet(ArrayList::new);
if (Objects.nonNull(registry)) { // update registries and secrets for ACR
Optional.ofNullable(getSecret(config)).ifPresent(secret -> {
secrets.removeIf(r -> r.name().equalsIgnoreCase(secret.name()));
secrets.add(secret);
});
Optional.ofNullable(getRegistryCredential(config)).ifPresent(credential -> {
registries.removeIf(r -> r.server().equalsIgnoreCase(credential.server()));
registries.add(credential);
});
}
update.withConfiguration(origin.configuration()
.withRegistries(registries)
.withSecrets(secrets));
// drop old containers because we want to replace the old image
return update.withTemplate(origin.template().withContainers(ImageConfig.toContainers(config)));
}
public void buildImageIfNeeded(ImageConfig imageConfig) {
if (!Optional.ofNullable(imageConfig).map(ImageConfig::getBuildImageConfig).map(b -> b.source).filter(Files::exists).isPresent()) {
OperationContext.action().setTelemetryProperty("needBuildImage", "false");
return;
}
OperationContext.action().setTelemetryProperty("needBuildImage", "true");
OperationContext.action().setTelemetryProperty("hasDockerFile", String.valueOf(imageConfig.sourceHasDockerFile()));
final BuildImageConfig buildConfig = Objects.requireNonNull(imageConfig.getBuildImageConfig());
final String fullImageName;
Path tempFolder = null;
if (imageConfig.sourceHasDockerFile()) {
// ACR Task is the only way we have for now to build a Dockerfile using Docker.
AzureMessager.getMessager().warning("Dockerfile detected. Running the build through ACR.");
fullImageName = buildThroughACR(imageConfig, buildConfig);
} else {
OperationContext.action().setTelemetryProperty("isDirectory", String.valueOf(Files.isDirectory(buildConfig.source)));
if (Files.isDirectory(buildConfig.source)) {
AzureMessager.getMessager().warning("No Dockerfile detected. Running the build through ACR with a generated Dockerfile.");
generateDockerfile(buildConfig, sourceDockerFilePath);
} else {
AzureMessager.getMessager().warning("Building container image from artifact through ACR with a generated Dockerfile.");
tempFolder = generateTempFolder(buildConfig);
}
fullImageName = buildThroughACR(imageConfig, buildConfig);
}
deleteTempFolder(tempFolder);
if (StringUtils.isNotBlank(fullImageName)) {
imageConfig.setFullImageName(fullImageName);
}
}
private static void tarSourceIfNeeded(final BuildImageConfig buildConfig) {
if (Files.isDirectory(buildConfig.source)) {
final HashSet ignored = Sets.newHashSet(".git", ".gitignore", ".bzr", "bzrignore", ".hg", ".hgignore", ".svn");
AzureMessager.getMessager().progress(AzureString.format("Creating tar.gz from %s.", buildConfig.source.getFileName()));
final Path sourceTar = Utils.tar(buildConfig.source, (path) -> ignored.contains(path.getFileName().toString()));
buildConfig.setSource(sourceTar);
}
}
private String buildThroughACR(final ImageConfig imageConfig, final BuildImageConfig buildConfig) {
Map overridingArguments = Optional.ofNullable(imageConfig.getBuildImageConfig())
.map(BuildImageConfig::getSourceBuildEnv)
.orElse(Collections.emptyMap())
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> new OverridingArgument(e.getValue(), false)));
final ContainerRegistry registry = getOrCreateRegistry(imageConfig);
tarSourceIfNeeded(buildConfig);
final RegistryTaskRun run = registry.buildImage(imageConfig.getAcrImageNameWithTag(), buildConfig.getSource(), "./Dockerfile", overridingArguments);
if (Objects.isNull(run)) {
throw new AzureToolkitRuntimeException("ACR is not ready, Failed to build image through ACR.");
}
return registry.waitForImageBuilding(run);
}
private static void deleteTempFolder(Path tempFolder) {
if (Objects.isNull(tempFolder)) {
return;
}
try {
FileUtils.deleteDirectory(tempFolder.toFile());
} catch (IOException e) {
throw new AzureToolkitRuntimeException("Failed to delete temporary directory: " + tempFolder, e);
}
}
private static void generateDockerfile(final BuildImageConfig buildConfig, String templatePath) {
Path destination = buildConfig.source.resolve("Dockerfile");
// Path to the Dockerfile inside the resources/template folder
// Load the Dockerfile from the resources
InputStream inputStream = ContainerAppDraft.class.getClassLoader().getResourceAsStream(templatePath);
if (inputStream == null) {
throw new AzureToolkitRuntimeException("Template dockerfile not found in the resources: " + templatePath);
}
// Copy the Dockerfile to the destination path
try {
Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new AzureToolkitRuntimeException("Failed to copy Dockerfile to the destination path: " + destination, e);
}
AzureMessager.getMessager().info("Dockerfile generated successfully to: " + destination);
}
private static Path generateTempFolder(final BuildImageConfig buildConfig) {
// Create a temporary directory and handle resources
Path tempDir = null;
try {
// Step 1: Create a temporary directory
tempDir = Files.createTempDirectory(String.format("aca-maven-plugin-%s", Utils.getTimestamp()));
AzureMessager.getMessager().info("Temporary directory created: " + tempDir);
// Step 2: Copy Jar to the temporary directory
Path sourceFile = buildConfig.getSource(); // replace with your file path
Path targetFile = tempDir.resolve("app.jar");
Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
AzureMessager.getMessager().info("File copied to temporary directory: " + targetFile);
buildConfig.setSource(tempDir);
generateDockerfile(buildConfig, artifactDockerFilePath);
} catch (IOException e) {
if (Objects.nonNull(tempDir)) {
deleteTempFolder(tempDir);
}
throw new AzureToolkitRuntimeException("Failed to create temporary directory and copy artifact", e);
}
return tempDir;
}
@Nonnull
private ContainerRegistry getOrCreateRegistry(final ImageConfig config) {
ContainerRegistry registry = config.getContainerRegistry();
if (Objects.isNull(registry)) {
final String registryName = Objects.requireNonNull(config.getAcrRegistryName());
final AzureContainerRegistryModule registryModule = Azure.az(AzureContainerRegistry.class)
.registry(this.getSubscriptionId());
registry = registryModule.get(registryName, this.getResourceGroupName());
if (Objects.isNull(registry)) {
final List registries = registryModule.listByResourceGroup(this.getResourceGroupName());
if (!registries.isEmpty()) {
registry = registries.stream().filter(ContainerRegistry::isAdminUserEnabled).findAny().orElse(null);
if (Objects.isNull(registry)) {
registry = registries.stream().findFirst().orElse(null);
}
}
if (Objects.isNull(registry)) {
AzureMessager.getMessager().info(AzureString.format("creating new container registry %s with admin user enabled.", registryName));
registry = registryModule.create(registryName, this.getResourceGroupName());
final ContainerRegistryDraft draft = (ContainerRegistryDraft) registry;
draft.setSku(Sku.Standard);
draft.setAdminUserEnabled(true);
draft.setRegion(Optional.ofNullable(this.getRegion()).orElse(Region.US_EAST));
draft.commit();
} else {
AzureMessager.getMessager().info(AzureString.format("use container registry %s.", registry.getName()));
}
}
}
if (registry.isDraftForCreating()) {
((ContainerRegistryDraft) registry).setAdminUserEnabled(true);
((ContainerRegistryDraft) registry).commit();
} else if (!registry.isAdminUserEnabled()) {// enable admin user
AzureMessager.getMessager().info(AzureString.format("Enabling admin user for container registry %s.", registry.getName()));
registry.enableAdminUser();
}
config.setContainerRegistry(registry);
return registry;
}
@Nullable
private static Secret getSecret(final ImageConfig config) {
final ContainerRegistry registry = config.getContainerRegistry();
if (Objects.nonNull(registry)) {
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;
}
@Nullable
private static RegistryCredentials getRegistryCredential(final ImageConfig config) {
final ContainerRegistry registry = config.getContainerRegistry();
if (Objects.nonNull(registry)) {
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);
return this.config;
}
@Override
@Nullable
public ScaleConfig getScaleConfig() {
return Optional.ofNullable(config).map(Config::getScaleConfig).orElse(super.getScaleConfig());
}
@Override
@Nullable
public IngressConfig getIngressConfig() {
return Optional.ofNullable(config).map(Config::getIngressConfig).orElse(super.getIngressConfig());
}
@Override
@Nullable
public ImageConfig getImageConfig() {
return Optional.ofNullable(config).map(Config::getImageConfig).orElse(super.getImageConfig());
}
@Override
@Nullable
public RevisionMode getRevisionMode() {
return Optional.ofNullable(config).map(Config::getRevisionMode).orElse(super.getRevisionMode());
}
@Nullable
@Override
public ContainerAppsEnvironment getManagedEnvironment() {
return Optional.ofNullable(config).map(Config::getEnvironment).orElseGet(super::getManagedEnvironment);
}
@Nullable
@Override
public String getManagedEnvironmentId() {
return Optional.ofNullable(config).map(Config::getEnvironment).map(ContainerAppsEnvironment::getId).orElseGet(super::getManagedEnvironmentId);
}
@Nullable
@Override
public Region getRegion() {
return Optional.ofNullable(config).map(Config::getEnvironment).map(ContainerAppsEnvironment::getRegion).orElseGet(super::getRegion);
}
@Override
public boolean isIngressEnabled() {
return Optional.ofNullable(config).map(Config::getIngressConfig).map(IngressConfig::isEnableIngress).orElseGet(super::isIngressEnabled);
}
public ResourceConfiguration getResourceConfiguration() {
return Optional.ofNullable(config).map(Config::getResourceConfiguration).orElseGet(super::getResourceConfiguration);
}
@Override
public boolean isModified() {
return this.config == null || Objects.equals(this.config, new Config());
}
@Data
public static class Config {
private String name;
private Subscription subscription;
private ResourceGroup resourceGroup;
@Nullable
private ContainerAppsEnvironment environment;
private RevisionMode revisionMode = RevisionMode.SINGLE;
@Nullable
private ImageConfig imageConfig;
@Nullable
private IngressConfig ingressConfig;
@Nullable
private ScaleConfig scaleConfig;
@Nullable
private ResourceConfiguration resourceConfiguration;
}
@Setter
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public static class ImageConfig {
@Nonnull
@EqualsAndHashCode.Include
private String fullImageName;
@Nullable
private ContainerRegistry containerRegistry;
@Nonnull
private List environmentVariables = new ArrayList<>();
@Nullable
private BuildImageConfig buildImageConfig;
@Nullable
private String identity;
public ImageConfig(@Nonnull String fullImageName) {
this.fullImageName = fullImageName;
}
public String getTag() {
return Optional.of(fullImageName.substring(fullImageName.lastIndexOf(':') + 1)).filter(StringUtils::isNotBlank).orElse("latest");
}
public String getRegistryUrl() {
return fullImageName.substring(0, fullImageName.indexOf('/'));
}
@Nullable
public String getAcrRegistryName() {
final String registryUrl = this.getRegistryUrl();
if (registryUrl.endsWith(ACR_IMAGE_SUFFIX)) {
return registryUrl.substring(0, registryUrl.length() - ACR_IMAGE_SUFFIX.length());
}
return null;
}
public String getAcrImageNameWithTag() {
return fullImageName.substring(fullImageName.indexOf('/') + 1);
}
public boolean sourceHasDockerFile() {
return Optional.ofNullable(buildImageConfig).map(BuildImageConfig::sourceHasDockerFile).orElse(false);
}
public static List toContainers(@Nonnull final ImageConfig config) {
return toContainers(config, null);
}
public static List toContainers(@Nonnull final ImageConfig config, @Nullable ResourceConfiguration resource) {
final String imageId = config.getFullImageName();
final String containerName = getContainerNameForImage(imageId);
// drop old containers because we want to replace the old image
final Container container = new Container().withName(containerName).withImage(imageId).withEnv(config.getEnvironmentVariables());
if (Objects.nonNull(resource)) {
final ContainerResources containerResources = new ContainerResources();
containerResources.withCpu(resource.getCpu());
containerResources.withMemory(resource.getMemory());
container.withResources(containerResources);
}
return Collections.singletonList(container);
}
private static String getContainerNameForImage(String containerImageName) {
final String name = containerImageName.substring(containerImageName.lastIndexOf('/') + 1).replaceAll("[^0-9a-zA-Z-]", "-").toLowerCase();
// The length of container name can not be more than 46.
return StringUtils.substring(name, 0, 46);
}
}
@Getter
@Setter
@NoArgsConstructor
public static class BuildImageConfig {
@Nonnull
private Path source;
private Map sourceBuildEnv;
public boolean sourceHasDockerFile() {
return Optional.of(source)
.filter(Files::isDirectory)
.map(p -> Files.isRegularFile(Paths.get(p.toString(), "Dockerfile"))).orElse(false);
}
}
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public static class ScaleConfig {
@EqualsAndHashCode.Include
private Integer maxReplicas;
@Builder.Default
@EqualsAndHashCode.Include
private Integer minReplicas = 1;
public static Scale toScale(ScaleConfig config) {
return Optional.ofNullable(config).map(s -> new Scale().withMinReplicas(s.minReplicas).withMaxReplicas(s.maxReplicas)).orElse(null);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy