File: Dcp\ContainerCreator.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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 ASPIRECERTIFICATES001
#pragma warning disable ASPIRECONTAINERSHELLEXECUTION001
 
using System.Collections.Immutable;
using System.Net.Sockets;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp.Model;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Dcp;
 
/// <summary>
/// Handles preparation and creation of Container and ContainerExec DCP resources.
/// </summary>
internal sealed class ContainerCreator : IObjectCreator<Container, ContainerNetworkEndpointContext>, IObjectCreator<ContainerExec, EmptyCreationContext>
{
    private readonly IConfiguration _configuration;
    private readonly IOptions<DcpOptions> _options;
    private readonly DcpNameGenerator _nameGenerator;
    private readonly DistributedApplicationModel _model;
    private readonly DistributedApplicationExecutionContext _executionContext;
    private readonly ResourceLoggerService _loggerService;
    private readonly IDcpDependencyCheckService _dcpDependencyCheckService;
    private readonly ILogger<ContainerCreator> _logger;
    private readonly DcpAppResourceStore _appResources;
    private readonly ContainerNetworkEndpointProvisioner _containerNetworkEndpointProvisioner;
 
    public ContainerCreator(
        IConfiguration configuration,
        IOptions<DcpOptions> options,
        DcpNameGenerator nameGenerator,
        DistributedApplicationModel model,
        DistributedApplicationExecutionContext executionContext,
        ResourceLoggerService loggerService,
        IDcpDependencyCheckService dcpDependencyCheckService,
        ILogger<ContainerCreator> logger,
        DcpAppResourceStore appResources,
        ContainerNetworkEndpointProvisioner containerNetworkEndpointProvisioner)
    {
        _configuration = configuration;
        _options = options;
        _nameGenerator = nameGenerator;
        _model = model;
        _executionContext = executionContext;
        _loggerService = loggerService;
        _dcpDependencyCheckService = dcpDependencyCheckService;
        _logger = logger;
        _appResources = appResources;
        _containerNetworkEndpointProvisioner = containerNetworkEndpointProvisioner;
    }
 
    internal IReadOnlyList<ContainerVolume> PrepareContainerVolumes()
    {
        var volumeNames = _model.GetContainerResources()
            .SelectMany(resource => resource.Annotations.OfType<ContainerMountAnnotation>())
            .Where(mount => mount.Type == ContainerMountType.Volume && !string.IsNullOrEmpty(mount.Source))
            .Select(mount => mount.Source!)
            .Distinct(StringComparer.Ordinal);
 
        var volumes = volumeNames
            .Select(volumeName => ContainerVolume.Create(DcpNameGenerator.GetContainerVolumeName(volumeName), volumeName))
            .ToArray();
 
        _appResources.AddRange(volumes.Select(volume => new AppResource<ContainerVolume>(volume)));
 
        return volumes;
    }
 
    public IEnumerable<RenderedModelResource<Container>> PrepareObjects()
    {
        var modelContainerResources = _model.GetContainerResources().ToArray();
        _containerNetworkEndpointProvisioner.ValidateContainerNameConflicts(modelContainerResources);
 
        var result = new List<RenderedModelResource<Container>>();
 
        foreach (var container in modelContainerResources)
        {
            if (!container.TryGetContainerImageName(out var containerImageName))
            {
                throw new InvalidOperationException();
            }
 
            EnsureRequiredAnnotations(container);
 
            var containerObjectInstance = DcpExecutor.GetDcpInstance(container, instanceIndex: 0);
            var ctr = Container.Create(containerObjectInstance.Name, containerImageName);
 
            ctr.Spec.ContainerName = containerObjectInstance.Name;
 
            if (container.GetLifetimeType() == Lifetime.Persistent)
            {
                ctr.Spec.Persistent = true;
                ApplyMonitorProcess(container, ctr.Spec);
            }
 
            if (container.TryGetContainerImagePullPolicy(out var pullPolicy))
            {
                ctr.Spec.PullPolicy = pullPolicy switch
                {
                    ImagePullPolicy.Default => null,
                    ImagePullPolicy.Always => ContainerPullPolicy.Always,
                    ImagePullPolicy.Missing => ContainerPullPolicy.Missing,
                    ImagePullPolicy.Never => ContainerPullPolicy.Never,
                    _ => throw new InvalidOperationException($"Unknown pull policy '{Enum.GetName(typeof(ImagePullPolicy), pullPolicy)}' for container '{container.Name}'")
                };
            }
 
            ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name);
            ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name);
            ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, container.GetOtelServiceInstanceId(containerObjectInstance));
            DcpExecutor.SetInitialResourceState(container, ctr);
 
            var aanns = container.Annotations.OfType<ContainerNetworkAliasAnnotation>().ToImmutableArray();
            if (aanns.Any(a => a.Network != KnownNetworkIdentifiers.DefaultAspireContainerNetwork))
            {
                throw new InvalidOperationException("Custom container networks are not supported yet.");
            }
 
            ctr.Spec.Networks = new List<ContainerNetworkConnection>
            {
                new ContainerNetworkConnection
                {
                    Name = KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value,
                    Aliases = aanns.Select(a => a.Alias)
                                .Prepend($"{container.Name}.dev.internal")
                                .Prepend(container.Name)
                                .ToList()
                }
            };
 
            if (container.TryGetLastAnnotation<ExplicitStartupAnnotation>(out _))
            {
                ctr.Spec.Start = false;
            }
 
            var containerAppResource = new RenderedModelResource<Container>(container, ctr);
            DcpModelUtilities.AddServicesProducedInfo(containerAppResource, _appResources.Get());
            _appResources.Add(containerAppResource);
            result.Add(containerAppResource);
        }
 
        return result;
    }
 
    private static void ApplyMonitorProcess(IResource resource, ContainerSpec spec)
    {
        if (resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp))
        {
            spec.MonitorPid = parentProcessId;
            spec.MonitorTimestamp = parentProcessTimestamp;
        }
    }
 
    public bool IsReadyToCreate(RenderedModelResource<Container> resource, ContainerNetworkEndpointContext context)
    {
        return !DcpModelUtilities.ShouldDeferCreateForExplicitStart(resource.ModelResource, resource.DcpResource.Spec.Start);
    }
 
    public async Task CreateObjectAsync(RenderedModelResource<Container> cr, ContainerNetworkEndpointContext context, ILogger logger, IDcpObjectFactory factory, CancellationToken cancellationToken)
    {
        var hostDependencies = (await GetHostDependenciesAsync(cr.ModelResource, cancellationToken).ConfigureAwait(false)).ToImmutableArray();
 
        if (hostDependencies.Any())
        {
            await CreateHostDependentContainerAsync(cr, hostDependencies, context, factory, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            await BuildAndCreateContainerAsync(cr, logger, factory, cancellationToken).ConfigureAwait(false);
        }
    }
 
    internal void PrepareContainerExecutables()
    {
        var modelContainerExecutableResources = _model.GetContainerExecutableResources();
        foreach (var containerExecutable in modelContainerExecutableResources)
        {
            EnsureRequiredAnnotations(containerExecutable);
            var exeInstance = DcpExecutor.GetDcpInstance(containerExecutable, instanceIndex: 0);
 
            var containerDcpName = containerExecutable.TargetContainerResource!.GetResolvedResourceName();
 
            var containerExec = ContainerExec.Create(
                name: exeInstance.Name,
                containerName: containerDcpName,
                command: containerExecutable.Command,
                args: containerExecutable.Args?.ToList(),
                workingDirectory: containerExecutable.WorkingDirectory);
 
            containerExec.Annotate(CustomResource.OtelServiceNameAnnotation, containerExecutable.Name);
            containerExec.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, containerExecutable.GetOtelServiceInstanceId(exeInstance));
            containerExec.Annotate(CustomResource.ResourceNameAnnotation, containerExecutable.Name);
            DcpExecutor.SetInitialResourceState(containerExecutable, containerExec);
 
            var exeAppResource = new RenderedModelResource<ContainerExec>(containerExecutable, containerExec);
            _appResources.Add(exeAppResource);
        }
    }
 
    private async Task BuildAndCreateContainerAsync(RenderedModelResource<Container> cr, ILogger logger, IDcpObjectFactory factory, CancellationToken cToken)
    {
        cToken.ThrowIfCancellationRequested();
 
        var dcpContainer = cr.DcpResource;
        var modelContainer = cr.ModelResource;
 
        await ApplyBuildArgumentsAsync(dcpContainer, cr.ModelResource, _executionContext, logger, cToken).ConfigureAwait(false);
 
        var spec = dcpContainer.Spec;
 
        spec.VolumeMounts = BuildContainerMounts(cr.ModelResource);
 
        var (runArgs, failedToApplyRunArgs) = await BuildRunArgsAsync(logger, cr.ModelResource, cToken).ConfigureAwait(false);
        if (failedToApplyRunArgs)
        {
            throw new FailedToApplyEnvironmentException();
        }
        spec.RunArgs = runArgs;
 
        var (configuration, pemCertificates, createFiles) = await BuildContainerConfiguration(cr, logger, cToken).ConfigureAwait(false);
 
        if (configuration.Exception is not null)
        {
            throw new FailedToApplyEnvironmentException($"Failed to apply configuration to container {cr.ModelResource.Name}", configuration.Exception);
        }
 
        // Environment callbacks can resolve proxyless endpoint ports and commit a fallback host port,
        // so build ports afterward.
        if (cr.ServicesProduced.Count > 0)
        {
            spec.Ports = BuildContainerPorts(cr);
        }
 
        var args = configuration.Arguments.ToList();
        if (modelContainer is ContainerResource { ShellExecution: true })
        {
            spec.Args = ["-c", $"{string.Join(' ', args.Select(a => a.Value))}"];
        }
        else
        {
            spec.Args = args.Select(a => a.Value).ToList();
        }
 
        var appLaunchArgumentAnnotations = modelContainer is ContainerResource { ShellExecution: true }
            ? args.Select(a => new AppLaunchArgumentAnnotation(a.Value, isSensitive: a.IsSensitive))
            : args.Select((a, index) => new AppLaunchArgumentAnnotation(a.Value, isSensitive: a.IsSensitive, effectiveArgumentIndex: index));
        dcpContainer.SetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, appLaunchArgumentAnnotations);
 
        spec.Env = configuration.EnvironmentVariables.Select(kvp => new EnvVar { Name = kvp.Key, Value = kvp.Value }).ToList();
        spec.CreateFiles = createFiles;
        if (modelContainer is ContainerResource containerResource)
        {
            spec.Command = containerResource.Entrypoint;
        }
        spec.PemCertificates = pemCertificates;
 
        // Configure the terminal spec if the resource has a TerminalAnnotation.
        // Containers are always single-replica, so we use the host at index 0
        // (TerminalAnnotation always has at least one entry). PTY allocation
        // is implemented by DCP for Windows (ConPTY), Linux, and macOS
        // (Unix98 /dev/ptmx); the container runtime CLI's `attach` command
        // is what actually gets PTY-attached, so behaviour is uniform across
        // hosts that support docker/podman.
        if (modelContainer.TryGetAnnotationsOfType<TerminalAnnotation>(out var terminalAnnotations))
        {
            var terminalAnnotation = terminalAnnotations.FirstOrDefault();
            if (terminalAnnotation is not null)
            {
                if (terminalAnnotation.TerminalHosts.Count > 0)
                {
                    spec.Terminal = new TerminalSpec
                    {
                        UdsPath = terminalAnnotation.TerminalHosts[0].Layout.ProducerUdsPath,
                        // The Aspire terminal host owns the listener at UdsPath; DCP must dial it.
                        SocketMode = "connect",
                        Cols = terminalAnnotation.Options.Columns,
                        Rows = terminalAnnotation.Options.Rows
                    };
                }
                else
                {
                    logger.LogWarning(
                        "Could not determine a producer UDS path for container resource '{ResourceName}'; terminal will not be attached.",
                        modelContainer.Name);
                }
            }
        }
 
        var dcpInfo = await _dcpDependencyCheckService.GetDcpInfoAsync(cancellationToken: cToken).ConfigureAwait(false);
        if (dcpInfo is not null)
        {
            DcpDependencyCheck.CheckDcpInfoAndLogErrors(logger, _options.Value, dcpInfo);
        }
 
        await factory.CreateDcpObjectsAsync(new[] { dcpContainer }, cToken).ConfigureAwait(false);
 
        var containerExes = _appResources.Get().OfType<RenderedModelResource<ContainerExec>>().Where(ar => ar.DcpResource.Spec.ContainerName == dcpContainer.Metadata.Name).ToArray();
        if (containerExes.Length > 0)
        {
            IObjectCreator<ContainerExec, EmptyCreationContext> containerExecCreator = this;
            await factory.CreateRenderedResourcesAsync(containerExecCreator, containerExes, EmptyCreationContext.s_instance, cToken).ConfigureAwait(false);
        }
    }
 
    bool IObjectCreator<ContainerExec, EmptyCreationContext>.IsReadyToCreate(RenderedModelResource<ContainerExec> resource, EmptyCreationContext context)
        => true;
 
    async Task IObjectCreator<ContainerExec, EmptyCreationContext>.CreateObjectAsync(RenderedModelResource<ContainerExec> er, EmptyCreationContext context, ILogger _, IDcpObjectFactory factory, CancellationToken cancellationToken)
    {
        if (er.DcpResource is not ContainerExec containerExe)
        {
            throw new InvalidOperationException($"Expected an {nameof(ContainerExec)} resource, but got {er.DcpResourceKind} instead");
        }
 
        await factory.CreateDcpObjectsAsync([containerExe], cancellationToken).ConfigureAwait(false);
    }
 
    internal async Task<IEnumerable<HostResourceWithEndpoints>> GetHostDependenciesAsync(IResource resource, CancellationToken cancellationToken)
    {
        var allDependencies = await ResourceExtensions.GetResourceDependenciesAsync(
            resource,
            _executionContext,
            new ResourceDependencyDiscoveryOptions
            {
                DiscoveryMode = ResourceDependencyDiscoveryMode.DirectOnly,
                CacheAnnotationCallbackResults = true
            },
            cancellationToken
        ).ConfigureAwait(false);
 
        List<HostResourceWithEndpoints> hostDependencies = [.. allDependencies.Select(HostResourceWithEndpoints.Create).OfType<HostResourceWithEndpoints>()];
 
        if (resource.TryGetAnnotationsOfType<OtlpExporterAnnotation>(out _))
        {
            if (_model.Resources.TryGetByName(KnownResourceNames.AspireDashboard, out var dashboardResource)
                && HostResourceWithEndpoints.Create(dashboardResource) is HostResourceWithEndpoints dashboard)
            {
                hostDependencies.Add(dashboard);
            }
        }
 
        return hostDependencies;
    }
 
    internal async Task CreateHostDependentContainerAsync(RenderedModelResource<Container> cr, ImmutableArray<HostResourceWithEndpoints> hostDependencies, ContainerNetworkEndpointContext context, IDcpObjectFactory factory, CancellationToken cToken)
    {
        cToken.ThrowIfCancellationRequested();
 
        await _containerNetworkEndpointProvisioner
            .EnsureEndpointsAsync(hostDependencies, context, factory, cToken)
            .ConfigureAwait(false);
 
        var hostEndpointAllocatedTasks = hostDependencies
            .SelectMany(h => h.Endpoints)
            .Where(endpoint => endpoint.Protocol == ProtocolType.Tcp)
            .Select(e => e.AllAllocatedEndpoints.GetAllocatedEndpointAsync(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, cToken))
            .ToArray();
        await Task.WhenAll(hostEndpointAllocatedTasks).ConfigureAwait(false);
 
        await BuildAndCreateContainerAsync(cr, _loggerService.GetLogger(cr.ModelResource), factory, cToken).ConfigureAwait(false);
    }
 
    private async Task<(IExecutionConfigurationResult, ContainerPemCertificates?, List<ContainerCreateFileSystem>?)>
    BuildContainerConfiguration(RenderedModelResource<Container> cr, ILogger resourceLogger, CancellationToken cancellationToken)
    {
        var certificatesDestination = ContainerCertificatePathsAnnotation.DefaultCustomCertificatesDestination;
        var bundlePaths = ContainerCertificatePathsAnnotation.DefaultCertificateBundlePaths.ToList();
        var certificateDirsPaths = ContainerCertificatePathsAnnotation.DefaultCertificateDirectoriesPaths.ToList();
 
        if (cr.ModelResource.TryGetLastAnnotation<ContainerCertificatePathsAnnotation>(out var pathsAnnotation))
        {
            certificatesDestination = pathsAnnotation.CustomCertificatesDestination ?? certificatesDestination;
            bundlePaths = pathsAnnotation.DefaultCertificateBundles ?? bundlePaths;
            certificateDirsPaths = pathsAnnotation.DefaultCertificateDirectories ?? certificateDirsPaths;
        }
 
        var serverAuthCertificatesBasePath = $"{certificatesDestination}/private";
 
        var configuration = await ExecutionConfigurationBuilder.Create(cr.ModelResource)
            .WithArgumentsConfig()
            .WithEnvironmentVariablesConfig()
            .WithCertificateTrustConfig(scope =>
            {
                var dirs = new List<string> { certificatesDestination + "/certs" };
                if (scope == CertificateTrustScope.Append)
                {
                    dirs.AddRange(certificateDirsPaths!);
                }
 
                return new()
                {
                    CertificateBundlePath = ReferenceExpression.Create($"{certificatesDestination}/cert.pem"),
                    CertificateDirectoriesPath = ReferenceExpression.Create($"{string.Join(':', dirs)}"),
                    RootCertificatesPath = certificatesDestination,
                    IsContainer = true,
                };
            })
            .WithHttpsCertificateConfig(cert => new()
            {
                CertificatePath = ReferenceExpression.Create($"{serverAuthCertificatesBasePath}/{cert.Thumbprint}.crt"),
                KeyPath = ReferenceExpression.Create($"{serverAuthCertificatesBasePath}/{cert.Thumbprint}.key"),
                CertificateWithKeyPath = ReferenceExpression.Create($"{serverAuthCertificatesBasePath}/{cert.Thumbprint}.pem"),
                PfxPath = ReferenceExpression.Create($"{serverAuthCertificatesBasePath}/{cert.Thumbprint}.pfx"),
            })
            .BuildAsync(_executionContext, resourceLogger, cancellationToken)
            .ConfigureAwait(false);
 
        List<ContainerFileSystemEntry> customBundleFiles = new();
 
        ContainerPemCertificates? pemCertificates = null;
        if (configuration.TryGetAdditionalData<CertificateTrustExecutionConfigurationData>(out var certificateTrustConfiguration)
            && certificateTrustConfiguration.Scope != CertificateTrustScope.None
            && certificateTrustConfiguration.Certificates.Count > 0)
        {
            pemCertificates = new ContainerPemCertificates
            {
                Certificates = CertificateUtilities.BuildPemCertificateList(certificateTrustConfiguration.Certificates),
                Destination = certificatesDestination,
                ContinueOnError = true,
            };
 
            if (certificateTrustConfiguration.Scope != CertificateTrustScope.Append)
            {
                pemCertificates.OverwriteBundlePaths = bundlePaths;
            }
 
            foreach (var bundleFactory in certificateTrustConfiguration.CustomBundlesFactories)
            {
                var bundleId = bundleFactory.Key;
                var bundleBytes = await bundleFactory.Value(certificateTrustConfiguration.Certificates, cancellationToken).ConfigureAwait(false);
 
                customBundleFiles.Add(new ContainerFileSystemEntry
                {
                    Name = bundleId,
                    Type = ContainerFileSystemEntryType.File,
                    RawContents = Convert.ToBase64String(bundleBytes),
                });
            }
        }
 
        var buildCreateFilesContext = new BuildCreateFilesContext
        {
            Resource = cr.ModelResource,
            CertificateTrustScope = certificateTrustConfiguration?.Scope ?? CertificateTrustScope.None,
            CertificateTrustBundlePath = $"{certificatesDestination}/cert.pem",
        };
 
        if (configuration.TryGetAdditionalData<HttpsCertificateExecutionConfigurationData>(out var tlsCertificateConfiguration))
        {
            var thumbprint = tlsCertificateConfiguration.Certificate.Thumbprint;
            buildCreateFilesContext.HttpsCertificateContext = new ContainerFileSystemCallbackHttpsCertificateContext
            {
                CertificatePath = ReferenceExpression.Create($"{serverAuthCertificatesBasePath}/{thumbprint}.crt"),
                KeyPath = tlsCertificateConfiguration.KeyPathReference,
                CertificateWithKeyPath = tlsCertificateConfiguration.CertificateWithKeyPathReference,
                PfxPath = tlsCertificateConfiguration.PfxPathReference,
                Password = tlsCertificateConfiguration.Password,
            };
        }
 
        var createFiles = await BuildCreateFilesAsync(buildCreateFilesContext, cancellationToken).ConfigureAwait(false);
 
        if (customBundleFiles.Count > 0)
        {
            createFiles.Add(new ContainerCreateFileSystem
            {
                Destination = certificatesDestination,
                Entries = [
                    new ContainerFileSystemEntry
                    {
                        Name = "bundles",
                        Type = ContainerFileSystemEntryType.Directory,
                        Entries = customBundleFiles,
                    },
                ],
            });
        }
 
        if (tlsCertificateConfiguration is not null)
        {
            var thumbprint = tlsCertificateConfiguration.Certificate.Thumbprint;
            var publicCertificatePem = tlsCertificateConfiguration.Certificate.ExportCertificatePem();
            (var keyPem, var pfxBytes) = await DeveloperCertificateService.GetKeyMaterialAsync(
                certificate: tlsCertificateConfiguration.Certificate,
                password: tlsCertificateConfiguration.Password,
                needKeyPem: tlsCertificateConfiguration.IsKeyPathReferenced || tlsCertificateConfiguration.IsCertificateWithKeyPathReferenced,
                needPfx: tlsCertificateConfiguration.IsPfxPathReferenced,
                cancellationToken
            ).ConfigureAwait(false);
 
            var certificateFiles = new List<ContainerFileSystemEntry>()
            {
                new ContainerFileSystemEntry
                {
                    Name = thumbprint + ".crt",
                    Type = ContainerFileSystemEntryType.File,
                    Contents = new string(publicCertificatePem),
                },
            };
 
            if (keyPem is not null)
            {
                certificateFiles.Add(new ContainerFileSystemEntry
                {
                    Name = thumbprint + ".key",
                    Type = ContainerFileSystemEntryType.File,
                    Contents = new string(keyPem),
                });
 
                if (tlsCertificateConfiguration.IsCertificateWithKeyPathReferenced)
                {
                    certificateFiles.Add(new ContainerFileSystemEntry
                    {
                        Name = thumbprint + ".pem",
                        Type = ContainerFileSystemEntryType.File,
                        Contents = new string([.. keyPem, '\n', .. publicCertificatePem]),
                    });
                }
 
                Array.Clear(keyPem, 0, keyPem.Length);
            }
 
            if (pfxBytes is not null)
            {
                certificateFiles.Add(new ContainerFileSystemEntry
                {
                    Name = thumbprint + ".pfx",
                    Type = ContainerFileSystemEntryType.File,
                    RawContents = Convert.ToBase64String(pfxBytes),
                });
 
                Array.Clear(pfxBytes, 0, pfxBytes.Length);
            }
 
            createFiles.Add(new ContainerCreateFileSystem
            {
                Destination = serverAuthCertificatesBasePath,
                Entries = certificateFiles,
            });
        }
 
        return (configuration, pemCertificates, createFiles);
    }
 
    private async Task<List<ContainerCreateFileSystem>> BuildCreateFilesAsync(BuildCreateFilesContext context, CancellationToken cancellationToken)
    {
        var createFiles = new List<ContainerCreateFileSystem>();
 
        if (context.Resource.TryGetAnnotationsOfType<ContainerFileSystemCallbackAnnotation>(out var createFileAnnotations))
        {
            foreach (var a in createFileAnnotations)
            {
                var entries = await a.Callback(
                    new()
                    {
                        Model = context.Resource,
                        Services = _executionContext.Services,
                        HttpsCertificateContext = context.HttpsCertificateContext,
                    },
                    cancellationToken).ConfigureAwait(false);
 
                if (entries?.Any() != true)
                {
                    continue;
                }
 
                createFiles.Add(new ContainerCreateFileSystem
                {
                    Destination = a.DestinationPath,
                    DefaultOwner = a.DefaultOwner,
                    DefaultGroup = a.DefaultGroup,
                    Umask = (int?)a.Umask,
                    Entries = entries.Select(e => e.ToContainerFileSystemEntry()).ToList(),
                });
            }
        }
 
        return createFiles;
    }
 
    private async Task<(List<string>, bool)> BuildRunArgsAsync(ILogger resourceLogger, IResource modelResource, CancellationToken cancellationToken)
    {
        var failedToApplyArgs = false;
        var runArgs = new List<string>();
 
        await modelResource.ProcessContainerRuntimeArgValues(
            _executionContext,
            (a, ex) =>
            {
                if (ex is not null)
                {
                    failedToApplyArgs = true;
                    resourceLogger.LogCritical(ex, "Failed to apply argument value '{ArgKey}'. A dependency may have failed to start.", a);
                    _logger.LogDebug(ex, "Failed to apply argument value '{ArgKey}' to '{ResourceName}'. A dependency may have failed to start.", a, modelResource.Name);
                }
                else if (a is string s)
                {
                    runArgs.Add(s);
                }
            },
            resourceLogger,
            cancellationToken).ConfigureAwait(false);
 
        return (runArgs, failedToApplyArgs);
    }
 
    private static async Task ApplyBuildArgumentsAsync(Container dcpContainerResource, IResource modelContainerResource, DistributedApplicationExecutionContext executionContext, ILogger logger, CancellationToken cancellationToken)
    {
        if (modelContainerResource.Annotations.OfType<DockerfileBuildAnnotation>().SingleOrDefault() is { } dockerfileBuildAnnotation)
        {
            await DockerfileHelper.ExecuteDockerfileFactoryAsync(dockerfileBuildAnnotation, modelContainerResource, executionContext.Services, cancellationToken).ConfigureAwait(false);
 
            var dcpBuildArgs = new List<EnvVar>();
 
            foreach (var buildArgument in dockerfileBuildAnnotation.BuildArguments)
            {
                var valueString = buildArgument.Value switch
                {
                    string stringValue => stringValue,
                    IValueProvider valueProvider => await valueProvider.GetValueAsync(cancellationToken).ConfigureAwait(false),
                    bool boolValue => boolValue ? "true" : "false",
                    null => null,
                    _ => buildArgument.Value.ToString()
                };
 
                dcpBuildArgs.Add(new EnvVar() { Name = buildArgument.Key, Value = valueString });
            }
 
            var dcpBuildSecrets = new List<BuildContextSecret>();
 
            foreach (var buildSecret in dockerfileBuildAnnotation.BuildSecrets)
            {
                var valueString = buildSecret.Value switch
                {
                    FileInfo filePath => filePath.FullName,
                    IValueProvider valueProvider => await valueProvider.GetValueAsync(cancellationToken).ConfigureAwait(false),
                    _ => throw new InvalidOperationException("Build secret can only be a parameter or a file.")
                };
 
                if (buildSecret.Value is FileInfo)
                {
                    dcpBuildSecrets.Add(new BuildContextSecret { Id = buildSecret.Key, Type = "file", Source = valueString });
                }
                else
                {
                    dcpBuildSecrets.Add(new BuildContextSecret { Id = buildSecret.Key, Type = "env", Value = valueString });
                }
            }
 
            dcpContainerResource.Spec.Build = new()
            {
                Context = dockerfileBuildAnnotation.ContextPath,
                Dockerfile = dockerfileBuildAnnotation.DockerfilePath,
                Stage = dockerfileBuildAnnotation.Stage,
                Args = dcpBuildArgs,
                Secrets = dcpBuildSecrets
            };
 
#pragma warning disable ASPIREPIPELINES003 // ContainerBuildOptions APIs are experimental.
            var buildOptionsContext = await modelContainerResource.ProcessContainerBuildOptionsCallbackAsync(
                executionContext.Services,
                logger,
                executionContext,
                cancellationToken).ConfigureAwait(false);
 
            if (buildOptionsContext.TargetPlatform is { } targetPlatform)
            {
                dcpContainerResource.Spec.Build.Platform = ToDcpPlatformString(targetPlatform);
            }
#pragma warning restore ASPIREPIPELINES003
        }
    }
 
    // Maps the publishing-side ContainerTargetPlatform enum to DCP-native ContainerPlatform string
    // constants. The publishing type is fully qualified so the DCP layer doesn't carry a
    // `using Aspire.Hosting.Publishing` directive.
#pragma warning disable ASPIREPIPELINES003 // ContainerTargetPlatform is experimental.
    private static string ToDcpPlatformString(Publishing.ContainerTargetPlatform platform)
    {
        var parts = new List<string>();
        if (platform.HasFlag(Publishing.ContainerTargetPlatform.LinuxAmd64)) { parts.Add(ContainerPlatform.LinuxAmd64); }
        if (platform.HasFlag(Publishing.ContainerTargetPlatform.LinuxArm64)) { parts.Add(ContainerPlatform.LinuxArm64); }
        if (platform.HasFlag(Publishing.ContainerTargetPlatform.LinuxArm)) { parts.Add(ContainerPlatform.LinuxArm); }
        if (platform.HasFlag(Publishing.ContainerTargetPlatform.Linux386)) { parts.Add(ContainerPlatform.Linux386); }
        if (platform.HasFlag(Publishing.ContainerTargetPlatform.WindowsAmd64)) { parts.Add(ContainerPlatform.WindowsAmd64); }
        if (platform.HasFlag(Publishing.ContainerTargetPlatform.WindowsArm64)) { parts.Add(ContainerPlatform.WindowsArm64); }
 
        if (parts.Count == 0)
        {
            throw new ArgumentOutOfRangeException(nameof(platform), platform, "Unknown container target platform");
        }
 
        return string.Join(",", parts);
    }
#pragma warning restore ASPIREPIPELINES003
 
    private static List<ContainerPortSpec> BuildContainerPorts(RenderedModelResource<Container> cr)
    {
        var ports = new List<ContainerPortSpec>();
 
        foreach (var sp in cr.ServicesProduced)
        {
            var ea = sp.EndpointAnnotation;
 
            var portSpec = new ContainerPortSpec()
            {
                ContainerPort = ea.TargetPort,
            };
 
            if (!ea.IsProxied && ea.SpecifiedPort is int hostPort)
            {
                sp.Service.Spec.Port ??= hostPort;
                portSpec.HostPort = hostPort;
            }
 
            switch (ea.Protocol)
            {
                case ProtocolType.Tcp:
                    portSpec.Protocol = PortProtocol.TCP;
                    break;
                case ProtocolType.Udp:
                    portSpec.Protocol = PortProtocol.UDP;
                    break;
            }
 
            if (ea.TargetHost != KnownHostNames.Localhost)
            {
                portSpec.HostIP = ea.TargetHost;
            }
 
            ports.Add(portSpec);
        }
 
        return ports;
    }
 
    private static List<VolumeMount> BuildContainerMounts(IResource container)
    {
        var volumeMounts = new List<VolumeMount>();
 
        if (container.TryGetContainerMounts(out var containerMounts))
        {
            foreach (var mount in containerMounts)
            {
                volumeMounts.Add(new VolumeMount
                {
                    Source = mount.Source,
                    Target = mount.Target,
                    Type = mount.Type == ContainerMountType.BindMount ? VolumeMountType.Bind : VolumeMountType.Volume,
                    IsReadOnly = mount.IsReadOnly
                });
            }
        }
 
        return volumeMounts;
    }
 
    private void EnsureRequiredAnnotations(IResource resource)
    {
        resource.AddLifeCycleCommands();
        _nameGenerator.EnsureDcpInstancesPopulated(resource);
    }
 
    private class BuildCreateFilesContext
    {
        public required IResource Resource { get; init; }
        public CertificateTrustScope CertificateTrustScope { get; init; }
        public string? CertificateTrustBundlePath { get; set; }
        public string? CertificateTrustDirectoriesPath { get; set; }
        public ContainerFileSystemCallbackHttpsCertificateContext? HttpsCertificateContext { get; set; }
    }
}