| File: AzureSandboxGroupResource.cs | Web Access |
| Project: src\src\Aspire.Hosting.Azure.Sandboxes\Aspire.Hosting.Azure.Sandboxes.csproj (Aspire.Hosting.Azure.Sandboxes) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREPIPELINES001 #pragma warning disable ASPIREPIPELINES003 #pragma warning disable ASPIREAZURE001 #pragma warning disable ASPIRECOMPUTE002 using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.Sandboxes.Provisioning; using Aspire.Hosting.Pipelines; using Aspire.Hosting.Publishing; using Azure.Provisioning.Primitives; using Azure.Provisioning.Resources; namespace Aspire.Hosting.Azure; /// <summary> /// Represents an Azure Container Apps sandbox group. /// </summary> [AspireExport] [Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public sealed class AzureSandboxGroupResource : AzureProvisioningResource, IAzureComputeEnvironmentResource { internal const string ImagePullIdentityClientIdOutputName = "imagePullIdentityClientId"; /// <summary> /// Initializes a new instance of the <see cref="AzureSandboxGroupResource"/> class. /// </summary> /// <param name="name">The Aspire resource name.</param> /// <param name="configureInfrastructure">The callback that configures Azure provisioning infrastructure.</param> public AzureSandboxGroupResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) : base(name, configureInfrastructure) { Annotations.Add(new PipelineStepAnnotation(async factoryContext => { var model = factoryContext.PipelineContext.Model; var steps = new List<PipelineStep> { new() { Name = $"prepare-azure-sandboxes-{Name}", Description = $"Prepares Azure sandbox deployment targets for {Name}.", Action = PrepareDeploymentTargetsAsync, DependsOnSteps = [AzureEnvironmentResource.PrepareResourcesStepName, WellKnownPipelineSteps.ValidateComputeEnvironments], RequiredBySteps = [WellKnownPipelineSteps.BeforeStart] } }; foreach (var computeResource in model.GetComputeResources()) { var deploymentTarget = computeResource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget; if (deploymentTarget is null || !deploymentTarget.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var annotations)) { continue; } foreach (var annotation in annotations) { var childFactoryContext = new PipelineStepFactoryContext { PipelineContext = factoryContext.PipelineContext, Resource = deploymentTarget }; var deploymentTargetSteps = await annotation.CreateStepsAsync(childFactoryContext).ConfigureAwait(false); foreach (var step in deploymentTargetSteps) { step.Resource ??= deploymentTarget; } steps.AddRange(deploymentTargetSteps); } } return steps; })); Annotations.Add(new PipelineConfigurationAnnotation(async context => { foreach (var computeResource in context.Model.GetComputeResources()) { var deploymentTarget = computeResource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget; if (deploymentTarget is null || !deploymentTarget.TryGetAnnotationsOfType<PipelineConfigurationAnnotation>(out var annotations)) { continue; } foreach (var annotation in annotations) { await annotation.Callback(context).ConfigureAwait(false); } } })); } /// <summary> /// Gets the Azure resource name output reference. /// </summary> public BicepOutputReference NameOutputReference => new("name", this); /// <summary> /// Gets the Azure resource ID output reference. /// </summary> public BicepOutputReference IdOutputReference => new("id", this); /// <summary> /// Gets the Azure resource location output reference. /// </summary> public BicepOutputReference LocationOutputReference => new("location", this); internal ManagedServiceIdentityType WorkloadManagedIdentityType { get; set; } = ManagedServiceIdentityType.None; internal List<AzureUserAssignedIdentityResource> WorkloadUserAssignedIdentities { get; } = []; internal AzureContainerRegistryResource? DefaultContainerRegistry { get; set; } IAzureContainerRegistryResource? IAzureComputeEnvironmentResource.ContainerRegistry => ContainerRegistry; /// <summary> /// Gets the Azure Container Registry resource used by this sandbox group. /// </summary> public AzureContainerRegistryResource? ContainerRegistry { get { var registry = GetContainerRegistry(); if (registry is null) { return null; } if (registry is not AzureContainerRegistryResource azureRegistry) { throw new InvalidOperationException( $"The container registry configured for the Azure sandbox group '{Name}' is not an Azure Container Registry. " + $"Only Azure Container Registry resources are supported. Use '.WithAzureContainerRegistry()' to configure an Azure Container Registry."); } return azureRegistry; } } private async Task PrepareDeploymentTargetsAsync(PipelineStepContext context) { if (!context.ExecutionContext.IsPublishMode) { return; } if (this.HasAnnotationOfType<ContainerRegistryReferenceAnnotation>() && DefaultContainerRegistry is not null) { context.Model.Resources.Remove(DefaultContainerRegistry); DefaultContainerRegistry = null; } var containerRegistry = ContainerRegistry ?? throw new InvalidOperationException($"No container registry associated with Azure sandbox group '{Name}'. This should have been added automatically."); var imagePullIdentity = this.TryGetLastAnnotation<AzureSandboxGroupAcrPullIdentityAnnotation>(out var imagePullIdentityAnnotation) ? imagePullIdentityAnnotation.Identity : null; if (imagePullIdentity is not null && WorkloadUserAssignedIdentities.Contains(imagePullIdentity)) { throw new InvalidOperationException( $"Azure sandbox group '{Name}' uses identity '{imagePullIdentity.Name}' for both image pulls and workloads. " + "Use a dedicated image-pull identity so its AcrPull permission is not exposed to sandbox workloads."); } var computeEnvironments = context.Model.Resources.OfType<IComputeEnvironmentResource>().ToList(); var canClaimUnassignedComputeResources = computeEnvironments.Count == 1 && ReferenceEquals(computeEnvironments[0], this); foreach (var resource in context.Model.GetComputeResources()) { var resourceComputeEnvironment = resource.GetComputeEnvironment(); if (resourceComputeEnvironment is null && !canClaimUnassignedComputeResources) { continue; } if (resourceComputeEnvironment is not null && resourceComputeEnvironment != this) { continue; } if (resource.GetDeploymentTargetAnnotation(this) is not null) { continue; } if (resource.TryGetLastAnnotation<AppIdentityAnnotation>(out var appIdentity)) { if (appIdentity.IdentityResource is not AzureUserAssignedIdentityResource userAssignedIdentity) { throw new NotSupportedException( $"Compute resource '{resource.Name}' uses an application identity type that Azure sandboxes do not support."); } if (this.IsExisting()) { throw new InvalidOperationException( $"Compute resource '{resource.Name}' uses managed identity '{userAssignedIdentity.Name}', but workload identities are not supported when publishing to existing Azure sandbox group '{Name}'."); } if (ReferenceEquals(imagePullIdentity, userAssignedIdentity)) { throw new InvalidOperationException( $"Azure sandbox group '{Name}' uses identity '{userAssignedIdentity.Name}' for both image pulls and workload '{resource.Name}'. " + "Use a dedicated image-pull identity so its AcrPull permission is not exposed to sandbox workloads."); } AddWorkloadUserAssignedIdentity(userAssignedIdentity); } AzureSandboxContainerDeployment.ValidateSandboxCompatibility(resource); resource.Annotations.Add(new ContainerBuildOptionsCallbackAnnotation(static buildOptions => { // ADC requires a single Docker-format linux/amd64 manifest. Buildx's default OCI // index with provenance attestations can produce a disk image with no bootable root filesystem. buildOptions.Destination = ContainerImageDestination.Registry; buildOptions.OutputPath = null; buildOptions.ImageFormat = ContainerImageFormat.Docker; buildOptions.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; })); var sandboxContainer = new AzureSandboxContainerResource( $"{resource.Name}-sandbox-container", resource, this); sandboxContainer.Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); sandboxContainer.Annotations.Add(new PipelineStepAnnotation(_ => AzureSandboxContainerDeployment.CreatePipelineSteps(sandboxContainer))); sandboxContainer.Annotations.Add(new PipelineConfigurationAnnotation(async context => { await AzureSandboxContainerDeployment.ConfigureDeployOrderingAsync(context, sandboxContainer).ConfigureAwait(false); AzureSandboxContainerDeployment.ConfigureDestroyOrdering(context, sandboxContainer); })); resource.Annotations.Add(new DeploymentTargetAnnotation(sandboxContainer) { ContainerRegistry = containerRegistry, ComputeEnvironment = this }); } await Task.CompletedTask.ConfigureAwait(false); } /// <inheritdoc/> [Experimental("ASPIRECOMPUTE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public ReferenceExpression GetHostAddressExpression(EndpointReference endpointReference) { ArgumentNullException.ThrowIfNull(endpointReference); return GetEndpointPropertyExpression(endpointReference.Property(EndpointProperty.Host)); } /// <inheritdoc/> [Experimental("ASPIRECOMPUTE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public ReferenceExpression GetEndpointPropertyExpression(EndpointReferenceExpression endpointReferenceExpression) { ArgumentNullException.ThrowIfNull(endpointReferenceExpression); var endpointReference = endpointReferenceExpression.Endpoint; var resource = endpointReference.Resource; var deploymentTargetAnnotation = resource.GetDeploymentTargetAnnotation(this); if (deploymentTargetAnnotation?.DeploymentTarget is not AzureSandboxContainerResource sandboxContainer) { throw new InvalidOperationException($"Resource '{resource.Name}' is not deployed to Azure sandbox group '{Name}'."); } var provider = new AzureSandboxEndpointPropertyValueProvider(sandboxContainer, endpointReferenceExpression); return ReferenceExpression.Create($"{provider}"); } /// <inheritdoc/> public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra) { var bicepIdentifier = this.GetBicepIdentifier(); var existing = infra.GetProvisionableResources() .OfType<SandboxGroup>() .SingleOrDefault(group => group.BicepIdentifier == bicepIdentifier); if (existing is not null) { return existing; } var sandboxGroup = SandboxGroup.FromExisting(bicepIdentifier); if (!TryApplyExistingResourceAnnotation(this, infra, sandboxGroup)) { sandboxGroup.Name = NameOutputReference.AsProvisioningParameter(infra); } infra.Add(sandboxGroup); return sandboxGroup; } private IContainerRegistry? GetContainerRegistry() { if (this.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var annotation)) { return annotation.Registry; } return DefaultContainerRegistry; } internal void AddWorkloadUserAssignedIdentity(AzureUserAssignedIdentityResource identity) { ArgumentNullException.ThrowIfNull(identity); if (WorkloadUserAssignedIdentities.Contains(identity)) { return; } WorkloadUserAssignedIdentities.Add(identity); WorkloadManagedIdentityType = WorkloadManagedIdentityType switch { ManagedServiceIdentityType.None => ManagedServiceIdentityType.UserAssigned, ManagedServiceIdentityType.SystemAssigned => ManagedServiceIdentityType.SystemAssignedUserAssigned, _ => WorkloadManagedIdentityType }; } }