diff --git a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs
index 2f1b3918db4..baaeedf32ca 100644
--- a/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs
+++ b/src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.cs
@@ -214,7 +214,12 @@ private static bool HasPersistedHelmReleaseState(JsonObject deploymentState)
///
/// Gets the underlying Kubernetes environment resource used for Helm-based deployment.
///
- internal KubernetesEnvironmentResource KubernetesEnvironment { get; set; } = default!;
+ ///
+ /// This is the environment used by the AKS chart-generation pipeline, not a separate compute environment.
+ /// Its is available after
+ /// the publish-{Name} pipeline step completes.
+ ///
+ public KubernetesEnvironmentResource KubernetesEnvironment { get; internal set; } = default!;
///
/// Gets the resource ID of the AKS cluster.
diff --git a/src/Aspire.Hosting.Kubernetes/HelmChartPublishingResult.cs b/src/Aspire.Hosting.Kubernetes/HelmChartPublishingResult.cs
new file mode 100644
index 00000000000..c766844e43d
--- /dev/null
+++ b/src/Aspire.Hosting.Kubernetes/HelmChartPublishingResult.cs
@@ -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;
+
+///
+/// Describes a generated Helm chart and the value mappings needed by an external deployment publisher.
+///
+///
+/// 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.
+///
+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(value.Section, value.ResourceKey, value.ValueKey, value.Parameter))
+ .ToArray());
+ ValueProviders = Array.AsReadOnly(environment.CapturedHelmValueProviders.Distinct()
+ .Select(value => new HelmValueMapping(value.Section, value.ResourceKey, value.ValueKey, value.ValueProvider))
+ .ToArray());
+ Images = Array.AsReadOnly(environment.CapturedHelmImageReferences.Distinct()
+ .Select(value => new HelmValueMapping(value.Section, value.ResourceKey, value.ValueKey, value.Resource))
+ .ToArray());
+ CrossReferences = Array.AsReadOnly(environment.CapturedHelmCrossReferences.Distinct()
+ .Select(value => new HelmValueMapping(value.Section, value.ResourceKey, value.ValueKey, value.TemplateValue))
+ .ToArray());
+ }
+
+ ///
+ /// Gets the directory containing Chart.yaml, values.yaml, and the chart templates.
+ ///
+ public string OutputPath { get; }
+
+ ///
+ /// Gets the mappings to parameters that require deployment-time resolution, including secret parameters.
+ ///
+ public IReadOnlyList> Parameters { get; }
+
+ ///
+ /// Gets the mappings to deferred value providers, such as provisioned outputs.
+ ///
+ public IReadOnlyList> ValueProviders { get; }
+
+ ///
+ /// Gets the mappings to resources whose container images must be resolved at deployment time.
+ ///
+ ///
+ /// Use to resolve an image after its registry is available.
+ ///
+ public IReadOnlyList> Images { get; }
+
+ ///
+ /// Gets the mappings to composite templates containing references to other Helm values.
+ ///
+ ///
+ /// For example, a template can contain {{ .Values.secrets.cache.password }}.
+ /// Resolve the referenced parameter mappings before substituting these expressions.
+ ///
+ public IReadOnlyList> CrossReferences { get; }
+}
diff --git a/src/Aspire.Hosting.Kubernetes/HelmValueMapping.cs b/src/Aspire.Hosting.Kubernetes/HelmValueMapping.cs
new file mode 100644
index 00000000000..8a23145fc19
--- /dev/null
+++ b/src/Aspire.Hosting.Kubernetes/HelmValueMapping.cs
@@ -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;
+
+///
+/// Maps a Helm values path to a source that can be resolved by a deployment publisher.
+///
+/// The type of the deferred source or template.
+///
+/// The path consists of three separate keys: , ,
+/// and . Treat each key as a literal dictionary key, not as a dotted path.
+///
+public sealed class HelmValueMapping
+{
+ internal HelmValueMapping(string section, string resourceKey, string valueKey, T value)
+ {
+ Section = section;
+ ResourceKey = resourceKey;
+ ValueKey = valueKey;
+ Value = value;
+ }
+
+ ///
+ /// Gets the top-level values section, such as parameters, config, or secrets.
+ ///
+ public string Section { get; }
+
+ ///
+ /// Gets the resource key within the values section.
+ ///
+ public string ResourceKey { get; }
+
+ ///
+ /// Gets the value key within the resource's values.
+ ///
+ public string ValueKey { get; }
+
+ ///
+ /// Gets the original deferred source or the unresolved Helm template.
+ ///
+ public T Value { get; }
+}
diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
index 0c41f9efa27..2e0b88526dc 100644
--- a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
+++ b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
@@ -149,6 +149,31 @@ public sealed class KubernetesEnvironmentResource : Resource, IComputeEnvironmen
internal IPortAllocator PortAllocator { get; } = new PortAllocator();
+ ///
+ /// Gets the generated Helm chart and its deferred value mappings, or null before publishing completes.
+ ///
+ ///
+ /// Read this property from a pipeline step that depends on publish-{Name}.
+ /// 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.
+ ///
+ ///
+ ///
+ /// 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");
+ ///
+ ///
+ [AspireExportIgnore(Reason = "Publishing results are consumed by C# deployment pipeline extensions, not AppHost authoring APIs.")]
+ public HelmChartPublishingResult? HelmChartPublishingResult { get; internal set; }
+
///
/// Captured parameter-to-values.yaml mappings populated during publish, consumed during deploy
/// to resolve parameter values and composite references into the environment values file.
diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs b/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs
index cbd0a961c80..b92a027164b 100644
--- a/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs
+++ b/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs
@@ -45,6 +45,8 @@ internal sealed class KubernetesPublishingContext(
internal async Task WriteModelAsync(DistributedApplicationModel model, KubernetesEnvironmentResource environment)
{
+ environment.HelmChartPublishingResult = null;
+
if (!executionContext.IsPublishMode)
{
return;
@@ -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);
}
diff --git a/src/Aspire.Hosting.Kubernetes/README.md b/src/Aspire.Hosting.Kubernetes/README.md
index c49bbc18752..4bc9ac6bf7b 100644
--- a/src/Aspire.Hosting.Kubernetes/README.md
+++ b/src/Aspire.Hosting.Kubernetes/README.md
@@ -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 `: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:
diff --git a/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs
index 8110a0bb363..64f77735dd0 100644
--- a/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesEnvironmentExtensionsTests.cs
@@ -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());
+ }
+
+ [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(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()
{
diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/HelmChartPublishingResultTests.cs b/tests/Aspire.Hosting.Kubernetes.Tests/HelmChartPublishingResultTests.cs
new file mode 100644
index 00000000000..4507338ad47
--- /dev/null
+++ b/tests/Aspire.Hosting.Kubernetes.Tests/HelmChartPublishingResultTests.cs
@@ -0,0 +1,156 @@
+// 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 Aspire.Hosting.Pipelines;
+using Aspire.Hosting.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Aspire.Hosting.Kubernetes.Tests;
+
+public class HelmChartPublishingResultTests(ITestOutputHelper outputHelper)
+{
+ [Fact]
+ public async Task ResultIsAvailableToDependentStepAndPreservesDeferredSources()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
+ var environment = builder.AddKubernetesEnvironment("env").WithDashboard(false);
+ var parameter = builder.AddParameter("ordinary");
+ var secret = builder.AddParameter("password", secret: true);
+ var provider = new TestValueProvider("must-not-be-published", "{outputs.deferred}");
+ var api = builder.AddContainer("api-service", "nginx")
+ .WithEnvironment("ORDINARY", parameter)
+ .WithEnvironment("SECRET", secret)
+ .WithEnvironment("DEFERRED", provider)
+ .WithEnvironment("COMPOSITE", $"prefix-{parameter}-{secret}-suffix");
+ var project = builder.AddDotnetProject("worker", "worker.csproj", options => options.ExcludeLaunchProfile = true);
+
+ Assert.Null(environment.Resource.HelmChartPublishingResult);
+ HelmChartPublishingResult? observed = null;
+ builder.Pipeline.AddStep("consume-chart", _ =>
+ {
+ observed = Assert.IsType(environment.Resource.HelmChartPublishingResult);
+ Assert.True(File.Exists(Path.Combine(observed.OutputPath, "Chart.yaml")));
+ Assert.True(File.Exists(Path.Combine(observed.OutputPath, "values.yaml")));
+ Assert.True(File.Exists(Path.Combine(observed.OutputPath, "templates", api.Resource.Name, "deployment.yaml")));
+ return Task.CompletedTask;
+ }, dependsOn: $"publish-{environment.Resource.Name}", requiredBy: WellKnownPipelineSteps.Publish);
+
+ using var app = builder.Build();
+ await app.RunAsync();
+
+ var result = Assert.IsType(observed);
+ Assert.Same(result, environment.Resource.HelmChartPublishingResult);
+ Assert.Equal(workspace.Path, result.OutputPath);
+ Assert.Contains(result.Parameters, mapping => ReferenceEquals(mapping.Value, parameter.Resource));
+ Assert.Contains(result.Parameters, mapping => ReferenceEquals(mapping.Value, secret.Resource));
+ Assert.Same(provider, Assert.Single(result.ValueProviders).Value);
+ Assert.Equal(0, provider.GetValueCallCount);
+ Assert.Same(project.Resource, Assert.Single(result.Images).Value);
+ Assert.NotEmpty(result.CrossReferences);
+
+ Assert.Equal(environment.Resource.CapturedHelmValues.Select(value =>
+ (value.Section, value.ResourceKey, value.ValueKey, value.Parameter)),
+ result.Parameters.Select(value => (value.Section, value.ResourceKey, value.ValueKey, value.Value)));
+ Assert.Equal(environment.Resource.CapturedHelmValueProviders.Select(value =>
+ (value.Section, value.ResourceKey, value.ValueKey, value.ValueProvider)),
+ result.ValueProviders.Select(value => (value.Section, value.ResourceKey, value.ValueKey, value.Value)));
+ Assert.Equal(environment.Resource.CapturedHelmImageReferences.Select(value =>
+ (value.Section, value.ResourceKey, value.ValueKey, value.Resource)),
+ result.Images.Select(value => (value.Section, value.ResourceKey, value.ValueKey, value.Value)));
+ Assert.Equal(environment.Resource.CapturedHelmCrossReferences.Select(value =>
+ (value.Section, value.ResourceKey, value.ValueKey, value.TemplateValue)),
+ result.CrossReferences.Select(value => (value.Section, value.ResourceKey, value.ValueKey, value.Value)));
+
+ await Verify(new
+ {
+ Parameters = result.Parameters.Select(value => new { value.Section, value.ResourceKey, value.ValueKey, value.Value.Name, value.Value.Secret }),
+ ValueProviders = result.ValueProviders.Select(value => new { value.Section, value.ResourceKey, value.ValueKey }),
+ Images = result.Images.Select(value => new { value.Section, value.ResourceKey, value.ValueKey, value.Value.Name }),
+ result.CrossReferences
+ }).AppendContentAsFile(await File.ReadAllTextAsync(Path.Combine(result.OutputPath, "values.yaml")), "yaml");
+ }
+
+ [Fact]
+ public async Task RepeatedPublishingReplacesResultWithoutMutatingPreviousSnapshot()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
+ var environment = builder.AddKubernetesEnvironment("env").WithDashboard(false);
+ var parameter = builder.AddParameter("parameter", secret: true);
+ var provider = new TestValueProvider("deferred");
+ builder.AddDotnetProject("api", "api.csproj", options => options.ExcludeLaunchProfile = true)
+ .WithEnvironment("PARAMETER", parameter)
+ .WithEnvironment("DEFERRED", provider)
+ .WithEnvironment("COMPOSITE", $"prefix-{parameter}");
+ using var app = builder.Build();
+ await app.RunAsync();
+
+ var first = Assert.IsType(environment.Resource.HelmChartPublishingResult);
+ var parameters = first.Parameters.ToArray();
+ var providers = first.ValueProviders.ToArray();
+ var images = first.Images.ToArray();
+ var templates = first.CrossReferences.ToArray();
+ Assert.Throws(() => ((IList>)first.Parameters).Clear());
+ Assert.Throws(() => ((IList>)first.ValueProviders).Clear());
+ Assert.Throws(() => ((IList>)first.Images).Clear());
+ Assert.Throws(() => ((IList>)first.CrossReferences).Clear());
+
+ var secondOutput = Path.Combine(workspace.Path, "second");
+ var context = new KubernetesPublishingContext(builder.ExecutionContext, secondOutput, NullLogger.Instance, environment.Resource);
+ await context.WriteModelAsync(app.Services.GetRequiredService(), environment.Resource);
+
+ var second = Assert.IsType(environment.Resource.HelmChartPublishingResult);
+ Assert.NotSame(first, second);
+ Assert.Equal(secondOutput, second.OutputPath);
+ Assert.Equal(first.Parameters.Count, second.Parameters.Count);
+ Assert.Equal(first.ValueProviders.Count, second.ValueProviders.Count);
+ Assert.Equal(first.Images.Count, second.Images.Count);
+ Assert.Equal(first.CrossReferences.Count, second.CrossReferences.Count);
+
+ environment.Resource.CapturedHelmValues.Clear();
+ environment.Resource.CapturedHelmValueProviders.Clear();
+ environment.Resource.CapturedHelmImageReferences.Clear();
+ environment.Resource.CapturedHelmCrossReferences.Clear();
+ Assert.Equal(parameters, first.Parameters);
+ Assert.Equal(providers, first.ValueProviders);
+ Assert.Equal(images, first.Images);
+ Assert.Equal(templates, first.CrossReferences);
+ Assert.Equal(workspace.Path, first.OutputPath);
+ }
+
+ [Fact]
+ public async Task FailedPublishingClearsPreviousResult()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
+ var environment = builder.AddKubernetesEnvironment("env").WithDashboard(false);
+ builder.AddContainer("api", "nginx");
+ using var app = builder.Build();
+ await app.RunAsync();
+ Assert.NotNull(environment.Resource.HelmChartPublishingResult);
+
+ var outputFile = Path.Combine(workspace.Path, "not-a-directory");
+ await File.WriteAllTextAsync(outputFile, "existing file");
+ var context = new KubernetesPublishingContext(builder.ExecutionContext, outputFile, NullLogger.Instance, environment.Resource);
+ await Assert.ThrowsAnyAsync(() =>
+ context.WriteModelAsync(app.Services.GetRequiredService(), environment.Resource));
+
+ Assert.Null(environment.Resource.HelmChartPublishingResult);
+ }
+
+ [Fact]
+ public async Task RunModeDoesNotProduceResult()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
+ var environment = builder.AddKubernetesEnvironment("env");
+ using var app = builder.Build();
+ var context = new KubernetesPublishingContext(builder.ExecutionContext, "unused", NullLogger.Instance, environment.Resource);
+
+ await context.WriteModelAsync(app.Services.GetRequiredService(), environment.Resource);
+
+ Assert.Null(environment.Resource.HelmChartPublishingResult);
+ }
+}
diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/HelmChartPublishingResultTests.ResultIsAvailableToDependentStepAndPreservesDeferredSources.verified.txt b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/HelmChartPublishingResultTests.ResultIsAvailableToDependentStepAndPreservesDeferredSources.verified.txt
new file mode 100644
index 00000000000..2d3e4948628
--- /dev/null
+++ b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/HelmChartPublishingResultTests.ResultIsAvailableToDependentStepAndPreservesDeferredSources.verified.txt
@@ -0,0 +1,41 @@
+{
+ Parameters: [
+ {
+ Section: config,
+ ResourceKey: api_service,
+ ValueKey: ordinary,
+ Name: ordinary,
+ Secret: false
+ },
+ {
+ Section: secrets,
+ ResourceKey: api_service,
+ ValueKey: password,
+ Name: password,
+ Secret: true
+ }
+ ],
+ ValueProviders: [
+ {
+ Section: config,
+ ResourceKey: api_service,
+ ValueKey: DEFERRED
+ }
+ ],
+ Images: [
+ {
+ Section: parameters,
+ ResourceKey: worker,
+ ValueKey: worker_image,
+ Name: worker
+ }
+ ],
+ CrossReferences: [
+ {
+ Section: secrets,
+ ResourceKey: api_service,
+ ValueKey: COMPOSITE,
+ Value: prefix-{{ .Values.config.api_service.ordinary }}-{{ .Values.secrets.api_service.password }}-suffix
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/HelmChartPublishingResultTests.ResultIsAvailableToDependentStepAndPreservesDeferredSources.verified.yaml b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/HelmChartPublishingResultTests.ResultIsAvailableToDependentStepAndPreservesDeferredSources.verified.yaml
new file mode 100644
index 00000000000..30ed87dcb03
--- /dev/null
+++ b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/HelmChartPublishingResultTests.ResultIsAvailableToDependentStepAndPreservesDeferredSources.verified.yaml
@@ -0,0 +1,13 @@
+parameters:
+ worker:
+ worker_image: "worker:latest"
+secrets:
+ api_service:
+ password: ""
+ COMPOSITE: ""
+config:
+ api_service:
+ ordinary: ""
+ DEFERRED: ""
+ worker:
+ OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/TestValueProvider.cs b/tests/Aspire.Hosting.Kubernetes.Tests/TestValueProvider.cs
index 1021ea4de2a..244eb23b8a4 100644
--- a/tests/Aspire.Hosting.Kubernetes.Tests/TestValueProvider.cs
+++ b/tests/Aspire.Hosting.Kubernetes.Tests/TestValueProvider.cs
@@ -9,13 +9,20 @@ internal sealed class TestValueProvider(
string value,
string valueExpression = "{test-value}") : IValueProvider, IManifestExpressionProvider
{
+ private int _getValueCallCount;
+
public string ValueExpression { get; } = valueExpression;
+ public int GetValueCallCount => Volatile.Read(ref _getValueCallCount);
+
public ValueTask GetValueAsync(CancellationToken cancellationToken = default)
- => new(value);
+ {
+ Interlocked.Increment(ref _getValueCallCount);
+ return new(value);
+ }
public ValueTask GetValueAsync(
ValueProviderContext context,
CancellationToken cancellationToken = default)
- => new(value);
+ => GetValueAsync(cancellationToken);
}