Skip to content

Commit ce53f4c

Browse files
committed
agentgateway emitter: add service-upstream backends
Project ingress-nginx service-upstream annotations into AgentgatewayBackend\nresources and rewrite covered HTTPRoute backendRefs to target those\nbackends. This keeps service-scoped backend protocol policy behavior\nintact while enabling static upstream routing semantics for\nagentgateway output.\n\nAdd an agentgateway integration fixture and golden output for\nservice_upstream, and update emitter/provider documentation to describe\nthe new mapping. Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>
1 parent 2bab127 commit ce53f4c

7 files changed

Lines changed: 384 additions & 6 deletions

File tree

pkg/i2gw/emitters/agentgateway/README.md

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,33 @@ This is projected into a **Service-targeted** `AgentgatewayPolicy` by setting:
382382
- The provider currently maps only gRPC-family values into policy IR, so the agentgateway emitter currently emits
383383
only `HTTP2` for this feature.
384384

385+
#### Service Upstream
386+
387+
The agentgateway emitter supports projecting Service-upstream behavior via:
388+
389+
- `nginx.ingress.kubernetes.io/service-upstream: "true"`
390+
391+
This is projected by emitting one `AgentgatewayBackend` per covered backend Service and rewriting covered
392+
`HTTPRoute.spec.rules[].backendRefs[]` entries to reference that `AgentgatewayBackend`.
393+
394+
Mappings:
395+
396+
- `<service>` backendRef → `AgentgatewayBackend` named `<service>-service-upstream`
397+
- `AgentgatewayBackend.spec.static.host` → `<service>.<namespace>.svc.cluster.local`
398+
- `AgentgatewayBackend.spec.static.port` → original HTTPRoute backendRef port
399+
- rewritten HTTPRoute backendRef:
400+
- `group: agentgateway.dev`
401+
- `kind: AgentgatewayBackend`
402+
- `name: <service>-service-upstream`
403+
- `port` removed (port is defined on `AgentgatewayBackend.spec.static.port`)
404+
405+
**Notes:**
406+
407+
- Rewrites only apply to core Service backendRefs (empty group and kind `Service` / unset kind).
408+
- If the provider could not determine an explicit backendRef port, that backendRef is skipped.
409+
- `backend-protocol` remains a Service-targeted `AgentgatewayPolicy` and continues to control upstream HTTP version
410+
(`spec.backend.http.version`) independently from `service-upstream`.
411+
385412
## AgentgatewayPolicy Projection
386413

387414
Rate limit, timeout, CORS, rewrite target, etc. annotations are converted into AgentgatewayPolicy resources.
@@ -451,8 +478,3 @@ to the output extensions list.
451478

452479
- Only the **ingress-nginx provider** is currently supported by the Agentgateway emitter.
453480
- Regex path matching is not currently implemented for agentgateway output.
454-
455-
## Future Work
456-
457-
The code defines GVKs for additional agentgateway extension types (e.g. `AgentgatewayBackend`), but they are not
458-
yet emitted by the current implementation.

pkg/i2gw/emitters/agentgateway/emitter.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
6868
// Track backend-scoped AgentgatewayPolicies per Service (ns/name) (e.g. TLS, connect timeout)
6969
backendPolicies := map[types.NamespacedName]*agentgatewayv1alpha1.AgentgatewayPolicy{}
7070

71+
// Track AgentgatewayBackends per Service-upstream backend key (ns/name).
72+
agentgatewayBackends := map[types.NamespacedName]*agentgatewayv1alpha1.AgentgatewayBackend{}
73+
7174
// Track HTTPRoutes that need SSL redirect splitting
7275
routesToSplitForSSLRedirect := map[types.NamespacedName]bool{}
7376

@@ -158,6 +161,16 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
158161
backendPolicies,
159162
)
160163

164+
// service-upstream maps to AgentgatewayBackend (spec.static) and rewrites HTTPRoute backendRefs.
165+
// Keep this after Service-targeted backend policy projection so those policies still target Services.
166+
applyServiceUpstream(
167+
pol,
168+
polSourceIngressName,
169+
httpRouteKey,
170+
&httpRouteContext,
171+
agentgatewayBackends,
172+
)
173+
161174
// BasicAuth maps to AgentgatewayPolicy.spec.traffic.basicAuthentication.
162175
// Note: agentgateway expects htpasswd content under a '.htaccess' key; see BasicAuthentication docs.
163176
if applyBasicAuthPolicy(pol, polSourceIngressName, httpRouteKey.Namespace, agentgatewayPolicies) {
@@ -326,6 +339,11 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
326339
agentgatewayObjs = append(agentgatewayObjs, ap)
327340
}
328341

342+
// Collect AgentgatewayBackends generated by service-upstream.
343+
for _, be := range agentgatewayBackends {
344+
agentgatewayObjs = append(agentgatewayObjs, be)
345+
}
346+
329347
// Sort by Kind, then Namespace, then Name to make output deterministic for testing.
330348
sort.SliceStable(agentgatewayObjs, func(i, j int) bool {
331349
oi, oj := agentgatewayObjs[i], agentgatewayObjs[j]

pkg/i2gw/emitters/agentgateway/emitter_integration_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,3 +440,29 @@ func TestAgentgatewayIngressNginxIntegration_SSLRedirect(t *testing.T) {
440440
})
441441
}
442442
}
443+
444+
func TestAgentgatewayIngressNginxIntegration_ServiceUpstream(t *testing.T) {
445+
t.Helper()
446+
447+
tests := []struct {
448+
name string
449+
inputRel string
450+
goldenRel string
451+
}{
452+
{
453+
name: "service_upstream",
454+
inputRel: filepath.Join(
455+
"pkg", "i2gw", "emitters", "agentgateway", "testing", "testdata", "input", "service_upstream.yaml",
456+
),
457+
goldenRel: filepath.Join(
458+
"pkg", "i2gw", "emitters", "agentgateway", "testing", "testdata", "output", "service_upstream.yaml",
459+
),
460+
},
461+
}
462+
463+
for _, tt := range tests {
464+
t.Run(tt.name, func(t *testing.T) {
465+
runGoldenTest(t, tt.inputRel, tt.goldenRel)
466+
})
467+
}
468+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package agentgateway
18+
19+
import (
20+
emitterir "github.com/kgateway-dev/ingress2gateway/pkg/i2gw/emitter_intermediate"
21+
22+
agentgatewayv1alpha1 "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/types"
25+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
26+
)
27+
28+
const sourceIngressAnnotation = "ingress2gateway.kubernetes.io/source-ingress"
29+
30+
// applyServiceUpstream projects provider service-upstream backend metadata into
31+
// AgentgatewayBackend CRs and rewrites HTTPRoute backendRefs to target them.
32+
func applyServiceUpstream(
33+
pol emitterir.Policy,
34+
ingressName string,
35+
httpRouteKey types.NamespacedName,
36+
httpRouteCtx *emitterir.HTTPRouteContext,
37+
backends map[types.NamespacedName]*agentgatewayv1alpha1.AgentgatewayBackend,
38+
) {
39+
if len(pol.Backends) == 0 || len(pol.RuleBackendSources) == 0 {
40+
return
41+
}
42+
43+
for _, idx := range pol.RuleBackendSources {
44+
if idx.Rule >= len(httpRouteCtx.Spec.Rules) {
45+
continue
46+
}
47+
rule := &httpRouteCtx.Spec.Rules[idx.Rule]
48+
if idx.Backend >= len(rule.BackendRefs) {
49+
continue
50+
}
51+
52+
br := &rule.BackendRefs[idx.Backend]
53+
54+
// Only core Services are eligible for service-upstream rewriting.
55+
if br.BackendRef.Group != nil && *br.BackendRef.Group != "" {
56+
continue
57+
}
58+
if br.BackendRef.Kind != nil && *br.BackendRef.Kind != "Service" {
59+
continue
60+
}
61+
if br.BackendRef.Name == "" {
62+
continue
63+
}
64+
65+
svcName := string(br.BackendRef.Name)
66+
backendKey := serviceUpstreamBackendKey(httpRouteKey.Namespace, svcName)
67+
68+
be, ok := pol.Backends[backendKey]
69+
if !ok {
70+
continue
71+
}
72+
73+
agb := ensureServiceUpstreamBackend(
74+
ingressName,
75+
backendKey,
76+
be.Host,
77+
be.Port,
78+
backends,
79+
)
80+
81+
// Rewrite HTTPRoute backendRef to point at the AgentgatewayBackend.
82+
group := gatewayv1.Group(AgentgatewayBackendGVK.Group)
83+
kind := gatewayv1.Kind(AgentgatewayBackendGVK.Kind)
84+
85+
br.BackendRef.Group = &group
86+
br.BackendRef.Kind = &kind
87+
br.BackendRef.Name = gatewayv1.ObjectName(agb.Name)
88+
// Port is defined by AgentgatewayBackend.spec.static.
89+
br.BackendRef.Port = nil
90+
}
91+
}
92+
93+
func serviceUpstreamBackendKey(ns, svcName string) types.NamespacedName {
94+
return types.NamespacedName{
95+
Namespace: ns,
96+
Name: svcName + "-service-upstream",
97+
}
98+
}
99+
100+
func ensureServiceUpstreamBackend(
101+
ingressName string,
102+
backendKey types.NamespacedName,
103+
host string,
104+
port int32,
105+
backends map[types.NamespacedName]*agentgatewayv1alpha1.AgentgatewayBackend,
106+
) *agentgatewayv1alpha1.AgentgatewayBackend {
107+
if be, ok := backends[backendKey]; ok {
108+
return be
109+
}
110+
111+
be := &agentgatewayv1alpha1.AgentgatewayBackend{
112+
TypeMeta: metav1.TypeMeta{
113+
Kind: AgentgatewayBackendGVK.Kind,
114+
APIVersion: AgentgatewayBackendGVK.GroupVersion().String(),
115+
},
116+
ObjectMeta: metav1.ObjectMeta{
117+
Name: backendKey.Name,
118+
Namespace: backendKey.Namespace,
119+
Labels: map[string]string{
120+
sourceIngressAnnotation: ingressName,
121+
},
122+
},
123+
Spec: agentgatewayv1alpha1.AgentgatewayBackendSpec{
124+
Static: &agentgatewayv1alpha1.StaticBackend{
125+
Host: agentgatewayv1alpha1.ShortString(host),
126+
Port: port,
127+
},
128+
},
129+
}
130+
131+
backends[backendKey] = be
132+
return be
133+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: Ingress
3+
metadata:
4+
annotations:
5+
nginx.ingress.kubernetes.io/service-upstream: "true"
6+
name: ingress-myservicea1
7+
namespace: default
8+
spec:
9+
ingressClassName: nginx
10+
rules:
11+
- host: myservicea.foo.org
12+
http:
13+
paths:
14+
- backend:
15+
service:
16+
name: myservicea
17+
port:
18+
number: 80
19+
path: /
20+
pathType: Prefix
21+
---
22+
# No service-upstream annotation here so an AgentgatewayBackend won't be created for this Ingress.
23+
apiVersion: networking.k8s.io/v1
24+
kind: Ingress
25+
metadata:
26+
annotations:
27+
name: ingress-myservicea2
28+
namespace: default
29+
spec:
30+
ingressClassName: nginx
31+
rules:
32+
- host: myservicea.foo.org
33+
http:
34+
paths:
35+
- backend:
36+
service:
37+
name: myservicea
38+
port:
39+
number: 80
40+
path: /2
41+
pathType: Prefix
42+
---
43+
apiVersion: networking.k8s.io/v1
44+
kind: Ingress
45+
metadata:
46+
annotations:
47+
nginx.ingress.kubernetes.io/service-upstream: "true"
48+
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
49+
name: ingress-myserviceb
50+
namespace: default
51+
spec:
52+
ingressClassName: nginx
53+
rules:
54+
- host: myserviceb.foo.org
55+
http:
56+
paths:
57+
- backend:
58+
service:
59+
name: myserviceb
60+
port:
61+
number: 80
62+
path: /
63+
pathType: Prefix

0 commit comments

Comments
 (0)