Skip to content
Draft
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
Expand Up @@ -214,7 +214,12 @@ private static bool HasPersistedHelmReleaseState(JsonObject deploymentState)
/// <summary>
/// Gets the underlying Kubernetes environment resource used for Helm-based deployment.
/// </summary>
internal KubernetesEnvironmentResource KubernetesEnvironment { get; set; } = default!;
/// <remarks>
/// This is the environment used by the AKS chart-generation pipeline, not a separate compute environment.
/// Its <see cref="KubernetesEnvironmentResource.HelmChartPublishingResult"/> is available after
/// the <c>publish-{Name}</c> pipeline step completes.
/// </remarks>
public KubernetesEnvironmentResource KubernetesEnvironment { get; internal set; } = default!;

/// <summary>
/// Gets the resource ID of the AKS cluster.
Expand Down
69 changes: 69 additions & 0 deletions src/Aspire.Hosting.Kubernetes/HelmChartPublishingResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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;

namespace Aspire.Hosting.Kubernetes;

/// <summary>
/// Describes a generated Helm chart and the value mappings needed by an external deployment publisher.
/// </summary>
/// <remarks>
/// This is a read-only snapshot of publishing metadata, not of the chart files or the application model.
/// Resource and value-provider objects retain their original identity and are not evaluated or cloned.
/// Secret parameters and provisioned outputs remain deferred until the consuming publisher resolves them.
/// </remarks>
public sealed class HelmChartPublishingResult
{
internal HelmChartPublishingResult(string outputPath, KubernetesEnvironmentResource environment)
{
OutputPath = outputPath;

// Repeated chart generation can capture the same mapping more than once.
// Copy distinct entries without exposing or modifying the publisher's capture buffers.
Parameters = Array.AsReadOnly(environment.CapturedHelmValues.Distinct()
.Select(value => new HelmValueMapping<ParameterResource>(value.Section, value.ResourceKey, value.ValueKey, value.Parameter))
.ToArray());
ValueProviders = Array.AsReadOnly(environment.CapturedHelmValueProviders.Distinct()
.Select(value => new HelmValueMapping<IValueProvider>(value.Section, value.ResourceKey, value.ValueKey, value.ValueProvider))
.ToArray());
Images = Array.AsReadOnly(environment.CapturedHelmImageReferences.Distinct()
.Select(value => new HelmValueMapping<IResource>(value.Section, value.ResourceKey, value.ValueKey, value.Resource))
Comment on lines +27 to +31
.ToArray());
CrossReferences = Array.AsReadOnly(environment.CapturedHelmCrossReferences.Distinct()
.Select(value => new HelmValueMapping<string>(value.Section, value.ResourceKey, value.ValueKey, value.TemplateValue))
.ToArray());
}

/// <summary>
/// Gets the directory containing <c>Chart.yaml</c>, <c>values.yaml</c>, and the chart templates.
/// </summary>
public string OutputPath { get; }

/// <summary>
/// Gets the mappings to parameters that require deployment-time resolution, including secret parameters.
/// </summary>
public IReadOnlyList<HelmValueMapping<ParameterResource>> Parameters { get; }

/// <summary>
/// Gets the mappings to deferred value providers, such as provisioned outputs.
/// </summary>
public IReadOnlyList<HelmValueMapping<IValueProvider>> ValueProviders { get; }

/// <summary>
/// Gets the mappings to resources whose container images must be resolved at deployment time.
/// </summary>
/// <remarks>
/// Use <see cref="ContainerImageReference"/> to resolve an image after its registry is available.
/// </remarks>
public IReadOnlyList<HelmValueMapping<IResource>> Images { get; }

/// <summary>
/// Gets the mappings to composite templates containing references to other Helm values.
/// </summary>
/// <remarks>
/// For example, a template can contain <c>{{ .Values.secrets.cache.password }}</c>.
/// Resolve the referenced parameter mappings before substituting these expressions.
/// </remarks>
public IReadOnlyList<HelmValueMapping<string>> CrossReferences { get; }
}
43 changes: 43 additions & 0 deletions src/Aspire.Hosting.Kubernetes/HelmValueMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.Kubernetes;

/// <summary>
/// Maps a Helm values path to a source that can be resolved by a deployment publisher.
/// </summary>
/// <typeparam name="T">The type of the deferred source or template.</typeparam>
/// <remarks>
/// The path consists of three separate keys: <see cref="Section"/>, <see cref="ResourceKey"/>,
/// and <see cref="ValueKey"/>. Treat each key as a literal dictionary key, not as a dotted path.
/// </remarks>
public sealed class HelmValueMapping<T>
{
internal HelmValueMapping(string section, string resourceKey, string valueKey, T value)
{
Section = section;
ResourceKey = resourceKey;
ValueKey = valueKey;
Value = value;
}

/// <summary>
/// Gets the top-level values section, such as <c>parameters</c>, <c>config</c>, or <c>secrets</c>.
/// </summary>
public string Section { get; }

/// <summary>
/// Gets the resource key within the values section.
/// </summary>
public string ResourceKey { get; }

/// <summary>
/// Gets the value key within the resource's values.
/// </summary>
public string ValueKey { get; }

/// <summary>
/// Gets the original deferred source or the unresolved Helm template.
/// </summary>
public T Value { get; }
}
25 changes: 25 additions & 0 deletions src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,31 @@ public sealed class KubernetesEnvironmentResource : Resource, IComputeEnvironmen

internal IPortAllocator PortAllocator { get; } = new PortAllocator();

/// <summary>
/// Gets the generated Helm chart and its deferred value mappings, or <c>null</c> before publishing completes.
/// </summary>
/// <remarks>
/// Read this property from a pipeline step that depends on <c>publish-{Name}</c>.
/// A successful publish replaces the result with a new read-only snapshot. Starting another
/// publish clears the property, and a failed publish does not expose a successful result.
/// Previously obtained snapshots remain unchanged, but their referenced resources and chart files
/// are not copied.
/// </remarks>
/// <example>
/// <code>
/// var environment = builder.AddKubernetesEnvironment("k8s");
/// builder.Pipeline.AddStep("consume-chart", context =>
/// {
/// var result = environment.Resource.HelmChartPublishingResult
/// ?? throw new InvalidOperationException("The Helm chart has not been published.");
/// // Consume result.OutputPath and its deferred value mappings.
/// return Task.CompletedTask;
/// }, dependsOn: $"publish-{environment.Resource.Name}", requiredBy: "publish");
/// </code>
/// </example>
[AspireExportIgnore(Reason = "Publishing results are consumed by C# deployment pipeline extensions, not AppHost authoring APIs.")]
public HelmChartPublishingResult? HelmChartPublishingResult { get; internal set; }

/// <summary>
/// Captured parameter-to-values.yaml mappings populated during publish, consumed during deploy
/// to resolve parameter values and composite references into the environment values file.
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ internal sealed class KubernetesPublishingContext(

internal async Task WriteModelAsync(DistributedApplicationModel model, KubernetesEnvironmentResource environment)
{
environment.HelmChartPublishingResult = null;

if (!executionContext.IsPublishMode)
{
return;
Expand All @@ -63,6 +65,8 @@ internal async Task WriteModelAsync(DistributedApplicationModel model, Kubernete

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

environment.HelmChartPublishingResult = new HelmChartPublishingResult(OutputPath, environment);

logger.FinishGeneratingKubernetes(OutputPath);
}

Expand Down
24 changes: 24 additions & 0 deletions src/Aspire.Hosting.Kubernetes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ For this example, `aspire publish` writes `registry.example.com/apps/api:latest`

Literal registry values and non-secret parameter defaults can be resolved during publishing. Registry values that require provisioning, secrets, or parameters without defaults remain deferred, with `<resource>:latest` as the published placeholder. `aspire deploy` resolves the image names again after provisioning and supplies them through a Helm values override file. If deploying the published chart yourself, supply the deferred image values to Helm.

### Consuming generated charts from a deployment publisher

C# deployment pipeline extensions can read `KubernetesEnvironmentResource.HelmChartPublishingResult`
from a step that depends on `publish-{environment.Name}`. For AKS, use
`AzureKubernetesEnvironmentResource.KubernetesEnvironment` to access the actual chart-producing
environment; do not register a second Kubernetes environment.

The result is `null` until chart generation succeeds. It contains the chart's `OutputPath` and
read-only mappings for `Parameters`, `ValueProviders`, `Images`, and `CrossReferences`.
Each mapping preserves `Section`, `ResourceKey`, and `ValueKey` as separate dictionary keys.
Parameter, provider, and image-resource objects retain their original identity; publishing the
result does not evaluate them or materialize secrets.

Resolve parameters and provisioned value providers only in the external publisher's appropriate
deployment phase. Resolve image resources with `ContainerImageReference` after registries are
available. Composite cross-reference templates contain Helm expressions such as
`{{ .Values.secrets.cache.password }}` that must be substituted using the resolved parameter
mappings. Merely copying `values.yaml` does not resolve these deferred values.

Every successful publish produces a new metadata snapshot. Previous snapshots do not change,
but referenced application-model objects and files in the output directory are not copied.
Starting a new publish clears the current result; a failed publish leaves it `null`.
This API generates no additional deployment steps and does not provision infrastructure.

### Volumes

Use a target-neutral volume when the Kubernetes environment's default storage policy is sufficient:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,48 @@ namespace Aspire.Hosting.Azure.Tests;

public class AzureKubernetesEnvironmentExtensionsTests(ITestOutputHelper outputHelper)
{
[Theory]
[InlineData(DistributedApplicationOperation.Run)]
[InlineData(DistributedApplicationOperation.Publish)]
public void KubernetesEnvironmentExposesActualChartEnvironment(DistributedApplicationOperation operation)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace, operation);
var aks = builder.AddAzureKubernetesEnvironment("aks");
var chart = aks.AddHelmChart("sample", "oci://registry.example.com/charts/sample", "1.0.0");

Assert.Same(aks.Resource.KubernetesEnvironment, chart.Resource.Parent);
Assert.Same(aks.Resource, aks.Resource.KubernetesEnvironment.OwningComputeEnvironment);
Assert.Null(aks.Resource.KubernetesEnvironment.HelmChartPublishingResult);
Assert.Empty(builder.Resources.OfType<KubernetesEnvironmentResource>());
}

[Fact]
public async Task KubernetesEnvironmentExposesPublishedChartToDependentStep()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
using var builder = AzureKubernetesTestBuilder.Create(outputHelper, workspace);
var aks = builder.AddAzureKubernetesEnvironment("aks");
var parameter = builder.AddParameter("setting");
builder.AddContainer("api", "nginx")
.WithEnvironment("SETTING", parameter)
.WithComputeEnvironment(aks);
HelmChartPublishingResult? observed = null;
builder.Pipeline.AddStep("consume-chart", _ =>
{
observed = Assert.IsType<HelmChartPublishingResult>(aks.Resource.KubernetesEnvironment.HelmChartPublishingResult);
Assert.True(File.Exists(Path.Combine(observed.OutputPath, "Chart.yaml")));
Assert.Contains(observed.Parameters, mapping => ReferenceEquals(mapping.Value, parameter.Resource));
return Task.CompletedTask;
}, dependsOn: $"publish-{aks.Resource.KubernetesEnvironment.Name}", requiredBy: WellKnownPipelineSteps.Publish);

using var app = builder.Build();
await app.RunAsync();

Assert.NotNull(observed);
Assert.Same(observed, aks.Resource.KubernetesEnvironment.HelmChartPublishingResult);
}

[Fact]
public async Task AddAzureKubernetesEnvironment_BasicConfiguration()
{
Expand Down
Loading
Loading