File: DockerComposeEnvironmentContext.cs
Web Access
Project: src\src\Aspire.Hosting.Docker\Aspire.Hosting.Docker.csproj (Aspire.Hosting.Docker)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Docker;
 
internal sealed class DockerComposeEnvironmentContext(DockerComposeEnvironmentResource environment, ILogger logger)
{
    public async Task<DockerComposeServiceResource> CreateDockerComposeServiceResourceAsync(IResource resource, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
    {
        if (environment.ResourceMapping.TryGetValue(resource, out var existingResource))
        {
            return existingResource;
        }
 
        logger.LogInformation("Creating Docker Compose resource for {ResourceName}", resource.Name);
 
        var serviceResource = new DockerComposeServiceResource(resource.Name, resource, environment);
        environment.ResourceMapping[resource] = serviceResource;
 
        // Process endpoints
        ProcessEndpoints(serviceResource);
 
        // Process volumes
        ProcessVolumes(serviceResource);
 
        // Process environment variables
        await ProcessEnvironmentVariablesAsync(serviceResource, executionContext, cancellationToken).ConfigureAwait(false);
 
        // Process command line arguments
        await ProcessArgumentsAsync(serviceResource, executionContext, cancellationToken).ConfigureAwait(false);
 
        return serviceResource;
    }
 
    private void ProcessEndpoints(DockerComposeServiceResource serviceResource)
    {
        var resolvedEndpoints = serviceResource.TargetResource.ResolveEndpoints(environment.PortAllocator);
 
        foreach (var resolved in resolvedEndpoints)
        {
            var endpoint = resolved.Endpoint;
 
            // Convert target port to string for Docker Compose
            // For default ProjectResource endpoints (TargetPort.Value is null), use container port placeholder
            string internalPort = resolved.TargetPort.Value is int port
                ? port.ToString(CultureInfo.InvariantCulture)
                : serviceResource.AsContainerPortPlaceholder();
 
            // Docker Compose can handle dynamic port allocation, so only use exposed port if it was explicitly specified
            // Skip allocated or implicit ports - only use explicit ports
            var exposedPort = (resolved.ExposedPort.IsAllocated || resolved.ExposedPort.IsImplicit) ? null : resolved.ExposedPort.Value;
 
            serviceResource.EndpointMappings.Add(endpoint.Name,
                new(serviceResource.TargetResource,
                    endpoint.UriScheme,
                    serviceResource.TargetResource.Name,
                    internalPort,
                    exposedPort,
                    endpoint.IsExternal,
                    endpoint.Name));
        }
    }
 
    private void ProcessVolumes(DockerComposeServiceResource serviceResource)
    {
        if (!serviceResource.TargetResource.TryGetContainerMounts(out var mounts))
        {
            return;
        }
 
        var bindMountIndex = 0;
 
        foreach (var mount in mounts)
        {
            if (mount.Source is null || mount.Target is null)
            {
                throw new InvalidOperationException("Volume source and target must be set");
            }
 
            var source = mount.Source;
            var name = mount.Source;
 
            // For bind mounts, create environment placeholders for the source path
            // Skip the docker socket which should be left as-is for portability
            if (mount.Type == ContainerMountType.BindMount && !IsDockerSocket(mount.Source))
            {
                // Create environment variable name: {RESOURCE_NAME}_BINDMOUNT_{INDEX}
                var envVarName = $"{serviceResource.Name.ToUpperInvariant().Replace("-", "_").Replace(".", "_")}_BINDMOUNT_{bindMountIndex}";
                bindMountIndex++;
 
                // Add the placeholder to captured environment variables so it gets written to the .env file
                // Use the original source path as the default value and pass the ContainerMountAnnotation as the source
                var placeholder = environment.AddEnvironmentVariable(
                    envVarName,
                    description: $"Bind mount source for {serviceResource.Name}:{mount.Target}",
                    defaultValue: mount.Source,
                    source: mount,
                    resource: serviceResource.TargetResource);
 
                // Log warning about host-specific path
                logger.BindMountHostSpecificPath(serviceResource.Name, mount.Source, envVarName);
 
                // Use the placeholder in the compose file
                source = placeholder;
                name = envVarName;
            }
 
            serviceResource.Volumes.Add(new Resources.ServiceNodes.Volume
            {
                Name = name,
                Source = source,
                Target = mount.Target,
                Type = mount.Type == ContainerMountType.BindMount ? "bind" : "volume",
                ReadOnly = mount.IsReadOnly
            });
        }
    }
 
    /// <summary>
    /// Checks if the source path is the Docker socket path.
    /// </summary>
    private static bool IsDockerSocket(string source)
    {
        // Check for common Docker socket paths across different platforms
        return source.Equals("/var/run/docker.sock", StringComparison.OrdinalIgnoreCase) ||
               source.Equals("//var/run/docker.sock", StringComparison.OrdinalIgnoreCase) ||  // WSL-style path
               source.Equals(@"\\.\pipe\docker_engine", StringComparison.OrdinalIgnoreCase);  // Windows named pipe
    }
 
    private static async Task ProcessEnvironmentVariablesAsync(DockerComposeServiceResource serviceResource, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
    {
        if (serviceResource.TargetResource.TryGetAnnotationsOfType<EnvironmentCallbackAnnotation>(out var environmentCallbacks))
        {
            var context = new EnvironmentCallbackContext(
                executionContext,
                serviceResource.TargetResource,
                serviceResource.EnvironmentVariables,
                cancellationToken: cancellationToken);
 
            foreach (var callback in environmentCallbacks)
            {
                await callback.Callback(context).ConfigureAwait(false);
            }
 
            // Remove HTTPS service discovery variables as Docker Compose doesn't handle certificates
            RemoveHttpsServiceDiscoveryVariables(context.EnvironmentVariables);
        }
    }
 
    private static void RemoveHttpsServiceDiscoveryVariables(Dictionary<string, object> environmentVariables)
    {
        var keysToRemove = environmentVariables
            .Where(kvp => kvp.Value is EndpointReference epRef && epRef.Scheme == "https" && kvp.Key.StartsWith("services__"))
            .Select(kvp => kvp.Key)
            .ToList();
 
        foreach (var key in keysToRemove)
        {
            environmentVariables.Remove(key);
        }
    }
 
    private static async Task ProcessArgumentsAsync(DockerComposeServiceResource serviceResource, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
    {
        if (serviceResource.TargetResource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var commandLineArgsCallbacks))
        {
            var context = new CommandLineArgsCallbackContext(serviceResource.Args, serviceResource.TargetResource, cancellationToken: cancellationToken)
            {
                ExecutionContext = executionContext
            };
 
            foreach (var callback in commandLineArgsCallbacks)
            {
                await callback.Callback(context).ConfigureAwait(false);
            }
        }
    }
}