[API Proposal] Attach an HTTPRoute to a pre-existing / cross-namespace Gateway, and model HTTPRoute filters (URLRewrite)
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
## 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`:
```csharp
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):
```diff
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:
```diff
+public static IResourceBuilder AsExisting(
+ this IResourceBuilder 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):
```diff
public sealed class HttpRouteRuleV1
{
[YamlMember(Alias = "matches")] public List Matches { get; } = [];
+ [YamlMember(Alias = "filters")] public List Filters { get; } = [];
[YamlMember(Alias = "backendRefs")] public List 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
```csharp
var gateway = env.AddGateway("platform")
.AsExisting("platform-gateway", "gateway-system");
gateway.WithRoute("/my-app", myService, rewritePrefix: "/");
```
```yaml
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`. Per the Gateway API spec
(`apis/v1/httproute_types.go`): *"Wherever possible, implementations SHOULD implement filters in
the order they are specified."*
Contributor guide
Research direction
Start with Resources/HttpRouteV1.cs and the emission logic in KubernetesEnvironmentResource.cs around lines 1080, 1109, 1133, and 1155. Review the existing StringBuilder HTTPRoute YAML and its snapshot tests, then compare the proposed parentRef, AsExisting, and URLRewrite model changes. Done means existing manifests remain unchanged while route-only output and the proposed filter fields are represented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, kubernetes
- Domain
- api, cloud, infrastructure
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100