Don't select skipped Gateways and Ingresses for TLS work - #19220
Conversation
A Gateway with no routes, and an Ingress with no paths and no default backend, are skipped during materialization. But five other sites re-selected those resources independently with no eligibility check, so deployment steps ran against objects that are never created: - tls-fqdn-discovery polled `kubectl get gateway` 180 x 5s (~15 min) before failing, for a Gateway that was never rendered. - The cert-manager HTTP-01 solver emitted a parentRef to the missing Gateway, leaving the solver HTTPRoute orphaned and Certificates stuck in Pending. This also suppressed the guard warning written to surface exactly that misconfiguration, since the ineligible Gateway made parentGateways.Count non-zero. - Field-manager cleanup and TLS secret bootstrap targeted non-existent Gateways and Ingresses, leaving orphaned self-signed Secrets. Centralize the rule as ShouldMaterialize on KubernetesGatewayResource and KubernetesIngressResource so every selection site shares one definition, and apply it at all five sites. Also reword the route-less Gateway and path-less Ingress warnings to name the omitted artifacts, and widen the cert-manager warning to cover both causes now that route-less Gateways reach that branch. Fixes #19217 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20d0667c-9a79-4368-85ff-e1876d55df72
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19220Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19220" |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Prevents skipped Kubernetes Gateways and Ingresses from entering TLS workflows, addressing #19217’s deployment stall and invalid TLS artifacts.
Changes:
- Centralizes materialization eligibility.
- Filters TLS discovery, bootstrap, cleanup, and cert-manager selection.
- Adds focused pipeline-step and manifest regression tests.
Show a summary per file
| File | Description |
|---|---|
src/Aspire.Hosting.Kubernetes/KubernetesGatewayResource.cs |
Adds Gateway eligibility predicate. |
src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs |
Adds Ingress eligibility predicate. |
src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs |
Filters pipeline selections and improves warnings. |
src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs |
Excludes skipped Gateways from solver references. |
tests/Aspire.Hosting.Kubernetes.Tests/PipelineStepTestHelpers.cs |
Adds shared pipeline-step inspection helpers. |
tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs |
Covers skipped and materialized Gateway steps. |
tests/Aspire.Hosting.Kubernetes.Tests/KubernetesIngressTests.cs |
Covers skipped Ingress TLS steps. |
tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs |
Covers cert-manager parent references. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 3
- Review effort level: Balanced
The XML doc claimed 60 retries / 5 minutes, but the policy uses 179 attempts with 5-second delays (~15 minutes). Also call out that kubectl failures are indistinguishable from 'no address yet', so callers must only pass Gateways that were actually materialized. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20d0667c-9a79-4368-85ff-e1876d55df72
PR Testing ReportPR Information
Artifact Version Verification
Testing was done A/B against a real Kubernetes cluster. Note that the fix ships in the
Changes AnalyzedFiles Changed
Change Categories
Test Scenarios ExecutedScenario 0: Baseline reproduction (pre-fix behavior)Objective: Confirm the bug actually reproduces on shipped bits, so the PR result is meaningful. Steps:
Observations:
Scenario 1: Route-less TLS Gateway with the PR build (headline A/B)Objective: The exact same app must no longer register FQDN discovery or stall. Steps:
Observations:
Note on the non-blocking failure: this deploy ultimately failed at Scenario 2: cert-manager solver
|
| Scenario | Status | Notes |
|---|---|---|
| 0. Baseline reproduction | ✅ Bug reproduced | Stalled 11m03s + orphaned public-tls Secret |
| 1. Route-less Gateway (PR) | ✅ Passed | tls-fqdn-discovery absent; no stall; no orphaned Secret |
2. cert-manager parentRefs |
✅ Passed | No dangling solver parentRef; warning reworded |
| 3. Routed Gateway (positive control) | ✅ Passed | Gateway YAML + object + both TLS steps intact |
| 4. Path-less Ingress + TLS | ✅ Passed | Deploy exit 0; no TLS step; no orphaned Secret |
| 5. Unit tests | ✅ Passed | 273/273, 0 warnings, filters proven load-bearing |
Overall Result
✅ PR VERIFIED
The headline defect is fixed and directly demonstrated A/B on a live cluster: the identical AppHost
stalls 11+ minutes on shipped bits and does not register the step at all with the PR build.
The two additional defects found during code review (cert-manager solver parentRefs, orphaned
Ingress TLS Secret) are also confirmed fixed. The positive control proves the change does not
over-filter — routed Gateways still materialize and still run their full TLS pipeline.
Recommendations
- None blocking.
- Follow-up (optional, out of scope):
DiscoverGatewayFqdnAsyncswallows all kubectl errors
(ThrowOnNonZeroReturnCode = false+ emptyOnErrorData, retrying whenever the result is null), so a
missing CRD, a deleted object, or an unreachable cluster are all indistinguishable from "no address yet"
and each burn the full ~15-minute budget. This PR removes the selection bug that made it easy to hit;
distinguishing hard failures from "not ready yet" would make the step fail fast for the remaining causes.
A doc-correction commit (b2d2531) has been pushed noting the real budget and this caveat.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (4)
src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs:94
ShouldMaterializecan still be true when no Ingress is emitted.BuildIngressObjectreturnsnullwhen every configured path/default backend lacks a deployment target or endpoint mapping (KubernetesEnvironmentResource.cs:827-830), but this predicate then letsCollectTlsSecretsregister the bootstrap step anyway, leaving the same orphaned secret this change is intended to prevent. Base TLS selection on successful materialization (or otherwise include backend resolvability in the eligibility decision).
internal bool ShouldMaterialize => Paths.Count > 0 || DefaultBackend is not null;
src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs:639
- The route-less regression test passes
NullLogger, so it does not verify this diagnostic even though the actionable warning is part of the user-facing fix. Removing this warning, or losing the issuer/environment/WithRouteguidance, would leave all added tests green. Capture the logger output and assert the key diagnostic fields.
logger.LogWarning(
"ClusterIssuer '{IssuerName}' has an HTTP-01 solver but no Gateway in environment '{EnvironmentName}' is both annotated with " +
ClusterIssuerAnnotationKey + "={IssuerName} and configured with at least one route. cert-manager will not be able to satisfy " +
"ACME challenges until at least one routed Gateway adopts this issuer (e.g. via WithRoute(...) and WithTls(issuer)).",
src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs:886
- The updated route-less Gateway test verifies artifacts and pipeline-step names but never asserts this new actionable warning. Since the issue explicitly requires users to be told which Gateway outputs are omitted, capture the publish logs and assert the Gateway name plus the omitted-resource guidance so this behavior cannot disappear unnoticed.
logger.LogWarning(
"Gateway '{GatewayName}' has no routes configured. The Gateway, routes, TLS certificate, and load-balancer frontend will not be created.",
gatewayResource.Name);
tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs:195
- These assertions use the weak
Assert.DoesNotContainpattern discouraged by the repository guidance for generated artifacts. The test can still pass if the manifest loses unrelated required content. Snapshot the complete generated YAML with Verify so both the solver shape and absence ofparentRefsare covered together.
Assert.DoesNotContain("parentRefs:", yaml);
Assert.DoesNotContain("name: public", yaml);
- Files reviewed: 8/8 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
🚀 Deployment tests starting on PR #19220... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
✅ Deployment E2E tests passed (real Azure)Ran the Kubernetes deployment E2E suite against this PR's branch on real Azure infrastructure. Run: 31461921150 · Conclusion: Dispatched
Each job reported Why this is the right coverage for this PR
In other words, the change's two highest-risk behaviors (dropping a Gateway from FQDN discovery, and dropping it from the cert-manager solver) are validated end-to-end against live infrastructure, not just unit tests. Combined with the earlier local A/B (route-less Gateway stalls 11m+ on shipped bits vs. the step never being registered with this PR) and the 273 passing unit tests, the fix is verified from both directions: the broken path is gone, and the working path is untouched. |
James Newton-King (JamesNK)
left a comment
There was a problem hiding this comment.
Approving with the expectation that the three existing unresolved review threads are addressed before merge, particularly the ingress materialization/TLS bootstrap correctness issue.
Validation: I reviewed the affected code paths locally and ran Aspire.Hosting.Kubernetes.Tests at b2d2531: 273 passed, 0 failed, 0 skipped. The PR testing report also records successful real-Azure Kubernetes deployment E2E coverage at this commit; I did not independently run that Azure deployment suite.
ShouldMaterialize is evaluated when pipeline steps are collected, which happens before any step runs, so it can only reject Ingresses that are statically ineligible. BuildIngressObject can still return null later (unresolvable backend), leaving a tls-bootstrap step registered for an Ingress that never appears in the chart and creating an orphaned Secret. Carry the owning resource through to deploy time via TlsSecretRequest and re-check effective materialization inside the bootstrap action, where GeneratedIngress is authoritative. Basing the original selection on GeneratedIngress is not possible: it is always null during step collection, so that would disable TLS bootstrap for every Ingress. Also assert the route-less Gateway and cert-manager solver warnings, which were previously emitted but untested. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20d0667c-9a79-4368-85ff-e1876d55df72
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs:92
- This documentation describes
ShouldMaterializeas an authoritative emitted-state signal, but an Ingress with configured paths can still be omitted when all backends fail resolution, as the new tests demonstrate. Describe this as configuration-time eligibility so future callers do not mistake it forGeneratedIngress is not null.
/// Gets a value indicating whether this ingress is emitted into the deployment artifacts.
/// An ingress with neither path rules nor a default backend is skipped, so TLS secret
/// collection must not select it — bootstrapping a secret for an Ingress that is never
/// created leaves an orphaned self-signed certificate in the cluster.
src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs:2077
- The new unresolvable-backend tests only call
OwnerWasMaterializeddirectly; they never execute this guard or thetls-bootstrapstep. Removing thisifleaves those tests green, so the orphaned-secret regression is not protected at the point where the secret is actually skipped. Add a test that executes the bootstrap action with an unmaterialized Ingress and verifies that nokubectl/secret creation is attempted, with a materialized positive control.
if (!OwnerWasMaterialized(tlsSecret.Owner))
src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs:1222
PublishAsyncdoes not populate these generated objects;PrepareDeploymentTargetsAsyncdoes so before the publish/Helm steps. Naming the actual preparation phase matters here because this method's correctness depends on that ordering.
/// Determines whether the routing resource that requested a TLS secret actually made it into
/// the rendered chart. Called at deploy time, once publish has populated the generated objects.
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
🚀 Deployment tests starting on PR #19220... This will deploy to real Azure infrastructure. Results will be posted here when complete. |
Deployment E2E re-run after the review fixesThe earlier E2E run predated commit Run 31481521922 — all green, 28m19s (
The three classes that exercise the changed code paths end-to-end — gateway TLS and both cert-manager classes — all pass, so the Merge state
One thing for the approver to note: James Newton-King (@JamesNK)'s approval predates |
Two gaps found while reviewing the change as a whole. The Ingress "will not be created" warning was reworded alongside the Gateway one but never asserted, which is the same weakness that review already called out for the Gateway and cert-manager warnings. More importantly, nothing pinned the invariant that CollectTlsSecrets must not depend on GeneratedIngress. In production the pipeline builds every step before running any of them, so collection always sees GeneratedIngress as null; moving the materialization check there would disable TLS bootstrap for every Ingress. The existing tests could not catch that, because they inspect collection after app.Run() has already populated the generated objects. The new test collects before app.Run() and fails against that exact mutation. Also fixes a brace placed on the declaration line in CollectTlsSecrets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 20d0667c-9a79-4368-85ff-e1876d55df72
Tests selector (audit mode)The full test matrix and all jobs still run in audit mode. The tests and jobs below are what selective CI would run under enforcement. 3 / 100 test projects · 3 jobs, from 9 changed files. Selected test projects (3 / 100)
Selected jobs (3)
How these were chosen — grouped by what changed🔧 🔧 🔧 🔧 🧪 🧪 🧪 🧪 🧪 Job reasons
Selection computed for commit |
There was a problem hiding this comment.
Review details
Suppressed comments (3)
src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs:93
- This summary overstates the property's contract:
truedoes not mean the Ingress is emitted, becauseBuildIngressObjectcan still returnnullwhen no backend resolves. Describe this as static/configuration eligibility so callers do not rely on a false materialization guarantee.
/// <summary>
/// Gets a value indicating whether this ingress is emitted into the deployment artifacts.
/// An ingress with neither path rules nor a default backend is skipped, so TLS secret
/// collection must not select it — bootstrapping a secret for an Ingress that is never
/// created leaves an orphaned self-signed certificate in the cluster.
/// </summary>
tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs:197
- This generated-YAML test uses negative substring assertions, contrary to the repository guidance in
AGENTS.md:267-268. It can pass if unrelated required solver content is accidentally dropped along withparentRefs. Verify the complete YAML with a snapshot (or an exact full-value assertion) instead.
Assert.Contains("- http01:", yaml);
Assert.Contains("gatewayHTTPRoute:", yaml);
Assert.DoesNotContain("parentRefs:", yaml);
Assert.DoesNotContain("name: public", yaml);
src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs:2083
- The new ingress tests verify that
OwnerWasMaterializedreturnsfalse, but none executes thistls-bootstrapaction. Removing this guard would therefore leave the added tests green while restoring the orphaned-secret behavior. Add a focused test that invokes the captured bootstrap step for an unmaterialized Ingress and verifies that nokubectlprocess is started.
if (!OwnerWasMaterialized(tlsSecret.Owner))
{
context.Logger.LogDebug(
"Skipping TLS bootstrap for '{ResourceName}' because it was not included in the generated chart.",
tlsSecret.Owner.Name);
continue;
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
/backport to release/13.2 |
|
Started backporting to |
|
Mitch Denny (@mitchdenny) backporting to git am output$ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch
Applying: Don't select skipped Gateways and Ingresses for TLS work
Using index info to reconstruct a base tree...
A src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs
M src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
A src/Aspire.Hosting.Kubernetes/KubernetesGatewayResource.cs
A src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs
A tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs
A tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs
A tests/Aspire.Hosting.Kubernetes.Tests/KubernetesIngressTests.cs
Falling back to patching base and 3-way merge...
CONFLICT (modify/delete): src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs deleted in HEAD and modified in Don't select skipped Gateways and Ingresses for TLS work. Version Don't select skipped Gateways and Ingresses for TLS work of src/Aspire.Hosting.Kubernetes/CertManagerExtensions.cs left in tree.
Auto-merging src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
CONFLICT (content): Merge conflict in src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentResource.cs
CONFLICT (modify/delete): src/Aspire.Hosting.Kubernetes/KubernetesGatewayResource.cs deleted in HEAD and modified in Don't select skipped Gateways and Ingresses for TLS work. Version Don't select skipped Gateways and Ingresses for TLS work of src/Aspire.Hosting.Kubernetes/KubernetesGatewayResource.cs left in tree.
CONFLICT (modify/delete): src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs deleted in HEAD and modified in Don't select skipped Gateways and Ingresses for TLS work. Version Don't select skipped Gateways and Ingresses for TLS work of src/Aspire.Hosting.Kubernetes/KubernetesIngressResource.cs left in tree.
CONFLICT (modify/delete): tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs deleted in HEAD and modified in Don't select skipped Gateways and Ingresses for TLS work. Version Don't select skipped Gateways and Ingresses for TLS work of tests/Aspire.Hosting.Kubernetes.Tests/CertManagerTests.cs left in tree.
CONFLICT (modify/delete): tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs deleted in HEAD and modified in Don't select skipped Gateways and Ingresses for TLS work. Version Don't select skipped Gateways and Ingresses for TLS work of tests/Aspire.Hosting.Kubernetes.Tests/KubernetesGatewayTests.cs left in tree.
CONFLICT (modify/delete): tests/Aspire.Hosting.Kubernetes.Tests/KubernetesIngressTests.cs deleted in HEAD and modified in Don't select skipped Gateways and Ingresses for TLS work. Version Don't select skipped Gateways and Ingresses for TLS work of tests/Aspire.Hosting.Kubernetes.Tests/KubernetesIngressTests.cs left in tree.
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0001 Don't select skipped Gateways and Ingresses for TLS work
Error: The process '/usr/bin/git' failed with exit code 128 |
Documents the warnings emitted when a route-less Gateway or path-less Ingress is skipped during deployment, per microsoft/aspire#19220. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pull request created: #1470
|
|
📝 Documentation has been drafted in microsoft/aspire.dev#1470 targeting Added troubleshooting guidance to two existing AKS deployment pages covering the new warnings introduced by this fix.
Note This draft PR needs human review before merging. |
|
/backport to release/13.5 |
|
Started backporting to |
Description
Deploying an AppHost that declares a Gateway without any routes made
aspire deployhang for roughly 15 minutes before failing. The reported symptom turned out to be one instance of a broader problem, so this PR fixes the class rather than the single case.A Gateway with no routes, and an Ingress with no paths and no default backend, are intentionally skipped during materialization — they are never written into the deployment artifacts. But five other sites re-selected those same resources independently, with no eligibility check, so deployment steps ran against Kubernetes objects that are never created:
CollectGatewaysNeedingFqdnDiscoverytls-fqdn-discovery-{env}pollskubectl get gateway180 × 5s (~15 min) waiting for an address on a Gateway that was never renderedCertManagerExtensions.AppendSolverparentRefto the missing GatewayCollectGatewaysWithTlsCollectTlsSecrets(gateway loop)CollectTlsSecrets(ingress loop)The cert-manager case is the most damaging and is not visible in the original issue.
WithTls(issuer)sets thecert-manager.io/cluster-issuerannotation, so a route-less Gateway still reached the solver and became aparentRefto a Gateway that never exists. The solver's HTTPRoute has nothing to attach to, the ACME challenge URL is unreachable, and Certificates sit inPendingindefinitely. Worse,AppendSolveralready had a guard that warns when no eligible Gateway is found — written precisely to surface this at deploy time — but the ineligible Gateway madeparentGateways.Count == 1, so the one diagnostic that would have explained the failure was suppressed.User-facing behavior
Given an AppHost with a Gateway that has no routes:
Before:
aspire deployregisteredtls-fqdn-discovery-envand blocked for ~15 minutes polling for a Gateway that would never appear, then failed. If cert-manager was in play, the ClusterIssuer manifest also shipped a danglingparentRefand no warning was emitted.After: no TLS or gateway steps are registered for the skipped Gateway, deployment does not stall, no dangling
parentRefis emitted, and a warning names what was omitted:Implementation
The eligibility rule is centralized as
internal bool ShouldMaterializeonKubernetesGatewayResource(Routes.Count > 0) andKubernetesIngressResource(Paths.Count > 0 || DefaultBackend is not null), and applied at all five selection sites plus the two existing materialization skips. Previously the rule was inlined in exactly one place, which is what allowed the other sites to drift.ShouldMaterializeis a static eligibility rule, and it is evaluated while pipeline steps are being collected.DistributedApplicationPipeline.ResolveStepsAsyncbuilds the complete step list before any step executes, so at selection timeKubernetesIngressResource.GeneratedIngressis alwaysnull. That leaves one residual gap:BuildIngressObjectcan still returnnulllater, when an Ingress path's backend cannot be resolved, so atls-bootstrapstep could remain registered for an Ingress that never reaches the chart, creating an orphaned Secret.CollectTlsSecretstherefore returnsList<TlsSecretRequest>(secret name, hostname, owning resource) instead of aHashSet<(ReferenceExpression, ReferenceExpression)>, andBootstrapTlsSecretsAsyncre-checksOwnerWasMaterialized(owner)at action time. That step depends onhelm-deploy-{env}, so it runs afterprepare-deployment-targetsandGeneratedIngressis authoritative there. Aseenset preserves the secret+host de-duplication theHashSetpreviously provided. Gateways need no second check:BuildGatewayObjectsalways emits the Gateway onceShouldMaterializeis true, and only skips individualHTTPRoutes on resolution failure.The 15-minute retry budget (
MaxRetryAttempts = 179, 5s delay) is deliberately unchanged. AGC and other Gateway controllers legitimately take 5–10 minutes to assign an address to a materialized Gateway; the bug was waiting for a Gateway that would never exist, not waiting too long for one that would.Validation
Each fix was confirmed to be load-bearing by temporarily reverting only the selection-site filters — leaving the materialization skips intact, i.e. the exact pre-fix state — and re-running the suite:
The
hasTls: Falsevariant correctly passed in both states, confirming it is genuine additional coverage rather than a false positive. With the fixes in place,Aspire.Hosting.Kubernetesbuilds with 0 warnings and all 277 tests inAspire.Hosting.Kubernetes.Testspass with no snapshot drift.One existing test had codified the cert-manager bug:
BuildClusterIssuerManifest_EmitsExpectedYamlForLetsEncryptHttp01used a route-less Gateway and asserted theparentRefwas present. It now gives the Gateway a route, and a new test covers the route-less case.Tests assert the complete set of TLS/gateway step names rather than using
Assert.DoesNotContain. The regression class here is "a new gateway step is added without the eligibility filter", which a name-by-name absence check cannot catch — andAssert.DoesNotContainis discouraged byAGENTS.mdas a weak assertion. The duplicated per-file pipeline-step harness was extracted into a sharedPipelineStepTestHelpers.Both diagnostics are now asserted by tests. They were previously emitted but unverified, so the route-less Gateway warning and the cert-manager solver warning could each have been weakened or dropped silently — and those messages are the only signal a user gets that their Gateway or certificate was skipped.
No new deployment E2E test is added. The regression mechanism is pure pipeline-step selection: deterministic, cluster-independent, and fully observable from the step assertions above, which fail without the fix. The existing Kubernetes deployment E2E suite was run against this branch on real Azure and all five classes passed, including
KubernetesGatewayTlsDeploymentTestsand both cert-manager classes (run 31481521922).Fixes #19217
Checklist
<remarks />and<code />elements on your triple slash comments?