Skip to content

Commit 9f431c6

Browse files
mitchdennyCopilot
andcommitted
Fix Kubernetes embedded environment values
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 207a672 commit 9f431c6

9 files changed

Lines changed: 162 additions & 19 deletions

src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,22 @@ private async Task AppendResourceContextToHelmValuesAsync(IResource resource, Ku
187187
{
188188
await AddValuesToHelmSectionAsync(resource, resourceContext.Parameters, HelmExtensions.ParametersKey).ConfigureAwait(false);
189189

190-
// Merge AdditionalConfigValues (e.g., branch parameters from if/else conditionals)
191-
// into a combined dictionary for the config section of values.yaml.
190+
// Embedded parameters need values.yaml entries for their Helm references, but they must
191+
// not become additional environment variables in the generated ConfigMap or Secret.
192192
var configItems = new Dictionary<string, KubernetesResource.HelmValue>(resourceContext.EnvironmentVariables);
193193
foreach (var kvp in resourceContext.AdditionalConfigValues)
194194
{
195195
configItems.TryAdd(kvp.Key, kvp.Value);
196196
}
197197

198+
var secretItems = new Dictionary<string, KubernetesResource.HelmValue>(resourceContext.Secrets);
199+
foreach (var kvp in resourceContext.AdditionalSecretValues)
200+
{
201+
secretItems.TryAdd(kvp.Key, kvp.Value);
202+
}
203+
198204
await AddValuesToHelmSectionAsync(resource, configItems, HelmExtensions.ConfigKey).ConfigureAwait(false);
199-
await AddValuesToHelmSectionAsync(resource, resourceContext.Secrets, HelmExtensions.SecretsKey).ConfigureAwait(false);
205+
await AddValuesToHelmSectionAsync(resource, secretItems, HelmExtensions.SecretsKey).ConfigureAwait(false);
200206
}
201207

202208
private async Task AddValuesToHelmSectionAsync(

src/Aspire.Hosting.Kubernetes/KubernetesResource.cs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ internal record EndpointMapping(string Scheme, string Protocol, string Host, Hel
5050
internal Dictionary<string, HelmValue> Secrets { get; } = [];
5151
internal Dictionary<string, HelmValue> Parameters { get; } = [];
5252
internal Dictionary<string, HelmValue> AdditionalConfigValues { get; } = [];
53+
internal Dictionary<string, HelmValue> AdditionalSecretValues { get; } = [];
5354
internal Dictionary<string, string> Labels { get; private set; } = [];
5455
internal List<string> Commands { get; } = [];
5556
internal List<VolumeMountV1> Volumes { get; } = [];
@@ -527,7 +528,13 @@ private async Task<object> ProcessValueAsync(KubernetesEnvironmentContext contex
527528

528529
if (value is ParameterResource param)
529530
{
530-
return AllocateParameter(param, TargetResource);
531+
var helmValue = AllocateParameter(param, TargetResource);
532+
if (embedded)
533+
{
534+
AllocateAdditionalParameter(param, helmValue);
535+
}
536+
537+
return helmValue;
531538
}
532539

533540
if (value is ConnectionStringReference cs)
@@ -652,8 +659,7 @@ private async Task<object> BuildHelmConditional(KubernetesEnvironmentContext con
652659

653660
/// <summary>
654661
/// Ensures that any <see cref="ParameterResource"/> instances referenced in a branch's
655-
/// value providers are allocated in the appropriate dictionary (EnvironmentVariables or
656-
/// Secrets) so their values flow to values.yaml via <c>AddValuesToHelmSectionAsync</c>.
662+
/// value providers are allocated so their values flow to values.yaml.
657663
/// </summary>
658664
private void AllocateBranchParameters(ReferenceExpression branch)
659665
{
@@ -662,23 +668,21 @@ private void AllocateBranchParameters(ReferenceExpression branch)
662668
if (vp is ParameterResource branchParam)
663669
{
664670
var helmValue = AllocateParameter(branchParam, TargetResource);
665-
var key = branchParam.Name.ToHelmValuesSectionName();
666-
667-
// Store in AdditionalConfigValues rather than EnvironmentVariables to avoid
668-
// case-insensitive key collisions in ToConfigMap's processedKeys. These values
669-
// flow to the config section of values.yaml but do not appear as env vars.
670-
if (helmValue.ExpressionContainsHelmSecretExpression)
671-
{
672-
Secrets.TryAdd(key, helmValue);
673-
}
674-
else
675-
{
676-
AdditionalConfigValues.TryAdd(key, helmValue);
677-
}
671+
AllocateAdditionalParameter(branchParam, helmValue);
678672
}
679673
}
680674
}
681675

676+
/// <summary>
677+
/// Allocates an embedded parameter without adding a synthetic environment variable.
678+
/// </summary>
679+
private void AllocateAdditionalParameter(ParameterResource parameter, HelmValue helmValue)
680+
{
681+
var key = parameter.Name.ToHelmValuesSectionName();
682+
var values = parameter.Secret ? AdditionalSecretValues : AdditionalConfigValues;
683+
values.TryAdd(key, helmValue);
684+
}
685+
682686
private static string GetEndpointValue(EndpointMapping mapping, EndpointProperty property, bool embedded = false)
683687
{
684688
var (scheme, _, host, targetPort, _, _, exposedPort) = mapping;

tests/Aspire.Hosting.Kubernetes.Tests/KubernetesDeployTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,50 @@ await HelmDeploymentEngine.ResolveAndWriteDeployValuesAsync(
15731573
Assert.Contains("e2e-test-pw-42", content);
15741574
}
15751575

1576+
[Fact]
1577+
public async Task EmbeddedParametersInEnvironmentExpressions_EndToEnd_PublishAndResolve()
1578+
{
1579+
using var workspace = TemporaryWorkspace.Create(outputHelper);
1580+
1581+
var builder = TestDistributedApplicationBuilder.Create(
1582+
DistributedApplicationOperation.Publish,
1583+
workspace.Path,
1584+
step: WellKnownPipelineSteps.Publish);
1585+
var mockActivityReporter = new TestPipelineActivityReporter(outputHelper);
1586+
1587+
builder.Services.AddSingleton<IResourceContainerImageManager, MockImageBuilder>();
1588+
builder.Services.AddSingleton<IPipelineActivityReporter>(mockActivityReporter);
1589+
1590+
var envBuilder = builder.AddKubernetesEnvironment("env");
1591+
var host = builder.AddParameter("host", "localhost");
1592+
var token = builder.AddParameter("token", "test-token", secret: true);
1593+
1594+
builder.AddContainer("myapp", "nginx")
1595+
.WithEnvironment("SOME_URL", $"http://{host}/test")
1596+
.WithEnvironment("SECRET_URL", $"http://{host}/test?token={token}");
1597+
1598+
using var app = builder.Build();
1599+
var env = envBuilder.Resource;
1600+
await app.RunAsync();
1601+
1602+
Assert.Contains(env.CapturedHelmValues, captured =>
1603+
captured.Section == "config" &&
1604+
captured.ResourceKey == "myapp" &&
1605+
captured.ValueKey == "host" &&
1606+
captured.Parameter == host.Resource);
1607+
Assert.Contains(env.CapturedHelmValues, captured =>
1608+
captured.Section == "secrets" &&
1609+
captured.ResourceKey == "myapp" &&
1610+
captured.ValueKey == "token" &&
1611+
captured.Parameter == token.Resource);
1612+
1613+
await HelmDeploymentEngine.ResolveAndWriteDeployValuesAsync(
1614+
workspace.Path, env, CancellationToken.None);
1615+
1616+
var overridePath = Path.Combine(workspace.Path, HelmDeploymentEngine.GetDeployValuesFileName("env"));
1617+
await Verify(await File.ReadAllTextAsync(overridePath), "yaml");
1618+
}
1619+
15761620
[Fact]
15771621
public void AddKubernetesEnvironment_CreatesDashboardByDefault()
15781622
{

tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,53 @@ public async Task KubernetesProbeUsesContainerTargetPortNotServicePort()
855855
await settingsTask;
856856
}
857857

858+
[Fact]
859+
public async Task PublishAsync_EmbeddedParametersInEnvironmentExpressionsPopulateValues()
860+
{
861+
using var workspace = TemporaryWorkspace.Create(outputHelper);
862+
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
863+
864+
builder.AddKubernetesEnvironment("env");
865+
866+
// Regression for https://github.com/microsoft/aspire/issues/11140: the base chart keeps
867+
// deployment parameters empty, but every nested Helm reference must still be declared.
868+
var host = builder.AddParameter("host", "localhost");
869+
var token = builder.AddParameter("token", "test-token", secret: true);
870+
871+
builder.AddContainer("myapp", "nginx")
872+
.WithEnvironment("SOME_URL", $"http://{host}/test")
873+
.WithEnvironment("SECRET_URL", $"http://{host}/test?token={token}");
874+
875+
var app = builder.Build();
876+
app.Run();
877+
878+
var expectedFiles = new[]
879+
{
880+
"values.yaml",
881+
"templates/myapp/config.yaml",
882+
"templates/myapp/secrets.yaml",
883+
};
884+
885+
SettingsTask settingsTask = default!;
886+
887+
foreach (var expectedFile in expectedFiles)
888+
{
889+
var filePath = Path.Combine(workspace.Path, expectedFile);
890+
var fileExtension = Path.GetExtension(filePath)[1..];
891+
892+
if (settingsTask is null)
893+
{
894+
settingsTask = Verify(File.ReadAllText(filePath), fileExtension);
895+
}
896+
else
897+
{
898+
settingsTask = settingsTask.AppendContentAsFile(File.ReadAllText(filePath), fileExtension);
899+
}
900+
}
901+
902+
await settingsTask;
903+
}
904+
858905
[Fact]
859906
public async Task PublishAsync_HandlesConditionalReferenceExpression()
860907
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
config:
2+
myapp:
3+
host: localhost
4+
SOME_URL: http://localhost/test
5+
secrets:
6+
myapp:
7+
token: test-token
8+
SECRET_URL: http://localhost/test?token=test-token
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
parameters: {}
2+
secrets:
3+
myapp:
4+
SECRET_URL: ""
5+
token: ""
6+
config:
7+
myapp:
8+
SOME_URL: ""
9+
host: ""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
apiVersion: "v1"
3+
kind: "ConfigMap"
4+
metadata:
5+
name: "myapp-config"
6+
labels:
7+
app.kubernetes.io/name: "{{ .Chart.Name }}"
8+
app.kubernetes.io/component: "myapp"
9+
app.kubernetes.io/instance: "{{ .Release.Name }}"
10+
data:
11+
SOME_URL: "http://{{ .Values.config.myapp.host }}/test"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
apiVersion: "v1"
3+
kind: "Secret"
4+
metadata:
5+
name: "myapp-secrets"
6+
labels:
7+
app.kubernetes.io/name: "{{ .Chart.Name }}"
8+
app.kubernetes.io/component: "myapp"
9+
app.kubernetes.io/instance: "{{ .Release.Name }}"
10+
stringData:
11+
SECRET_URL: "http://{{ .Values.config.myapp.host }}/test?token={{ .Values.secrets.myapp.token }}"
12+
type: "Opaque"

tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesSpecialResourceName#01.verified.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ secrets:
55
SpeciaL_ApP:
66
param3: ""
77
ConnectionStrings__api_cs: ""
8+
param1: ""
89
config:
910
SpeciaL_ApP:
1011
OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY: "in_memory"
1112
ConnectionStrings__api_cs2: "host.local:80"
1213
OTEL_EXPORTER_OTLP_ENDPOINT: "http://env-dashboard-service:18889"
1314
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
1415
OTEL_SERVICE_NAME: "SpeciaL-ApP"
16+
param0: ""

0 commit comments

Comments
 (0)