Add typed cert-manager API for Kubernetes/AKS environments - #17008
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17008Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17008" |
|
/deployment-test |
|
🚀 Deployment tests starting on PR #17008... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
Mitch Denny (mitchdenny)
left a comment
There was a problem hiding this comment.
Self-review: 4 issues found.
- Fix doc-comment grammar in CertManagerIssuerResource. - Log warning when an HTTP-01 issuer has no annotated parent gateway, instead of silently emitting an unsatisfiable solver. - Narrow temp-dir cleanup catch to IOException/UnauthorizedAccessException so OperationCanceledException and unexpected failures aren't swallowed. - Use ResourceNameComparer to match parent environment, matching the pattern in KubernetesEnvironmentContext / KubernetesEnvironmentResource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a first-class cert-manager modeling layer for Kubernetes/AKS environments so AppHost code can declare cert-manager installation, ClusterIssuers, and TLS-enabled Gateways/Ingresses without raw Helm/kubectl wiring.
Changes:
- Adds
CertManagerResource,CertManagerIssuerResource, and extension APIs for installing cert-manager, configuring ACME issuers, HTTP-01 solvers, andWithTls(issuer). - Adds an AKS-specific
AddCertManageroverload. - Adds unit coverage, an Azure deployment E2E test, and a playground app demonstrating the typed API.
Show a summary per file
| File | Description |
|---|---|
src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs |
Implements typed cert-manager API, Helm install setup, issuer apply pipeline step, and TLS annotation overloads. |
src/Aspire.Hosting.Kubernetes/CertManagerResource.cs |
Adds cert-manager wrapper resource and underlying Helm chart reference. |
src/Aspire.Hosting.Kubernetes/CertManagerIssuerResource.cs |
Adds typed ClusterIssuer resource and internal issuer/solver model types. |
src/Aspire.Hosting.Azure.Kubernetes/AzureCertManagerExtensions.cs |
Adds AKS-specific forwarding overload for AddCertManager. |
tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs |
Adds unit tests for resource registration, issuer config, TLS annotations, and run-mode behavior. |
tests/Aspire.Deployment.EndToEnd.Tests/AksAzureKubernetesEnvironmentCertManagerDeploymentTests.cs |
Adds live AKS cert-manager deployment E2E validation. |
playground/CertManagerDemo/CertManagerDemo.AppHost/AppHost.cs |
Adds playground AppHost using AKS, AGC, cert-manager, issuer, and TLS gateway APIs. |
playground/CertManagerDemo/CertManagerDemo.AppHost/CertManagerDemo.AppHost.csproj |
Adds playground AppHost project. |
playground/CertManagerDemo/CertManagerDemo.AppHost/Properties/launchSettings.json |
Adds launch profiles for the playground AppHost. |
playground/CertManagerDemo/CertManagerDemo.AppHost/appsettings.json |
Adds playground AppHost logging settings. |
playground/CertManagerDemo/CertManagerDemo.ApiService/Program.cs |
Adds sample API endpoints for routing/TLS validation. |
playground/CertManagerDemo/CertManagerDemo.ApiService/CertManagerDemo.ApiService.csproj |
Adds playground API service project. |
playground/CertManagerDemo/CertManagerDemo.ApiService/Properties/launchSettings.json |
Adds API service launch profile. |
playground/CertManagerDemo/CertManagerDemo.ApiService/appsettings.json |
Adds API service logging/host settings. |
playground/CertManagerDemo/aspire.config.json |
Points Aspire tooling at the playground AppHost. |
Aspire.slnx |
Includes the new CertManagerDemo projects in the solution. |
Copilot's findings
- Files reviewed: 16/16 changed files
- Comments generated: 16
- Require explicit name on AddCertManager (no default 'cert-manager') so
multiple cert-manager installations across environments don't collide
- Drop ingress WithTls(issuer) overload; the HTTP-01 solver only emits a
gatewayHTTPRoute parentRefs block today, so the ingress overload would
silently produce a non-functional config. Will revisit when DNS-01 lands.
- Cross-environment validation in gateway WithTls(issuer): throw if the
gateway and the issuer's cert-manager installation aren't in the same
Kubernetes environment (cert-manager is per-cluster).
- Normalize ClusterIssuer metadata.name, privateKeySecretRef.name suffix,
and Gateway parentRef name via ToKubernetesResourceName() so DNS-1123
rules are satisfied even when the user picks a mixed-case Aspire name.
- Validate {name}-chart against the 64-char DNS-1123 label limit at
AddCertManager time.
- Add destroy step (cm-issuer-delete-{name}) that runs before
helm-uninstall-{chart} so ClusterIssuers are torn down while the
cert-manager controller is still alive to clean up account secrets and
while the CRDs still exist.
- Fix CertManagerIssuerResource doc: manifests are kubectl applied, not
baked into the helm chart output.
- Trim 'change the chart version' claim from CertManagerResource.HelmChart
doc; ChartVersion is get-only.
- Add per-overload XML doc params/returns on parameterized
WithLetsEncryptProduction/Staging/AcmeServer.
- New unit tests:
* BuildClusterIssuerManifest_EmitsExpectedYamlForLetsEncryptHttp01
(covers DNS-1123 normalisation and parentRef wiring end-to-end)
* Gateway_WithTls_Issuer_FromDifferentEnvironment_Throws
- Drop Ingress_WithTls_Issuer_AddsClusterIssuerAnnotation test
(overload removed).
- Update playground + E2E test to pass explicit 'cert-manager' name.
- Remove stale 'Bug 2 fix' comment from E2E test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per Dan's review feedback (#17008): v1.18.2 was ~10 months old with v1.20.2 being the current stable. v1.20.x has been GA since 2025-09 and keeps the same Gateway API solver shape we already exercise, so no API changes required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-test |
|
🚀 Deployment tests starting on PR #17008... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
|
Where is the typescript API? I don't see it in the pr description. |
My bad. I'll get it added and I'll add an E2E test for it as well. |
Addresses David Fowler's review feedback (where is the typescript API?) by exercising the [AspireExport]-generated TypeScript surface of the cert-manager API in two ways: - Polyglot apphost.ts: exercises addCertManager + addIssuer + withLetsEncryptProduction/Staging/Param + withAcmeServer/Param + withHttp01Solver + gateway.withGatewayTlsIssuer. CI runs aspire restore --apphost + tsc --noEmit on this file on every PR push. - AksAzureKubernetesEnvironmentCertManagerTypeScriptDeploymentTests: TS-AppHost variant of the C# AKS cert-manager E2E test. Uses the Express/React starter and patches apphost.ts to wire AKS + AGC + cert-manager + Let's Encrypt production around the Express API, deploys to AKS, and verifies the served cert is from Let's Encrypt via openssl + that https://<fqdn>/ returns 2xx. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t-manager E2E tests In Helm v4, --server-side changed from a bool flag to a string flag that requires a 'true'|'false'|'auto' value (helm/helm#13649). Our previous '--server-side --force-conflicts' caused Helm v4 to consume '--force-conflicts' as the value for --server-side, poisoning the release metadata with the literal string '--force-conflicts' as the apply-method enum. The first install appeared to succeed, but every subsequent 'helm upgrade --install' for that release failed with: Error: UPGRADE FAILED: invalid/unknown release server-side apply method: --force-conflicts Pass --server-side=true (with explicit value) so the flag parses identically under Helm v3.18 (where the bool flag also accepts an explicit value) and Helm v4. Add a 'Step N: Re-deploying to validate helm upgrade idempotency' step to both AKS cert-manager E2E tests (C# and TypeScript variants) that runs 'aspire deploy' a second time without --clear-cache. This exercises the helm UPGRADE path that the original single-deploy test did not cover. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, WithTls() on both KubernetesGatewayResource and KubernetesIngressResource
snapshotted Resource.Hostnames into the GatewayTlsConfig / IngressTlsConfig record at
the moment WithTls() was called. This meant calling order mattered:
gateway.WithHostname("api.example.com")
.WithTls("my-secret"); // OK — snapshot has hostname
gateway.WithTls("my-secret") // BUG — snapshot is empty
.WithHostname("api.example.com");
In the buggy second form, the generated HTTPS listener fell back to the
no-hostname code path. With cert-manager + an auto-FQDN gateway controller
like Azure Application Gateway for Containers, the listener would then be
patched to use the controller-assigned FQDN, and cert-manager would issue
a certificate for the wrong hostname (the auto-FQDN, not the user's
custom hostname).
Fix: drop the Hosts field from both record types and resolve hostnames
from gatewayResource.Hostnames / ingressResource.Hostnames at manifest-emit
time. WithTls() now only stores the secret name; ordering with WithHostname()
no longer affects the generated manifests.
Add two regression tests:
- AddGateway_WithTls_BeforeWithHostname_HostnameStillAppliedToHttpsListener
- AddIngress_WithTls_BeforeWithHostname_HostnameIncludedInTlsHosts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…urce) The C# WithAcmeServer parameterized overload signature is (string serverUrl, IResourceBuilder<ParameterResource> email). The TypeScript polyglot test was incorrectly calling it with two parameters, which TypeScript rejected as TS2345. Pass a literal URL for the ACME directory and keep the email as the ParameterResource argument. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When Gateway FQDN discovery patches the listener hostname via 'kubectl patch',
the field manager defaults to 'kubectl-patch'. A subsequent 'helm upgrade'
that uses server-side apply with field manager 'helm' then conflicts:
conflict with "kubectl-patch" using gateway.networking.k8s.io/v1:
.spec.listeners[name="https"].hostname
Pass --field-manager=helm so Helm is the registered owner from the start
and SSA on the next deploy does not conflict.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-test |
|
🚀 Deployment tests starting on PR #17008... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
The runner-image's preinstalled helm predates v3.18 (which added `helm install/upgrade --server-side` and `--force-conflicts`). The cert-manager E2E tests rely on those flags via `WithForceConflicts()`, so they fail with: Error: unknown flag: --server-side Pin helm to v3.21.0 so the flags are available regardless of the runner image's helm version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-test |
|
🚀 Deployment tests starting on PR #17008... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Older Aspire builds patched Gateway listener hostnames with kubectl's
default field manager ("kubectl-patch") rather than "helm". The
resulting Update entry persists in the resource's managedFields after
later builds switched to --field-manager=helm, and helm's server-side
apply still conflicts with the foreign Update ownership of
.spec.listeners[name="https"].hostname on every subsequent upgrade
(including the case where the user transitions from auto-discovered to
explicit hostname). Add a pre-helm-deploy step that scans Gateways with
TLS for stale non-helm Update entries owning listener fields and removes
them via JSON Patch on managedFields. No-op on first deploy and on
clusters with no foreign managers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-test |
|
🚀 Deployment tests starting on PR #17008... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
The previous 5-minute timeout was insufficient for AGC, which has been observed taking 5-10 minutes to assign an address when the cluster and AGC are freshly provisioned in the same deploy. The deployment-test for the AKS cert-manager TypeScript scenario hit this exact case: AGC took ~6 minutes to publish the address, so discovery exhausted its 60 attempts and bailed out with a logged warning. The deploy then 'succeeded' without ever patching the listener hostname, so cert-manager's gateway shim never created a Certificate, and the test failed downstream with 'certificates.cert-manager.io api-gw-tls not found'. Two fixes: - Bump MaxRetryAttempts to 179 (~15 minutes) to match the wait budget the E2E test uses for the same condition. - Throw on timeout instead of logging a warning and continuing. A deploy that completes without patching the listener hostname produces no valid TLS, which is worse than failing visibly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/deployment-test |
|
🚀 Deployment tests starting on PR #17008... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
|
❌ Deployment E2E Tests failed — 36 passed, 2 failed, 0 cancelled View test results and recordings
|
|
🎬 CLI E2E Test Recordings — 82 recordings uploaded (commit View all recordings
📹 Recordings uploaded automatically from CI run #25854215965 |
Description
Adds a typed cert-manager API surface to
Aspire.Hosting.Kubernetesso cert-manager can be installed, configured, and wired to gateways/ingress through the Aspire app model — without dropping into rawWithAnnotationcalls or hand-written Helm values.What's added
AddCertManager(name)onIResourceBuilder<KubernetesEnvironmentResource>— installs the cert-manager Helm chart (helm install --wait) and returns aCertManagerResourcefor further configuration.AddIssuer(name)onCertManagerResource— declares aClusterIssuerthat gets applied at deploy time after cert-manager is healthy.WithLetsEncryptProduction(email)/WithLetsEncryptProductionParam(parameter)WithLetsEncryptStaging(email)/WithLetsEncryptStagingParam(parameter)WithAcmeServer(serverUrl, email)/WithAcmeServer(serverUrl, parameter)for custom ACME serversWithHttp01Solver()(DNS-01 deferred to a follow-up PR)gateway.WithTls(issuer)overload — wires a cert-managerClusterIssuerto a gateway listener via thecert-manager.io/cluster-issuerannotation. Validates that the gateway and issuer live in the sameKubernetesEnvironmentso cross-cluster mistakes fail loudly at app-model build time.Pipeline
The
AddCertManager(...)call enqueues the existing helm-install pipeline step. EachAddIssuer(...)enqueues acm-issuer-apply-{name}step that depends on the helm install (so cert-manager's validating webhook is guaranteed to be Available before theClusterIssueris applied — otherwise we race the webhook and getfailed calling webhook "webhook.cert-manager.io": no endpoints available).For gateways with TLS but no explicit
WithHostname, the integration also pre-creates a self-signed bootstrap TLS secret so the controller (e.g., AGC) will program the listener before cert-manager has issued the real certificate. Without this the listener would deadlock waiting for a secret that doesn't exist yet.C# example (Express + React on AKS, Let's Encrypt prod via HTTP-01)
TypeScript example (same pattern, polyglot apphost)
Tests
tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs— unit coverage forAddCertManager,AddIssuer, the variousWithLetsEncrypt*andWithAcmeServeroverloads,WithHttp01Solver,WithTls(issuer), and the cross-environment validation.tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.csandKubernetesIngressTests.cs— added regression tests covering theWithTls()-then-WithHostname()ordering case.tests/PolyglotAppHosts/Aspire.Hosting.Kubernetes/TypeScript/apphost.ts— exercises the entire surface in TypeScript viatsc --noEmit(so the[AspireExport]-driven TS bindings stay valid).tests/Aspire.Deployment.EndToEnd.Tests/AksAzureKubernetesEnvironmentCertManagerDeploymentTests.csandAksAzureKubernetesEnvironmentCertManagerTypeScriptDeploymentTests.cs— full deploy E2E tests that stand up AKS + AGC, install cert-manager, issue a real Let's Encrypt production certificate via HTTP-01, verify trusted HTTPS, and then re-deploy to exercise the helm UPGRADE path before destroying the cluster.Bug fixes pulled in along the way
--server-sideflag parsing — Helm v4 changed--server-sidefrom a bool flag to a string-valued one (true|false|auto) in helm/helm#13649. Our code shelled out--server-side --force-conflicts, which Helm v4 silently parses as--server-side=--force-conflicts, poisoning release metadata so every subsequent upgrade fails withinvalid/unknown release server-side apply method: --force-conflicts. The first install appeared to succeed; the second always failed. Now using--server-side=trueso the flag parses identically under v3.18 and v4.WithTls(...)snapshottedResource.Hostnamesat call time, so callingWithTls()beforeWithHostname()produced an HTTPS listener with no hostname (and cert-manager would then issue a cert for the wrong host). The TLS config records now resolve hostnames lazily at manifest-emit time so calling order doesn't matter.Out of scope (follow-ups)