Skip to content

Add AddHelmChart for installing external Helm charts - #16589

Merged
Mitch Denny (mitchdenny) merged 13 commits into
mainfrom
feature/add-helm-chart
May 11, 2026
Merged

Add AddHelmChart for installing external Helm charts#16589
Mitch Denny (mitchdenny) merged 13 commits into
mainfrom
feature/add-helm-chart

Conversation

@mitchdenny

@mitchdenny Mitch Denny (mitchdenny) commented Apr 30, 2026

Copy link
Copy Markdown
Member

Description

Adds AddHelmChart infrastructure for installing external Helm charts into a Kubernetes environment as aspire deploy pipeline steps. This is in addition to (and runs after) the application's own Helm chart that Aspire generates and deploys.

This enables installing pre-existing Helm charts (e.g., cert-manager, NGINX ingress controller, podinfo, monitoring tools) alongside the Aspire-generated application chart. Charts are installed with helm upgrade --install after the main application deploy step, and — when opted in — uninstalled with helm uninstall during aspire destroy.

New types and APIs

  • KubernetesHelmChartResource — models an external chart. ChartReference and ChartVersion are non-nullable, get-only constructor arguments (the resource cannot be put into an invalid state). Namespace, ReleaseName, and Values remain mutable via the builder.
  • AddHelmChart(name, chartReference, chartVersion) — extension on IResourceBuilder<KubernetesEnvironmentResource>. Validates inputs eagerly, creates the resource, and registers a helm-install-{name} pipeline step that runs after the environment''s own helm-deploy-{environment} step.
  • WithHelmValue(key, value) — sets --set key=value. Both the key (allowlist regex ^[A-Za-z0-9_.\-\[\]]+$, supports indexed paths like args[0]) and value (rejects ", \, and control characters) are validated to keep the generated helm argument string well-formed.
  • WithNamespace(namespace) — validated against the same RFC 1123 DNS-label rules used by HelmChartOptions (max 63 chars).
  • WithReleaseName(releaseName) — validated against Helm''s release-name rules (DNS label, max 53 chars).
  • WithDestroy() — opt-in: registers a second pipeline step that runs helm uninstall during aspire destroy. Off by default so unrelated workloads installed in the cluster (e.g., cert-manager) aren''t accidentally torn down.
  • AKS overloadAddHelmChart on AzureKubernetesEnvironmentResource that delegates to the inner Kubernetes environment.

Input validation / hardening

  • chartReference is validated against an allowlist regex (^[A-Za-z0-9_./:@+~\-]+$). Permits OCI URLs, HTTP(S) URLs, local paths, packaged .tgz filenames, and repo/chart references; rejects whitespace, quotes, and shell metacharacters that could break helm''s argument tokenization.
  • chartVersion is parsed with SemanticVersion.TryParse (matches HelmChartOptions.WithChartVersion).
  • WithHelmValue rejects null values and validates both key and value as above.

Example usage

var k8s = builder.AddKubernetesEnvironment("k8s");

k8s.AddHelmChart("cert-manager", "oci://quay.io/jetstack/charts/cert-manager", "1.17.0")
    .WithHelmValue("crds.enabled", "true")
    .WithHelmValue("config.enableGatewayAPI", "true");
// AKS overload — opt in to destroy-time uninstall
var aks = builder.AddAzureKubernetesEnvironment("aks");

aks.AddHelmChart("podinfo", "oci://ghcr.io/stefanprodan/charts/podinfo", "6.7.1")
    .WithHelmValue("replicaCount", "2")
    .WithDestroy();

Also included

Moves KubernetesGatewayExtensions and KubernetesIngressExtensions to the Aspire.Hosting namespace (matching the convention used by other hosting extension methods). Also shipped as #16588.

Testing

  • 48 unit tests in KubernetesHelmChartTests.cs covering: argument validation (null/empty, malicious chart references, invalid namespaces/release names, invalid value keys), WithDestroy default + opt-in + chaining, and end-to-end pipeline-step factory shape (1 step without WithDestroy, 2 steps with).
  • 16 unit tests in AzureKubernetesHelmChartTests.cs covering the AKS overload (parent type, basic props, all With* builders, null-arg rejection, version validation, chart-ref validation).
  • CLI E2E test (KubernetesHelmChartDeploymentTests) — installs podinfo into a KinD cluster.
  • AKS deployment E2E test (AksWithHelmChartDeploymentTests) — provisions a real AKS cluster via AddAzureKubernetesEnvironment, installs podinfo, verifies replicaCount=2 was applied, port-forwards and curls the chart''s Service for HTTP 200, then runs aspire destroy and asserts helm list -n podinfo is empty (proves WithDestroy() actually uninstalled the chart).

✅ All 194 K8S tests + 53 Azure K8S tests pass locally. Latest deployment run: 35/35 passed including AksWithHelmChartDeploymentTests.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

Copilot AI review requested due to automatic review settings April 30, 2026 06:35
@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16589

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16589"

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds infrastructure to model and deploy external Helm charts as post-deploy pipeline steps for Kubernetes (and AKS via delegation), plus aligns Kubernetes Ingress/Gateway extension-method namespaces with other hosting extensions.

Changes:

  • Introduces KubernetesHelmChartResource and KubernetesHelmChartExtensions.AddHelmChart(...) with WithHelmValue/WithNamespace/WithReleaseName configuration.
  • Registers a helm-install-{name} deploy pipeline step that runs after helm-deploy-{environment}.
  • Moves KubernetesIngressExtensions / KubernetesGatewayExtensions into the Aspire.Hosting namespace and adds an AKS AddHelmChart delegating overload.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Kubernetes.Tests/KubernetesHelmChartTests.cs Adds unit coverage for the new Helm chart resource/builder behavior and pipeline annotation presence.
src/Aspire.Hosting.Kubernetes/KubernetesIngressExtensions.cs Moves ingress extensions into Aspire.Hosting namespace (adds using Aspire.Hosting.Kubernetes).
src/Aspire.Hosting.Kubernetes/KubernetesGatewayExtensions.cs Moves gateway extensions into Aspire.Hosting namespace (adds using Aspire.Hosting.Kubernetes).
src/Aspire.Hosting.Kubernetes/KubernetesHelmChartResource.cs Adds the public resource type modeling an external Helm chart and its configuration.
src/Aspire.Hosting.Kubernetes/KubernetesHelmChartExtensions.cs Adds the public fluent APIs and the deploy-time helm install pipeline step implementation.
src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesIngressExtensions.cs Adds AKS AddHelmChart overload delegating to the inner Kubernetes environment.

Comment on lines +121 to +128
public static IResourceBuilder<KubernetesHelmChartResource> WithNamespace(
this IResourceBuilder<KubernetesHelmChartResource> builder,
string @namespace)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(@namespace);

builder.Resource.Namespace = @namespace;

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithNamespace only checks for null/empty, but Kubernetes namespaces must be valid DNS labels (max length 63, lowercase alnum + '-' and must start/end alnum). Without validating here (and/or at deploy time), invalid namespaces can make the generated helm command fail or allow unexpected argument splitting. Consider validating with the same rules used by HelmChartOptions/HelmDeploymentEngine and throwing an ArgumentException with a helpful message.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14cb05cWithNamespace now calls HelmChartOptions.ValidateNamespace (the same RFC 1123 DNS-label rules, max 63 chars) so invalid namespaces throw ArgumentException immediately.

Comment on lines +145 to +150
ArgumentException.ThrowIfNullOrEmpty(releaseName);

builder.Resource.ReleaseName = releaseName;
return builder;
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithReleaseName only checks for null/empty, but Helm release names are constrained (DNS label, max length 53). Please validate releaseName (and ideally enforce lowercase) similarly to HelmChartOptions/HelmDeploymentEngine so consumers get an early, actionable error instead of a helm failure later.

Suggested change
ArgumentException.ThrowIfNullOrEmpty(releaseName);
builder.Resource.ReleaseName = releaseName;
return builder;
}
ValidateHelmReleaseName(releaseName);
builder.Resource.ReleaseName = releaseName;
return builder;
}
private static void ValidateHelmReleaseName(string releaseName)
{
ArgumentException.ThrowIfNullOrEmpty(releaseName);
if (releaseName.Length > 53)
{
throw new ArgumentException("Helm release name must be 53 characters or fewer.", nameof(releaseName));
}
if (!char.IsAsciiLetterOrDigit(releaseName[0]) || !char.IsAsciiLetterOrDigit(releaseName[^1]))
{
throw new ArgumentException("Helm release name must start and end with a lowercase letter or digit.", nameof(releaseName));
}
foreach (var c in releaseName)
{
if (char.IsAsciiLower(c) || char.IsDigit(c) || c == '-')
{
continue;
}
if (char.IsLetter(c) && char.IsUpper(c))
{
throw new ArgumentException("Helm release name must be lowercase and may contain only lowercase letters, digits, and hyphens.", nameof(releaseName));
}
throw new ArgumentException("Helm release name may contain only lowercase letters, digits, and hyphens.", nameof(releaseName));
}
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14cb05cWithReleaseName now calls HelmChartOptions.ValidateReleaseName (DNS label, max 53 chars, lowercase-alnum start/end). I made the existing validators in HelmChartOptions internal and reused them rather than duplicating the rules.

Comment on lines +52 to +68
public static IResourceBuilder<KubernetesHelmChartResource> AddHelmChart(
this IResourceBuilder<KubernetesEnvironmentResource> builder,
[ResourceName] string name,
string chartReference,
string chartVersion)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(chartReference);
ArgumentException.ThrowIfNullOrEmpty(chartVersion);

var environment = builder.Resource;
var resource = new KubernetesHelmChartResource(name, environment)
{
ChartReference = chartReference,
ChartVersion = chartVersion
};

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddHelmChart currently accepts any non-empty chartVersion, but elsewhere (HelmChartOptions.WithChartVersion) the repo validates Helm chart versions as strict semantic versions. Consider validating chartVersion similarly here so invalid versions fail fast with a clear ArgumentException rather than surfacing as a helm CLI error during deployment.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14cb05cAddHelmChart now validates chartVersion via SemanticVersion.TryParse (the same check used by HelmChartOptions.WithChartVersion), so non-semver versions fail fast with ArgumentException instead of surfacing as a helm CLI error during deployment.

Comment on lines +49 to +60
/// <summary>
/// Gets or sets the Helm chart reference. This can be an OCI registry URL
/// (e.g., <c>oci://quay.io/jetstack/charts/cert-manager</c>) or a chart name
/// from an added repository.
/// </summary>
public string? ChartReference { get; set; }

/// <summary>
/// Gets or sets the chart version to install.
/// </summary>
public string? ChartVersion { get; set; }

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChartReference and ChartVersion are nullable/settable even though AddHelmChart requires them to be provided. This makes it easy to put the resource into an invalid state (e.g., ChartVersion set to null) and then get a late failure or different behavior at deploy time. Consider making these non-nullable and required (constructor/init-only), or enforce non-null/valid values consistently before running helm.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cd58a41KubernetesHelmChartResource now takes chartReference and chartVersion as constructor arguments and exposes them as non-nullable, read-only properties. They can no longer be put into an invalid state after construction.

Comment on lines +162 to +180
var releaseName = chart.ReleaseName ?? chart.Name;
var @namespace = chart.Namespace ?? chart.Name;
var chartRef = chart.ChartReference ?? throw new InvalidOperationException($"Helm chart '{chart.Name}' has no chart reference configured.");
var chartVersion = chart.ChartVersion;

logger.LogInformation(
"Installing Helm chart '{ChartName}' ({ChartRef}:{ChartVersion}) into namespace '{Namespace}'.",
chart.Name, chartRef, chartVersion, @namespace);

var arguments = new StringBuilder();
arguments.Append(CultureInfo.InvariantCulture, $"upgrade --install {releaseName} \"{chartRef}\"");
arguments.Append(CultureInfo.InvariantCulture, $" --namespace {@namespace}");
arguments.Append(" --create-namespace");
arguments.Append(" --wait");

if (!string.IsNullOrEmpty(chartVersion))
{
arguments.Append(CultureInfo.InvariantCulture, $" --version {chartVersion}");
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InstallHelmChartAsync treats ChartVersion as optional (it may be null/empty and then no --version is passed), but AddHelmChart requires a version. This can lead to silently installing 'latest' if ChartVersion is cleared via direct mutation. Either make chartVersion truly optional in the public API/docs, or throw if ChartVersion is missing at deploy time to preserve the API contract.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cd58a41 — since ChartVersion is now non-nullable and read-only, it can't be cleared after construction; InstallHelmChartAsync always passes --version.

string value)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(key);

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WithHelmValue validates the key but not the value. At runtime a null value can be stored in the dictionary and will produce a "key=" assignment in the generated helm arguments, which is hard to diagnose and may not match caller intent. Consider throwing for null values (and possibly validating/escaping quotes/newlines) to keep the generated command line well-formed.

Suggested change
ArgumentException.ThrowIfNullOrEmpty(key);
ArgumentException.ThrowIfNullOrEmpty(key);
ArgumentNullException.ThrowIfNull(value);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14cb05cWithHelmValue now rejects null values and validates both key and value. Keys must match ^[A-Za-z0-9_.\-\[\]]+$ (alphanumerics, ., -, _, plus [] for indexed paths like args[0]). Values reject ", \, and control characters so the generated --set key="value" argument is always well-formed.

@mitchdenny

Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #16589...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions
github-actions Bot temporarily deployed to deployment-testing May 1, 2026 00:51 Inactive
@github-actions
github-actions Bot had a problem deploying to deployment-testing May 1, 2026 00:51 Failure
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

@mitchdenny

Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions

github-actions Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #16589...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Found 1 correctness issue not covered by existing review comments:

  • Unescaped chart reference in helm arguments (line 173): The chartRef parameter is interpolated directly into a raw helm command string. Special characters could cause argument injection or parsing errors. Recommend early validation to reject or escape problematic characters.

chart.Name, chartRef, chartVersion, @namespace);

var arguments = new StringBuilder();
arguments.Append(CultureInfo.InvariantCulture, $"upgrade --install {releaseName} \"{chartRef}\"");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chartRef value is interpolated directly into a raw helm argument string without escaping. If a user supplies a chart reference containing special characters (e.g., quotes, spaces, or shell metacharacters), they could unintentionally break out of the intended token and inject additional helm flags, changing deployment behavior or causing failures. Since the runner executes a single raw arguments string (not structured argv), validation is needed.

Recommendation: Validate chartRef format early (whitelist allowed characters like alphanumerics, hyphens, slashes, colons, dots for OCI/repo syntax), or reject characters like quotes and newlines with a clear error message so users get fast feedback at definition time rather than a helm CLI error at deploy time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 14cb05cchartReference is now validated up-front against an allowlist regex (^[A-Za-z0-9_./:@+~\-]+$). It permits OCI URLs (oci://...), HTTP(S) URLs, local paths, packaged .tgz filenames, and repo/chart references, but rejects whitespace, quotes, and shell metacharacters that could break helm's argument tokenization. Coverage in KubernetesHelmChartTests.cs exercises the rejected forms.

Mitch Denny (mitchdenny) and others added 11 commits May 11, 2026 08:44
Adds KubernetesHelmChartResource and extension methods for installing
external Helm charts into a Kubernetes environment as pipeline steps.

- KubernetesHelmChartResource: models an external chart (OCI/repo ref,
  version, namespace, release name, values)
- AddHelmChart(): creates the resource and registers a helm-install
  pipeline step that runs after the main app Helm deploy
- WithHelmValue(): sets --set values for the chart
- WithNamespace()/WithReleaseName(): configure chart installation

The pipeline step uses IHelmRunner (existing abstraction) to execute
helm upgrade --install with the configured values.

This is the foundation for AddCertManager and other Helm-based
integrations in subsequent PRs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move KubernetesGatewayExtensions, KubernetesIngressExtensions, and
KubernetesHelmChartExtensions to the Aspire.Hosting namespace to
match the convention used by other hosting extension methods.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The chart reference (OCI URL) and --set values need quoting to prevent
shell interpretation issues with special characters like # in values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds AddHelmChart extension on AzureKubernetesEnvironmentResource that
delegates to the inner KubernetesEnvironmentResource, matching the
existing pattern for AddIngress and AddGateway.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests the full aspire deploy flow with an external Helm chart (podinfo)
on a local KinD cluster with a local Docker registry. Verifies:
- aspire deploy installs both the app and the external chart
- podinfo is deployed with 2 replicas as configured via WithHelmValue
- Helm release exists in the podinfo namespace

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- WithHelmValue: validate value is not null (prevents --set key= args)
- InstallHelmChartAsync: throw if ChartVersion is null at deploy time
  (enforces the contract established by AddHelmChart requiring version)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests the full flow on a real AKS cluster:
- Creates AKS cluster + ACR via az CLI
- Scaffolds project with AddHelmChart for podinfo (2 replicas)
- Deploys with aspire deploy
- Verifies podinfo has 2 ready replicas
- Verifies podinfo serves HTTP 200 via port-forward
- Verifies Helm release exists

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ChartReference and ChartVersion previously were nullable settable
properties, which meant the resource's identity could drift from what
the install pipeline step described. Switch them to constructor-set
read-only properties, and add a DestroyOnUninstall opt-in flag (set
via the new WithDestroy() extension) so callers can request
destroy-time uninstall without it becoming the default for shared
charts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Validate every interpolated token before it's spliced into the helm
  argument string. chartReference must match an OCI/HTTP/path
  whitelist; chartVersion goes through HelmChartOptions.ValidateChartVersion;
  WithNamespace and WithReleaseName reuse the existing DNS-label
  validators (which are now internal); WithHelmValue rejects keys
  containing whitespace/quotes and values containing quotes,
  backslashes, or control characters, and the install code wraps every
  --set value in double quotes so the OS process call sees one token
  per --set.
* Switch the install step to chartBuilder.WithAnnotation(...)
  (matches the pattern in JavaScriptHostingExtensions and
  ContainerResourceBuilderExtensions).
* Add WithDestroy() opt-in. When set, an extra helm-uninstall-{name}
  pipeline step is registered to run during 'aspire destroy' and the
  install step persists release name + namespace into the deployment
  state manager so the uninstall step can find the release later.
* Add <remarks> docs to the AKS AddHelmChart overload pointing at the
  shared WithDestroy/WithHelmValue/WithNamespace/WithReleaseName
  extensions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Extend KubernetesHelmChartTests with: null-arg coverage, malicious
  chart-reference rejection, valid/invalid chart version cases, DNS
  label rejection for namespace/release name (including length),
  WithHelmValue key/value rejection, WithDestroy default+opt-in
  behavior, and end-to-end pipeline-step factory checks for both the
  default (install only) and WithDestroy (install + uninstall) shapes.
* Add AzureKubernetesHelmChartTests covering the AKS-side overload:
  parent type, basic properties, WithHelmValue, WithNamespace,
  WithReleaseName, WithDestroy, null/empty arg rejection, version
  validation, and chart-reference validation.
* Add AksWithHelmChartDeploymentTests, an AKS E2E test that mirrors
  AksWithAzureResourcesDeploymentTests but uses
  AddAzureKubernetesEnvironment + aks.AddHelmChart(podinfo) +
  WithDestroy(). Verifies podinfo pods come up with the configured
  replica count, that podinfo serves traffic, and that aspire destroy
  uninstalls the chart (helm list -n podinfo is empty afterward).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per repo terminology preference; no functional change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny

Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #16589...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

AddAzureKubernetesEnvironment implicitly adds a default workload pool with Standard_D2s_v5 (DSv5 family). The previous test only overrode the system pool and routinely hit DSv5 vCPU quota in westus3. Mirror AksWithAzureResourcesDeploymentTests by also pinning the workload pool to Standard_D2as_v5 (DASv5 family).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny

Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #16589...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Deployment E2E Tests passed — 35 passed, 0 failed, 0 cancelled

View test results and recordings

View workflow run

Test Result Recording
Deployment.EndToEnd-AcaCompactNamingDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureKeyVaultDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureStorageDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureLogAnalyticsDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-FrontDoorDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-TypeScriptVnetSqlServerInfraDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-TypeScriptExpressDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-KubernetesHelmChartDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureServiceBusDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-KubernetesGatewayTlsDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-VnetKeyVaultConnectivityDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AcaExistingRegistryDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AcaDeploymentErrorOutputTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AcaStarterDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureAppConfigDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-VnetKeyVaultInfraDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AcaManagedRedisDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksBlazorRedisDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksMultipleNodePoolsDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksVnetWithAzureResourcesDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureEventHubsDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksVnetInfraDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AcaCustomRegistryDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-VnetStorageBlobConnectivityDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AppServiceReactDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-VnetSqlServerConnectivityDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AzureContainerRegistryDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksWithHelmChartDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-VnetSqlServerInfraDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-NspStorageKeyVaultDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksStarterDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksWithAzureResourcesDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-VnetStorageBlobInfraDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AksStarterWithRedisHelmDeploymentTests ✅ Passed ▶️ View Recording
Deployment.EndToEnd-AuthenticationTests ✅ Passed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the new AddHelmChart infrastructure. Found 2 issues:\n\n- 1 correctness issue: Default namespace/release name fallback bypasses DNS-label validation that the explicit WithNamespace/WithReleaseName paths enforce.\n- 1 resource leak: Process not disposed in test cleanup method.

}

await deploymentStateManager.DeleteSectionAsync(stateSection, context.CancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Default namespace/release name fallback is not DNS-label-validated

ResolveReleaseAndNamespace falls back to chart.Name when no explicit namespace or release name is set. This value is then interpolated unquoted into the helm command on lines 222 and 302 without any DNS label validation.

By contrast, the existing HelmDeploymentEngine.ResolveReleaseNameAsync (line 63) explicitly calls ValidateHelmReleaseName on the resolved value, and ResolveNamespaceAsync calls ValidateKubernetesNamespace.

If a resource name contains uppercase characters or exceeds 53/63 chars, the helm command will fail at deploy time with an opaque helm error instead of a clear ArgumentException at configuration time.

Suggestion: Validate the resolved release name and namespace here (or at the call sites) using the same HelmChartOptions.ValidateReleaseName/ValidateNamespace helpers already used by WithReleaseName/WithNamespace:

private static (string ReleaseName, string Namespace) ResolveReleaseAndNamespace(KubernetesHelmChartResource chart)
{
    var releaseName = chart.ReleaseName ?? chart.Name;
    var @namespace = chart.Namespace ?? chart.Name;
    HelmChartOptions.ValidateReleaseName(releaseName, nameof(releaseName));
    HelmChartOptions.ValidateNamespace(@namespace, nameof(@namespace));
    return (releaseName, @namespace);
}

Alternatively, validate at AddHelmChart time when the name is first provided, since that's the value that will be used as the default.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 04fc1cc — added validation inside ResolveReleaseAndNamespace and hoisted the call into the PipelineStepAnnotation factory so it runs at step-creation time. Now the user gets a clear InvalidOperationException (with a WithReleaseName/WithNamespace hint) before helm ever runs. Added PipelineStepFactory_RejectsResourceNameThatIsNotValidDnsLabel covering uppercase and over-length resource names, plus PipelineStepFactory_AcceptsInvalidResourceNameWhenOverridesProvided to confirm explicit overrides skip the fallback.

finally
{
output.WriteLine($"Cleaning up resource group: {resourceGroupName}");
await CleanupResourceGroupAsync(resourceGroupName);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource leak: Process not disposed

CleanupResourceGroupAsync creates a System.Diagnostics.Process and awaits WaitForExitAsync() but never disposes the process. Compare with the equivalent method in AksWithHelmChartDeploymentTests.TriggerCleanupResourceGroup which correctly uses using var process.

Suggested change
await CleanupResourceGroupAsync(resourceGroupName);
using var process = new System.Diagnostics.Process

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 04fc1cc — added using var.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage suggestions: 2 missing tests for chart reference allowlist positive coverage and uninstall step description fallback behavior.

Comment on lines +348 to +353

// Allowlist for Helm chart references. Covers OCI URLs (oci://host/path), HTTP/HTTPS URLs,
// local paths, plain chart names ("repo/chart"), and packaged chart filenames. Rejects anything
// that could break helm argument tokenization (whitespace, quotes, control chars).
[GeneratedRegex(@"^[A-Za-z0-9_./:@+~\-]+$")]
private static partial Regex ChartReferencePattern();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing positive test coverage for chart reference allowlist

The existing tests (AddHelmChart_RejectsMaliciousChartReference) only verify rejection of invalid chart references. There are no positive tests confirming the regex accepts the real-world formats documented in the comment above (OCI URLs, HTTP/HTTPS URLs, local paths, repo/chart names, .tgz filenames, + and ~ characters).

Suggested test:

[Theory]
[InlineData("oci://quay.io/jetstack/charts/cert-manager")]   // OCI URL
[InlineData("oci://ghcr.io/stefanprodan/charts/podinfo")]    // OCI URL with ghcr.io
[InlineData("https://charts.example.com/repo/chart")]         // HTTPS URL
[InlineData("http://charts.example.com/repo/chart")]          // HTTP URL
[InlineData("myrepo/mychart")]                                // repo/chart
[InlineData("mychart-1.0.0.tgz")]                             // packaged chart filename
[InlineData("./local-chart")]                                  // local path
[InlineData("chart+extra~tag")]                                // plus and tilde chars
[InlineData("oci://registry:5000/charts/app")]                // registry with port
[InlineData("oci://user@registry.io/charts/app")]             // registry with @
public void AddHelmChart_AcceptsValidChartReferences(string chartReference)
{
    var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
    var k8s = builder.AddKubernetesEnvironment("env");

    var chart = k8s.AddHelmChart("test", chartReference, "1.0.0");
    Assert.Equal(chartReference, chart.Resource.ChartReference);
}

This protects against accidental narrowing of the regex in future refactors.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 04fc1cc — added AddHelmChart_AcceptsValidChartReferences [Theory] covering all 10 cases you suggested (OCI URLs, HTTPS/HTTP URLs, repo/chart, packaged .tgz, relative path, +~ chars, registry with port, and registry with @ user).

var uninstallStep = new PipelineStep
{
Name = $"helm-uninstall-{name}",
Description = $"Uninstalls Helm chart '{name}' from namespace '{resource.Namespace ?? name}'",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test: uninstall step description uses resource name as namespace fallback

When Namespace is null, this description falls back to name. This fallback behavior is untested — the existing PipelineStepFactory_WithDestroy_ProducesInstallAndUninstallSteps test only checks step names and dependency wiring, not the description text.

Suggested test:

[Fact]
public async Task PipelineStepFactory_UninstallStepDescription_ShowsNamespaceDefault()
{
    var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
    var k8s = builder.AddKubernetesEnvironment("env");
    var chart = k8s.AddHelmChart("my-release", "oci://example.com/chart", "1.0.0")
        .WithDestroy();

    // Namespace is not set, so the description should use the resource name as the namespace.
    Assert.Null(chart.Resource.Namespace);

    var steps = await CreateStepsAsync(builder, chart.Resource);
    var uninstallStep = Assert.Single(steps, s => s.Name == "helm-uninstall-my-release");
    Assert.Contains("my-release", uninstallStep.Description); // fallback namespace = name
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 04fc1cc — added PipelineStepFactory_UninstallStepDescription_FallsBackToResourceNameForNamespace (asserts the description contains the resource name when Namespace is null) and PipelineStepFactory_UninstallStepDescription_UsesExplicitNamespace (asserts it uses the explicit value when WithNamespace is called).

…d coverage

- ResolveReleaseAndNamespace now validates the resource-name fallback for both release name and namespace, and is invoked at step-creation time so a clear error fires before helm runs.
- Dispose Process in KubernetesHelmChartDeploymentTests.CleanupResourceGroupAsync.
- Added [Theory] of valid chart references that the allowlist must accept.
- Added [Fact]s for uninstall step description fallback (and explicit namespace) plus tests proving the new validation rejects uppercase / over-length names that would otherwise fail in helm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

🎬 CLI E2E Test Recordings — 78 recordings uploaded (commit 04fc1cc)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View Recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View Recording
DeployK8sBasicApiService ▶️ View Recording
DeployK8sWithExternalHelmChart ▶️ View Recording
DeployK8sWithGarnet ▶️ View Recording
DeployK8sWithMongoDB ▶️ View Recording
DeployK8sWithMySql ▶️ View Recording
DeployK8sWithPostgres ▶️ View Recording
DeployK8sWithRabbitMQ ▶️ View Recording
DeployK8sWithRedis ▶️ View Recording
DeployK8sWithSqlServer ▶️ View Recording
DeployK8sWithValkey ▶️ View Recording
DeployTypeScriptAppToKubernetes ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View Recording
DoListStepsShowsPipelineSteps ▶️ View Recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View Recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LatestCliCanStartStableChannelAppHost ▶️ View Recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
OtelLogsReturnsStructuredLogsFromStarterAppCore ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View Recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View Recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View Recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View Recording

📹 Recordings uploaded automatically from CI run #25647795567

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Pull request created: #905

Generated by PR Documentation Check

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

📝 Documentation has been drafted in microsoft/aspire.dev#905 targeting main.

Drafted documentation for the new AddHelmChart API. Fell back to main because release/13.4 does not exist on microsoft/aspire.dev.

Files created/modified:

  • New: src/frontend/src/content/docs/deployment/kubernetes/helm-charts.mdx — full guide for AddHelmChart, WithHelmValue, WithNamespace, WithReleaseName, WithDestroy, covering both Kubernetes and AKS environments
  • Updated: kubernetes.mdx, aks.mdx, index.mdx — cross-reference sections added
  • Updated: src/frontend/config/sidebar/deployment.topics.ts — "External Helm charts" sidebar entry added

Note

This draft PR needs human review before merging.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants