Skip to content

Commit 6295976

Browse files
authored
Merge pull request #94 from kgateway-dev/issue_59_13
agentgateway emitter: add service-upstream backend mapping
2 parents e14c2b3 + 4073202 commit 6295976

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
@@ -415,6 +415,33 @@ These are projected into an `AgentgatewayPolicy` by setting:
415415
- `client-body-buffer-size` is used as a fallback when `proxy-body-size` is unset.
416416
- The selected quantity is resolved to bytes for `maxBufferSize`.
417417

418+
#### Service Upstream
419+
420+
The agentgateway emitter supports projecting Service-upstream behavior via:
421+
422+
- `nginx.ingress.kubernetes.io/service-upstream: "true"`
423+
424+
This is projected by emitting one `AgentgatewayBackend` per covered backend Service and rewriting covered
425+
`HTTPRoute.spec.rules[].backendRefs[]` entries to reference that `AgentgatewayBackend`.
426+
427+
Mappings:
428+
429+
- `<service>` backendRef → `AgentgatewayBackend` named `<service>-service-upstream`
430+
- `AgentgatewayBackend.spec.static.host` → `<service>.<namespace>.svc.cluster.local`
431+
- `AgentgatewayBackend.spec.static.port` → original HTTPRoute backendRef port
432+
- rewritten HTTPRoute backendRef:
433+
- `group: agentgateway.dev`
434+
- `kind: AgentgatewayBackend`
435+
- `name: <service>-service-upstream`
436+
- `port` removed (port is defined on `AgentgatewayBackend.spec.static.port`)
437+
438+
**Notes:**
439+
440+
- Rewrites only apply to core Service backendRefs (empty group and kind `Service` / unset kind).
441+
- If the provider could not determine an explicit backendRef port, that backendRef is skipped.
442+
- `backend-protocol` remains a Service-targeted `AgentgatewayPolicy` and continues to control upstream HTTP version
443+
(`spec.backend.http.version`) independently from `service-upstream`.
444+
418445
## AgentgatewayPolicy Projection
419446

420447
Rate limit, timeout, CORS, rewrite target, access log, etc. annotations are converted into AgentgatewayPolicy resources.
@@ -484,8 +511,3 @@ to the output extensions list.
484511

485512
- Only the **ingress-nginx provider** is currently supported by the Agentgateway emitter.
486513
- Regex path matching is not currently implemented for agentgateway output.
487-
488-
## Future Work
489-
490-
The code defines GVKs for additional agentgateway extension types (e.g. `AgentgatewayBackend`), but they are not
491-
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

@@ -163,6 +166,16 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
163166
backendPolicies,
164167
)
165168

169+
// service-upstream maps to AgentgatewayBackend (spec.static) and rewrites HTTPRoute backendRefs.
170+
// Keep this after Service-targeted backend policy projection so those policies still target Services.
171+
applyServiceUpstream(
172+
pol,
173+
polSourceIngressName,
174+
httpRouteKey,
175+
&httpRouteContext,
176+
agentgatewayBackends,
177+
)
178+
166179
// BasicAuth maps to AgentgatewayPolicy.spec.traffic.basicAuthentication.
167180
// Note: agentgateway expects htpasswd content under a '.htaccess' key; see BasicAuthentication docs.
168181
if applyBasicAuthPolicy(pol, polSourceIngressName, httpRouteKey.Namespace, agentgatewayPolicies) {
@@ -333,6 +346,11 @@ func (e *Emitter) Emit(ir emitterir.EmitterIR) (i2gw.GatewayResources, field.Err
333346
agentgatewayObjs = append(agentgatewayObjs, ap)
334347
}
335348

349+
// Collect AgentgatewayBackends generated by service-upstream.
350+
for _, be := range agentgatewayBackends {
351+
agentgatewayObjs = append(agentgatewayObjs, be)
352+
}
353+
336354
// Sort by Kind, then Namespace, then Name to make output deterministic for testing.
337355
sort.SliceStable(agentgatewayObjs, func(i, j int) bool {
338356
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
@@ -492,3 +492,29 @@ func TestAgentgatewayIngressNginxIntegration_SSLRedirect(t *testing.T) {
492492
})
493493
}
494494
}
495+
496+
func TestAgentgatewayIngressNginxIntegration_ServiceUpstream(t *testing.T) {
497+
t.Helper()
498+
499+
tests := []struct {
500+
name string
501+
inputRel string
502+
goldenRel string
503+
}{
504+
{
505+
name: "service_upstream",
506+
inputRel: filepath.Join(
507+
"pkg", "i2gw", "emitters", "agentgateway", "testing", "testdata", "input", "service_upstream.yaml",
508+
),
509+
goldenRel: filepath.Join(
510+
"pkg", "i2gw", "emitters", "agentgateway", "testing", "testdata", "output", "service_upstream.yaml",
511+
),
512+
},
513+
}
514+
515+
for _, tt := range tests {
516+
t.Run(tt.name, func(t *testing.T) {
517+
runGoldenTest(t, tt.inputRel, tt.goldenRel)
518+
})
519+
}
520+
}
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)