From 5d1c234ffd10431e5bcbe6ee3f7ce661940aabc5 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 25 Mar 2026 11:40:56 +1100 Subject: [PATCH] Fix Kubernetes publisher crash on Boolean/scalar env vars The Kubernetes publisher's ProcessValueAsync method only handled string, resource references, and expression types. When third-party integrations (like Scalar.Aspire) set environment variables to non-string scalar values (bool, int, double, etc.), it threw NotSupportedException. Add handling for IConvertible types (which covers all primitives) by converting them to their InvariantCulture string representation. Fixes #15229 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../KubernetesResource.cs | 7 +++ .../KubernetesPublisherTests.cs | 49 +++++++++++++++++++ ...rEnvironmentVariableTypes#00.verified.yaml | 11 +++++ ...rEnvironmentVariableTypes#01.verified.yaml | 9 ++++ ...rEnvironmentVariableTypes#02.verified.yaml | 36 ++++++++++++++ ...rEnvironmentVariableTypes#03.verified.yaml | 15 ++++++ 6 files changed, 127 insertions(+) create mode 100644 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#00.verified.yaml create mode 100644 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#01.verified.yaml create mode 100644 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#02.verified.yaml create mode 100644 tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#03.verified.yaml diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs b/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs index 88fe890846e..6899fb7b105 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs @@ -386,6 +386,13 @@ private async Task ProcessValueAsync(KubernetesEnvironmentContext contex return s; } + // Handle scalar/primitive types (bool, int, long, double, etc.) + // These can appear when third-party integrations set environment variables to non-string values. + if (value is IConvertible) + { + return string.Format(CultureInfo.InvariantCulture, "{0}", value); + } + if (value is EndpointReference ep) { var referencedResource = ep.Resource == this diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs b/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs index 92d7f73d92e..33f82f2f66c 100644 --- a/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs +++ b/tests/Aspire.Hosting.Kubernetes.Tests/KubernetesPublisherTests.cs @@ -619,6 +619,55 @@ public async Task PublishAsync_ConditionalWithParameterBranch_UsesIfElseSyntax() await settingsTask; } + [Fact] + public async Task PublishAsync_HandlesScalarEnvironmentVariableTypes() + { + using var tempDir = new TestTempDirectory(); + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path); + + builder.AddKubernetesEnvironment("env"); + + var api = builder.AddContainer("myapp", "mcr.microsoft.com/dotnet/aspnet:8.0") + .WithEnvironment(context => + { + context.EnvironmentVariables["BOOL_TRUE"] = true; + context.EnvironmentVariables["BOOL_FALSE"] = false; + context.EnvironmentVariables["INT_VALUE"] = 42; + context.EnvironmentVariables["DOUBLE_VALUE"] = 3.14; + }) + .WithEnvironment("STRING_VALUE", "hello"); + + var app = builder.Build(); + app.Run(); + + var expectedFiles = new[] + { + "Chart.yaml", + "values.yaml", + "templates/myapp/deployment.yaml", + "templates/myapp/config.yaml", + }; + + SettingsTask settingsTask = default!; + + foreach (var expectedFile in expectedFiles) + { + var filePath = Path.Combine(tempDir.Path, expectedFile); + var fileExtension = Path.GetExtension(filePath)[1..]; + + if (settingsTask is null) + { + settingsTask = Verify(File.ReadAllText(filePath), fileExtension); + } + else + { + settingsTask = settingsTask.AppendContentAsFile(File.ReadAllText(filePath), fileExtension); + } + } + + await settingsTask; + } + private sealed class TestConditionProvider(string value) : IValueProvider, IManifestExpressionProvider { public string ValueExpression => "test-condition"; diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#00.verified.yaml b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#00.verified.yaml new file mode 100644 index 00000000000..d05c0dbf228 --- /dev/null +++ b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#00.verified.yaml @@ -0,0 +1,11 @@ +apiVersion: "v2" +name: "aspire-hosting-tests" +version: "0.1.0" +kubeVersion: ">= 1.18.0-0" +description: "Aspire Helm Chart" +type: "application" +keywords: + - "aspire" + - "kubernetes" +appVersion: "0.1.0" +deprecated: false diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#01.verified.yaml b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#01.verified.yaml new file mode 100644 index 00000000000..68352bf6708 --- /dev/null +++ b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#01.verified.yaml @@ -0,0 +1,9 @@ +parameters: {} +secrets: {} +config: + myapp: + BOOL_TRUE: "True" + BOOL_FALSE: "False" + INT_VALUE: "42" + DOUBLE_VALUE: "3.14" + STRING_VALUE: "hello" diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#02.verified.yaml b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#02.verified.yaml new file mode 100644 index 00000000000..37de00c4947 --- /dev/null +++ b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#02.verified.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: "apps/v1" +kind: "Deployment" +metadata: + name: "myapp-deployment" + labels: + app.kubernetes.io/name: "aspire-hosting-tests" + app.kubernetes.io/component: "myapp" + app.kubernetes.io/instance: "{{ .Release.Name }}" +spec: + template: + metadata: + labels: + app.kubernetes.io/name: "aspire-hosting-tests" + app.kubernetes.io/component: "myapp" + app.kubernetes.io/instance: "{{ .Release.Name }}" + spec: + containers: + - image: "mcr.microsoft.com/dotnet/aspnet:8.0" + name: "myapp" + envFrom: + - configMapRef: + name: "myapp-config" + imagePullPolicy: "IfNotPresent" + selector: + matchLabels: + app.kubernetes.io/name: "aspire-hosting-tests" + app.kubernetes.io/component: "myapp" + app.kubernetes.io/instance: "{{ .Release.Name }}" + replicas: 1 + revisionHistoryLimit: 3 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: "RollingUpdate" diff --git a/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#03.verified.yaml b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#03.verified.yaml new file mode 100644 index 00000000000..936d96906e3 --- /dev/null +++ b/tests/Aspire.Hosting.Kubernetes.Tests/Snapshots/KubernetesPublisherTests.PublishAsync_HandlesScalarEnvironmentVariableTypes#03.verified.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: "v1" +kind: "ConfigMap" +metadata: + name: "myapp-config" + labels: + app.kubernetes.io/name: "aspire-hosting-tests" + app.kubernetes.io/component: "myapp" + app.kubernetes.io/instance: "{{ .Release.Name }}" +data: + BOOL_TRUE: "{{ .Values.config.myapp.BOOL_TRUE }}" + BOOL_FALSE: "{{ .Values.config.myapp.BOOL_FALSE }}" + INT_VALUE: "{{ .Values.config.myapp.INT_VALUE }}" + DOUBLE_VALUE: "{{ .Values.config.myapp.DOUBLE_VALUE }}" + STRING_VALUE: "{{ .Values.config.myapp.STRING_VALUE }}"