Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"appHostPath": "../Publishers.AppHost.csproj"
}
22 changes: 6 additions & 16 deletions playground/publishers/Publishers.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
// 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 ASPIRECOMPUTE001
#pragma warning disable ASPIREAZURE001
#pragma warning disable ASPIREPUBLISHERS001

var builder = DistributedApplication.CreateBuilder(args);

if (builder.ExecutionContext.PublisherName == "azure" ||
builder.ExecutionContext.IsInspectMode)
IResourceBuilder<IComputeEnvironmentResource> environment = builder.Configuration["Deployment:Target"] switch
{
builder.AddAzureContainerAppEnvironment("env");
}

if (builder.ExecutionContext.PublisherName == "docker-compose" ||
builder.ExecutionContext.IsInspectMode)
{
builder.AddDockerComposeEnvironment("docker-env");
}

if (builder.ExecutionContext.PublisherName == "kubernetes" ||
builder.ExecutionContext.IsInspectMode)
{
builder.AddKubernetesEnvironment("k8s-env");
}
"k8s" or "kube" => builder.AddKubernetesEnvironment("env"),
"aca" or "azure" => builder.AddAzureContainerAppEnvironment("env"),
_ => builder.AddDockerComposeEnvironment("env"),
};

var param0 = builder.AddParameter("param0");
var param1 = builder.AddParameter("param1", secret: true);
Expand Down
3 changes: 3 additions & 0 deletions playground/publishers/Publishers.AppHost/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"Aspire.Hosting.Dcp": "Warning",
"Aspire.Hosting": "Information"
}
},
"Deployment": {
"Target": "kube"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public static IResourceBuilder<DockerComposeEnvironmentResource> AddDockerCompos

var resource = new DockerComposeEnvironmentResource(name);
builder.Services.TryAddLifecycleHook<DockerComposeInfrastructure>();
builder.AddDockerComposePublisher();
if (builder.ExecutionContext.IsRunMode)
{

Expand Down
28 changes: 26 additions & 2 deletions src/Aspire.Hosting.Docker/DockerComposeEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Docker.Resources;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting.Docker;

Expand All @@ -14,8 +16,7 @@ namespace Aspire.Hosting.Docker;
/// <remarks>
/// Initializes a new instance of the <see cref="DockerComposeEnvironmentResource"/> class.
/// </remarks>
/// <param name="name">The name of the Docker Compose environment.</param>
public class DockerComposeEnvironmentResource(string name) : Resource(name), IComputeEnvironmentResource
public class DockerComposeEnvironmentResource : Resource, IComputeEnvironmentResource
{
/// <summary>
/// The container registry to use.
Expand All @@ -34,4 +35,27 @@ public class DockerComposeEnvironmentResource(string name) : Resource(name), ICo
/// These will be populated into a top-level .env file adjacent to the Docker Compose file.
/// </summary>
internal Dictionary<string, (string Description, string? DefaultValue)> CapturedEnvironmentVariables { get; } = [];

/// <param name="name">The name of the Docker Compose environment.</param>
public DockerComposeEnvironmentResource(string name) : base(name)
{
Annotations.Add(new PublishingCallbackAnnotation(PublishAsync));
}

private Task PublishAsync(PublishingContext context)
{
#pragma warning disable ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
var imageBuilder = context.Services.GetRequiredService<IResourceContainerImageBuilder>();
#pragma warning restore ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

var publishOptions = new DockerComposePublisherOptions
{
OutputPath = context.OutputPath
};

var dockerComposePublishingContext = new DockerComposePublishingContext(context.ExecutionContext,
publishOptions, imageBuilder, context.Logger, context.CancellationToken);

return dockerComposePublishingContext.WriteModelAsync(context.Model, this);
}
}
31 changes: 18 additions & 13 deletions src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal sealed class DockerComposePublishingContext(
public readonly IResourceContainerImageBuilder ImageBuilder = imageBuilder;
public readonly DockerComposePublisherOptions PublisherOptions = publisherOptions;

internal async Task WriteModelAsync(DistributedApplicationModel model)
internal async Task WriteModelAsync(DistributedApplicationModel model, DockerComposeEnvironmentResource? environmentResource = null)
{
if (!executionContext.IsPublishMode)
{
Expand All @@ -49,26 +49,31 @@ internal async Task WriteModelAsync(DistributedApplicationModel model)
return;
}

await WriteDockerComposeOutputAsync(model).ConfigureAwait(false);
await WriteDockerComposeOutputAsync(model, environmentResource).ConfigureAwait(false);

logger.FinishGeneratingDockerCompose(PublisherOptions.OutputPath);
}

private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel model)
private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel model, DockerComposeEnvironmentResource? environmentResource)
{
var dockerComposeEnvironments = model.Resources.OfType<DockerComposeEnvironmentResource>().ToArray();
var environment = environmentResource;

if (dockerComposeEnvironments.Length > 1)
if (environment is null)
{
throw new NotSupportedException("Multiple Docker Compose environments are not supported.");
}
var dockerComposeEnvironments = model.Resources.OfType<DockerComposeEnvironmentResource>().ToArray();

var environment = dockerComposeEnvironments.FirstOrDefault();
if (dockerComposeEnvironments.Length > 1)
{
throw new NotSupportedException("Multiple Docker Compose environments are not supported.");
}

if (environment == null)
{
// No Docker Compose environment found
throw new InvalidOperationException($"No Docker Compose environment found. Ensure a Docker Compose environment is registered by calling {nameof(DockerComposeEnvironmentExtensions.AddDockerComposeEnvironment)}.");
environment = dockerComposeEnvironments.FirstOrDefault();

if (environment == null)
{
// No Docker Compose environment found
throw new InvalidOperationException($"No Docker Compose environment found. Ensure a Docker Compose environment is registered by calling {nameof(DockerComposeEnvironmentExtensions.AddDockerComposeEnvironment)}.");
}
}

var defaultNetwork = new Network
Expand All @@ -82,7 +87,7 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod

foreach (var resource in model.Resources)
{
if (resource.GetDeploymentTargetAnnotation()?.DeploymentTarget is DockerComposeServiceResource serviceResource)
if (resource.GetDeploymentTargetAnnotation(environment)?.DeploymentTarget is DockerComposeServiceResource serviceResource)
{
if (PublisherOptions.BuildImages)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public static IResourceBuilder<KubernetesEnvironmentResource> AddKubernetesEnvir

var resource = new KubernetesEnvironmentResource(name);
builder.Services.TryAddLifecycleHook<KubernetesInfrastructure>();
builder.AddKubernetesPublisher();
if (builder.ExecutionContext.IsRunMode)
{

Expand Down
19 changes: 17 additions & 2 deletions src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ namespace Aspire.Hosting.Kubernetes;
/// <remarks>
/// Initializes a new instance of the <see cref="KubernetesEnvironmentResource"/> class.
/// </remarks>
/// <param name="name">The name of the Kubernetes environment.</param>
public sealed class KubernetesEnvironmentResource(string name) : Resource(name), IComputeEnvironmentResource
public sealed class KubernetesEnvironmentResource : Resource, IComputeEnvironmentResource
{
/// <summary>
/// Gets or sets the name of the Helm chart to be generated.
Expand Down Expand Up @@ -78,4 +77,20 @@ public sealed class KubernetesEnvironmentResource(string name) : Resource(name),
/// (e.g., ClusterIP, NodePort, LoadBalancer) created in Kubernetes for the application.
/// </remarks>
public string DefaultServiceType { get; set; } = "ClusterIP";

/// <param name="name">The name of the Kubernetes environment.</param>
public KubernetesEnvironmentResource(string name) : base(name)
{
Annotations.Add(new PublishingCallbackAnnotation(PublishAsync));
}

private Task PublishAsync(PublishingContext context)
{
var publisherOptions = new KubernetesPublisherOptions()
{
OutputPath = context.OutputPath
};
var kubernetesContext = new KubernetesPublishingContext(context.ExecutionContext, publisherOptions, context.Logger, context.CancellationToken);
return kubernetesContext.WriteModelAsync(context.Model, this);
}
}
31 changes: 18 additions & 13 deletions src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal sealed class KubernetesPublishingContext(

public ILogger Logger => logger;

internal async Task WriteModelAsync(DistributedApplicationModel model)
internal async Task WriteModelAsync(DistributedApplicationModel model, KubernetesEnvironmentResource? environmentResource = null)
{
if (!executionContext.IsPublishMode)
{
Expand All @@ -58,31 +58,36 @@ internal async Task WriteModelAsync(DistributedApplicationModel model)
return;
}

await WriteKubernetesOutputAsync(model).ConfigureAwait(false);
await WriteKubernetesOutputAsync(model, environmentResource).ConfigureAwait(false);

logger.FinishGeneratingKubernetes(publisherOptions.OutputPath);
}

private async Task WriteKubernetesOutputAsync(DistributedApplicationModel model)
private async Task WriteKubernetesOutputAsync(DistributedApplicationModel model, KubernetesEnvironmentResource? environmentResource)
{
var kubernetesEnvironments = model.Resources.OfType<KubernetesEnvironmentResource>().ToArray();
var environment = environmentResource;

if (kubernetesEnvironments.Length > 1)
if (environment is null)
{
throw new NotSupportedException("Multiple Kubernetes environments are not supported.");
}
var kubernetesEnvironments = model.Resources.OfType<KubernetesEnvironmentResource>().ToArray();

var environment = kubernetesEnvironments.FirstOrDefault();
if (kubernetesEnvironments.Length > 1)
{
throw new NotSupportedException("Multiple Kubernetes environments are not supported.");
}

if (environment == null)
{
// No Kubernetes environment found
throw new InvalidOperationException($"No Kubernetes environment found. Ensure a Kubernetes environment is registered by calling {nameof(KubernetesEnvironmentExtensions.AddKubernetesEnvironment)}.");
environment = kubernetesEnvironments.FirstOrDefault();

if (environment == null)
{
// No Kubernetes environment found
throw new InvalidOperationException($"No Kubernetes environment found. Ensure a Kubernetes environment is registered by calling {nameof(KubernetesEnvironmentExtensions.AddKubernetesEnvironment)}.");
}
}

foreach (var resource in model.Resources)
{
if (resource.GetDeploymentTargetAnnotation()?.DeploymentTarget is KubernetesResource serviceResource)
if (resource.GetDeploymentTargetAnnotation(environment)?.DeploymentTarget is KubernetesResource serviceResource)
{
if (serviceResource.TargetResource.TryGetAnnotationsOfType<KubernetesServiceCustomizationAnnotation>(out var annotations))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a default publishing callback annotation for a distributed application model.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="PublishingCallbackAnnotation"/> class.
/// </remarks>
/// <param name="callback">The publishing callback.</param>
public sealed class PublishingCallbackAnnotation(Func<PublishingContext, Task> callback) : IResourceAnnotation
{
/// <summary>
/// The publishing callback.
/// </summary>
public Func<PublishingContext, Task> Callback { get; } = callback ?? throw new ArgumentNullException(nameof(callback));
}
46 changes: 26 additions & 20 deletions src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,35 +575,41 @@ public static int GetReplicaCount(this IResource resource)
/// Gets the deployment target for the specified resource, if any. Throws an exception if
/// there are multiple compute environments and a compute environment is not explicitly specified.
/// </summary>
public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource)
{
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource, IComputeEnvironmentResource? computeEnvironmentResource = null)
{
if (resource.TryGetLastAnnotation<ComputeEnvironmentAnnotation>(out var computeEnvironmentAnnotation))
{
// find the deployment target for the compute environment
return resource.Annotations
.OfType<DeploymentTargetAnnotation>()
.LastOrDefault(a => a.ComputeEnvironment == computeEnvironmentAnnotation.ComputeEnvironment);
// If you have a ComputeEnvironmentAnnotation, it means the resource is bound to a specific compute environment.
// Skip the annotation if it doesn't match the specified computeEnvironmentResource.
if (computeEnvironmentResource is not null && computeEnvironmentResource != computeEnvironmentAnnotation.ComputeEnvironment)
{
return null;
}

// If the resource has a ComputeEnvironmentAnnotation, use it to get the compute environment.
// This wins over the specified computeEnvironmentResource
computeEnvironmentResource = computeEnvironmentAnnotation.ComputeEnvironment;
}
else

if (resource.TryGetAnnotationsOfType<DeploymentTargetAnnotation>(out var deploymentTargetAnnotations))
{
DeploymentTargetAnnotation? result = null;
var computeEnvironments = resource.Annotations.OfType<DeploymentTargetAnnotation>();
foreach (var annotation in computeEnvironments)
var annotations = deploymentTargetAnnotations.ToArray();

if (computeEnvironmentResource is not null)
{
if (result is null)
{
result = annotation;
}
else
{
var computeEnvironmentNames = string.Join(", ", computeEnvironments.Select(a => a.ComputeEnvironment?.Name));
throw new InvalidOperationException($"Resource '{resource.Name}' has multiple compute environments - '{computeEnvironmentNames}'. Please specify a single compute environment using 'WithComputeEnvironment'.");
}
return annotations.SingleOrDefault(a => a.ComputeEnvironment == computeEnvironmentResource);
}

if (annotations.Length > 1)
{
var computeEnvironmentNames = string.Join(", ", annotations.Select(a => a.ComputeEnvironment?.Name));
throw new InvalidOperationException($"Resource '{resource.Name}' has multiple compute environments - '{computeEnvironmentNames}'. Please specify a single compute environment using 'WithComputeEnvironment'.");
}

return result;
return annotations[0];
}
return null;
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
}

Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
// Publishing support
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.MutateHttp2TransportAsync);
this.AddPublisher<ManifestPublisher, PublishingOptions>("manifest");
this.AddPublisher<Publisher, PublishingOptions>("default");
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, DockerContainerRuntime>("docker");
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, PodmanContainerRuntime>("podman");
_innerBuilder.Services.AddSingleton<IResourceContainerImageBuilder, ResourceContainerImageBuilder>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static IDistributedApplicationBuilder AddPublisher<TPublisher, TPublisher

if (configureOptions is not null)
{
builder.Services.Configure("name", configureOptions);
builder.Services.Configure(name, configureOptions);
}

builder.Services.Configure<TPublisherOptions>(name, builder.Configuration.GetSection(nameof(PublishingOptions.Publishing)));
Expand Down
28 changes: 28 additions & 0 deletions src/Aspire.Hosting/Publishing/Publisher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Publishing;

internal class Publisher(
ILogger<Publisher> logger,
IOptions<PublishingOptions> options,
DistributedApplicationExecutionContext executionContext,
IServiceProvider serviceProvider) : IDistributedApplicationPublisher
{
public Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
{
if (options.Value.OutputPath == null)
{
throw new DistributedApplicationException(
"The '--output-path [path]' option was not specified."
);
}

var context = new PublishingContext(model, executionContext, serviceProvider, logger, cancellationToken, options.Value.OutputPath);
return context.WriteModelAsync(model);
}
}
Loading