Skip to content

[API Proposal] Attach an HTTPRoute to a pre-existing / cross-namespace Gateway, and model HTTPRoute filters (URLRewrite) #19830

Description

@timritzer

Background and Motivation

Aspire.Hosting.Kubernetes ships a good Gateway API model (AddGateway().WithRoute().WithHostname().WithTls()),
but it can only drive a Gateway it generates. Two things block the most common production
topology — a shared, platform-owned Gateway that application teams attach routes to.

1. HttpRouteParentRefV1 carries only Name (Resources/HttpRouteV1.cs:51) — no
namespace, sectionName, group or kind. The emission hardcodes a name-only parentRef at
KubernetesEnvironmentResource.cs:1155:

httpRoute.Spec.ParentRefs.Add(new HttpRouteParentRefV1 { Name = gatewayName });

where gatewayName is always resolved from an in-model KubernetesGatewayResource. There is also
no AsExisting mode on AddGateway, so a Gateway object is always generated.

2. HttpRouteRuleV1 has no Filters (Resources/HttpRouteV1.cs, the type carries only
Matches and BackendRefs). So URLRewrite (and RequestHeaderModifier, RequestRedirect,
RequestMirror, ExtensionRef) cannot be expressed at all. Prefix-stripping is required whenever a
service is exposed under a path prefix on a shared Gateway, which is the normal case for (1).

These are filed together because they are the same file and the same scenario: our own notes had
them as separate gaps until it became obvious they fold into one change.

allowedRoutes does not block this — the parentRef fields are the only thing missing

Worth stating up front, because it is the obvious first objection. Aspire hardcodes
AllowedRoutes.Namespaces.From = "Same" on the listeners of Gateways it generates
(KubernetesEnvironmentResource.cs:1080, :1109, :1133). That governs who may attach to an
Aspire-created Gateway, and it is irrelevant to this proposal: in the existing-gateway case Aspire
emits no Gateway object at all, and the platform's own Gateway owns its allowedRoutes policy.

We have confirmed this end to end. An HTTPRoute in a workload's own namespace, attached to a
shared platform Gateway in a different namespace, reports Accepted=True / ResolvedRefs=True
and serves live traffic — because the platform Gateway's listener already permits routes from other
namespaces, as shared gateways generally do. Cross-namespace attachment works today; the only
thing that cannot be expressed is the parentRef itself.
That makes the model additions below
not just necessary but sufficient.

(For completeness: the "Same" default does mean an Aspire-generated Gateway cannot currently
accept routes from other namespaces. That is a separate limitation, out of scope here.)

Is this the right ask? (division of responsibility)

This is squarely the Gateway API's own role model, which is why we think it is the right ask:

Gateway API role Owns In Aspire today
Infrastructure provider / cluster operator the Gateway ✅ AddGateway generates one
Application developer the HTTPRoute, attached to a Gateway they did not create ❌ blocked

The Gateway API explicitly separates these so app teams can attach routes to infrastructure they
do not own — that is the entire point of parentRefs, cross-namespace ReferenceGrant, and
sectionName. Aspire currently supports only the case where the app team owns both, which is the
less common production shape.

We are not asking Aspire to manage a Gateway it does not own — the opposite. We are asking it to
stop requiring ownership in order to emit a route.

Proposed API

1. Gateway-API-standard optional fields on the parent ref (omitted when null):

 public sealed class HttpRouteParentRefV1
 {
     [YamlMember(Alias = "name")]
     public string Name { get; set; } = null!;
+
+    [YamlMember(Alias = "namespace")]   public string? Namespace   { get; set; }
+    [YamlMember(Alias = "sectionName")] public string? SectionName { get; set; }
+    [YamlMember(Alias = "group")]       public string? Group       { get; set; }
+    [YamlMember(Alias = "kind")]        public string? Kind        { get; set; }
 }

2. An AsExisting mode on the gateway, mirroring the AsExisting / PublishAsExisting
annotation pattern already used in Aspire.Hosting.Azure — skip emitting the Gateway, emit
route-only:

+public static IResourceBuilder<KubernetesGatewayResource> AsExisting(
+    this IResourceBuilder<KubernetesGatewayResource> builder,
+    string gatewayName,
+    string? gatewayNamespace = null);

3. Filters on the rule, serialized after matches and before backendRefs (a
readability and CRD-consistency choice, not a functional requirement — see Risks):

 public sealed class HttpRouteRuleV1
 {
     [YamlMember(Alias = "matches")]     public List<HttpRouteMatchV1> Matches { get; } = [];
+    [YamlMember(Alias = "filters")]     public List<HttpRouteFilterV1> Filters { get; } = [];
     [YamlMember(Alias = "backendRefs")] public List<HttpRouteBackendRefV1> BackendRefs { get; } = [];
 }

+[YamlSerializable]
+public sealed class HttpRouteFilterV1
+{
+    [YamlMember(Alias = "type")]       public string Type { get; set; } = null!;   // e.g. "URLRewrite"
+    [YamlMember(Alias = "urlRewrite")] public HttpUrlRewriteFilterV1? UrlRewrite { get; set; }
+}
+
+[YamlSerializable]
+public sealed class HttpUrlRewriteFilterV1
+{
+    [YamlMember(Alias = "path")] public HttpPathModifierV1? Path { get; set; }
+}
+
+[YamlSerializable]
+public sealed class HttpPathModifierV1
+{
+    [YamlMember(Alias = "type")]               public string Type { get; set; } = "ReplacePrefixMatch";
+    [YamlMember(Alias = "replacePrefixMatch")] public string? ReplacePrefixMatch { get; set; }
+    [YamlMember(Alias = "replaceFullPath")]    public string? ReplaceFullPath { get; set; }
+}

Start with URLRewrite; the other filter kinds can be added later as null-omitted properties.

Usage Examples

var gateway = env.AddGateway("platform")
                 .AsExisting("platform-gateway", "gateway-system");

gateway.WithRoute("/my-app", myService, rewritePrefix: "/");
spec:
  parentRefs:
    - name: platform-gateway
      namespace: gateway-system
      sectionName: https
  rules:
    - matches:
        - path: { type: PathPrefix, value: /my-app }
      filters:
        - type: URLRewrite
          urlRewrite:
            path: { type: ReplacePrefixMatch, replacePrefixMatch: / }
      backendRefs:
        - name: my-app-service
          port: 8080

Alternative Designs

  • Extend only the parent ref, not filters. Insufficient in practice: a workload sharing a host
    on a shared Gateway needs a unique path prefix externally while the pod keeps serving /.
  • Hand-roll the HTTPRoute YAML. What we do today — a StringBuilder that emits
    matches → filters → backendRefs — because the model cannot represent filters and the
    environment's serialization is not extensible from outside the package. It works and is
    snapshot-tested, but it is the single most divergent part of our output and we would delete it
    immediately.

Risks

Low. All additions are optional and null-omitted, so no existing generated manifest changes.
AsExisting follows an established pattern in the codebase.

The behaviour is not speculative — see the note above on cross-namespace attachment already working
against a live shared gateway once the parentRef can express it.

Correction (post-filing). An earlier revision of this issue claimed filters must precede
backendRefs because "Istio and most controllers expect" it. That is wrong: Kubernetes
deserializes these documents into Go structs, so mapping-key order carries no semantics and
no controller can depend on it. The ordering is still worth keeping — it matches the upstream CRD
field order and keeps generated manifests and snapshots stable — but it is a style choice, not a
conformance constraint.

What is semantically significant is the order of elements within the filters list, which
this proposal preserves by modelling it as an ordered List<T>. Per the Gateway API spec
(apis/v1/httproute_types.go): "Wherever possible, implementations SHOULD implement filters in
the order they are specified."


Implementation note — scope is wider than "additive to Aspire.Hosting.Kubernetes". Prototyping
this surfaced two prerequisites that are not visible from the API surface above, and that a reviewer
should weigh:

  1. The new group field could not round-trip until a bug in a shared YAML serializer was fixed.
    string implements IEnumerable, so src/Shared/Yaml/YamlIEnumerableSkipEmptyObjectGraphVisitor.cs
    treated an empty string as an empty collection and dropped it. That matters here because the
    Gateway API assigns empty strings distinct meaning: parentRef.group: "" means "the core API
    group", and replacePrefixMatch: "" means "strip the matched prefix". This file is shared with
    other publishers, not Kubernetes-specific.
  2. Pre-existing rewrite-prefix validation contradicted the Gateway API spec on two counts and had
    to be corrected before the URLRewrite filter behaved to spec.

Neither alters the proposed API surface, and no previously generated manifest changed — verified by
zero pre-existing snapshot drift across both the Kubernetes and Docker suites, the latter exercising
that same shared serializer.

Activity

  1. added
    area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
    on Aug 31, 2026
  2. joperezr commented on Aug 31, 2026

    @joperezr
    Member
    🤖 Aspire triage pass — 2026-08-31 22:56 UTC

    Area: area-app-model, area-deployment
    Kind: feature-request (API proposal)
    Repro: partial — proposal includes proposed API + usage examples, not a bug repro
    Affected version(s): not version-bound (design proposal against current Aspire.Hosting.Kubernetes Gateway API model)
    Severity: S3 — missing capability with a working workaround (author states they hand-roll the HTTPRoute YAML today; see the "Alternative Designs" section)

    Urgency: ⚪ backlog — no milestone
    Reason: feature request / API proposal — belongs in API review + backlog while the current release stabilizes; not a regression or must-fix bug

    Suggested owner(s): Mitch Denny (@mitchdenny) (K8s gateway/routing), backup Sébastien Ros (@sebastienros) (K8s manifest/ATS)
    Evidence:

    Confidence: high
    Needs human?: no — clean feature-request/API-proposal classification (will still need standard API review before implementation)

  3. added 2 commits that reference this issue on Sep 1, 2026
    9c26579
    f2d827f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplicationarea-deploymenttriage:bot-seenAspire triage bot has seen this issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions