Skip to content

Commit d065707

Browse files
authored
Merge pull request #16 from danehans/svc_up
Adds Support for `service-upstream` Ingress-Nginx Annotation
2 parents 646fab0 + 7dacc14 commit d065707

10 files changed

Lines changed: 561 additions & 4 deletions

File tree

pkg/i2gw/implementations/kgateway/README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ The command should generate Gateway API and Kgateway resources.
7373
- `nginx.ingress.kubernetes.io/session-cookie-expires`: Sets the TTL/expiration time for the cookie. Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.ttl`.
7474
- `nginx.ingress.kubernetes.io/session-cookie-max-age`: Sets the TTL/expiration time for the cookie (takes precedence over `session-cookie-expires`). Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.ttl`.
7575
- `nginx.ingress.kubernetes.io/session-cookie-secure`: Sets the Secure flag on the cookie. Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.secure`.
76+
- `nginx.ingress.kubernetes.io/service-upstream`: When set to `"true"`, configures Kgateway to route to the Service’s cluster IP (or equivalent static host) instead of individual Pod IPs. For each covered Service, the emitter creates a `Backend` resource with `spec.type: Static` and rewrites the corresponding `HTTPRoute.spec.rules[].backendRefs[]` to reference that `Backend` (group `gateway.kgateway.dev`, kind `Backend`).
7677

7778
### External Auth
7879

@@ -123,12 +124,26 @@ Currently supported:
123124
If multiple Ingresses target the same Service with conflicting `proxy-connect-timeout` values,
124125
the lowest timeout wins and a warning is emitted.
125126

127+
## Backend Projection
128+
129+
Annotations that change how upstreams are represented (rather than how they are load balanced or configured)
130+
can be projected into Kgateway `Backend` resources.
131+
132+
Currently supported:
133+
134+
- `nginx.ingress.kubernetes.io/service-upstream`:
135+
- For each Service backend covered by an Ingress with `service-upstream: "true"`, the emitter creates a `Backend` with:
136+
- `spec.type: Static`
137+
- `spec.static.hosts` containing a single `{host, port}` entry derived from the Service (e.g. `myservice.default.svc.cluster.local:80`).
138+
- Matching `HTTPRoute.spec.rules[].backendRefs[]` are rewritten to reference this `Backend` instead of the core Service.
139+
126140
### Summary of Policy Types
127141

128-
| Annotation Type | Kgateway Resource |
129-
|------------------------------|-----------------------|
130-
| Request/response behavior | `TrafficPolicy` |
131-
| Upstream connection behavior | `BackendConfigPolicy` |
142+
| Annotation Type | Kgateway Resource |
143+
|------------------------------------|-----------------------|
144+
| Request/response behavior | `TrafficPolicy` |
145+
| Upstream connection behavior | `BackendConfigPolicy` |
146+
| Upstream representation (static IP)| `Backend` |
132147

133148
## Limitations
134149

pkg/i2gw/implementations/kgateway/emitter.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
8181
// Track GatewayExtensions per ingress name (for external auth).
8282
gatewayExtensions := map[string]*kgateway.GatewayExtension{}
8383

84+
// Track Backends per (namespace, svcName) for service-upstream.
85+
staticBackends := map[types.NamespacedName]*kgateway.Backend{}
86+
8487
for httpRouteKey, httpRouteContext := range ir.HTTPRoutes {
8588
ingx := httpRouteContext.ProviderSpecificIR.IngressNginx
8689
if ingx == nil {
@@ -150,6 +153,15 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
150153
backendCfg,
151154
)
152155

156+
// Apply service-upstream via Backend and HTTPRoute backendRef rewrites.
157+
applyServiceUpstreamBackend(
158+
pol,
159+
polSourceIngressName,
160+
httpRouteKey,
161+
&httpRouteContext,
162+
staticBackends,
163+
)
164+
153165
// Apply enable-access-log via HTTPListenerPolicy.
154166
applyAccessLogPolicy(
155167
pol,
@@ -219,6 +231,11 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
219231
}
220232
}
221233

234+
// Collect all static Backends computed across HTTPRoutes.
235+
for _, b := range staticBackends {
236+
out = append(out, b)
237+
}
238+
222239
// Collect all BackendConfigPolicies computed across HTTPRoutes.
223240
for _, bcp := range backendCfg {
224241
out = append(out, bcp)

pkg/i2gw/implementations/kgateway/emitter_integration_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,15 @@ func TestKgatewayIngressNginxIntegration_Golden(t *testing.T) {
233233
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "output", "external_auth.yaml",
234234
),
235235
},
236+
{
237+
name: "service_upstream",
238+
inputRel: filepath.Join(
239+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "input", "service_upstream.yaml",
240+
),
241+
goldenRel: filepath.Join(
242+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "output", "service_upstream.yaml",
243+
),
244+
},
236245
}
237246

238247
for _, tt := range tests {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
Copyright 2023 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 kgateway
18+
19+
import (
20+
"github.com/kgateway-dev/ingress2gateway/pkg/i2gw/intermediate"
21+
kgw "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/kgateway"
22+
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/types"
25+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
26+
)
27+
28+
// applyServiceUpstreamBackend projects provider-specific static backend mappings
29+
// into typed Kgateway Backend CRs and rewrites HTTPRoute backendRefs to reference
30+
// those Backends.
31+
//
32+
// Semantics:
33+
// - One Backend CR per (namespace, svcName-service-upstream)
34+
// - Backend.Spec.Static.Hosts contains a single host+port
35+
// - HTTPRoute backendRefs for those services are rewritten to:
36+
// group: gateway.kgateway.dev
37+
// kind: Backend
38+
// name: <svc>-service-upstream
39+
//
40+
// Returns true if it mutated the HTTPRouteContext or produced a Backend CR.
41+
func applyServiceUpstreamBackend(
42+
pol intermediate.Policy,
43+
ingressName string,
44+
httpRouteKey types.NamespacedName,
45+
httpRouteCtx *intermediate.HTTPRouteContext,
46+
backends map[types.NamespacedName]*kgw.Backend,
47+
) {
48+
if len(pol.Backends) == 0 || len(pol.RuleBackendSources) == 0 {
49+
return
50+
}
51+
52+
for _, idx := range pol.RuleBackendSources {
53+
// Validate indices
54+
if idx.Rule >= len(httpRouteCtx.Spec.Rules) {
55+
continue
56+
}
57+
rule := &httpRouteCtx.Spec.Rules[idx.Rule]
58+
if idx.Backend >= len(rule.BackendRefs) {
59+
continue
60+
}
61+
62+
br := &rule.BackendRefs[idx.Backend]
63+
64+
// Only core Services
65+
if br.BackendRef.Group != nil && *br.BackendRef.Group != "" {
66+
continue
67+
}
68+
if br.BackendRef.Kind != nil && *br.BackendRef.Kind != "Service" {
69+
continue
70+
}
71+
if br.BackendRef.Name == "" {
72+
continue
73+
}
74+
75+
svcName := string(br.BackendRef.Name)
76+
77+
backendName := svcName + "-service-upstream"
78+
backendKey := types.NamespacedName{
79+
Namespace: httpRouteKey.Namespace,
80+
Name: backendName,
81+
}
82+
83+
// Find provider-produced backend metadata
84+
be, ok := pol.Backends[backendKey]
85+
if !ok {
86+
continue
87+
}
88+
89+
// Create or reuse typed Backend CR
90+
kb, exists := backends[backendKey]
91+
if !exists {
92+
kb = &kgw.Backend{
93+
TypeMeta: metav1.TypeMeta{
94+
Kind: BackendGVK.Kind,
95+
APIVersion: BackendGVK.GroupVersion().String(),
96+
},
97+
ObjectMeta: metav1.ObjectMeta{
98+
Name: backendKey.Name,
99+
Namespace: backendKey.Namespace,
100+
Labels: map[string]string{
101+
"ingress2gateway.kubernetes.io/source-ingress": ingressName,
102+
},
103+
},
104+
Spec: kgw.BackendSpec{
105+
Type: kgw.BackendTypeStatic,
106+
Static: &kgw.StaticBackend{
107+
Hosts: []kgw.Host{
108+
{
109+
Host: be.Host,
110+
Port: gwv1.PortNumber(be.Port),
111+
},
112+
},
113+
},
114+
},
115+
}
116+
backends[backendKey] = kb
117+
}
118+
119+
// Rewrite BackendRef to point to this Backend
120+
group := gwv1.Group(BackendGVK.Group)
121+
kind := gwv1.Kind(BackendGVK.Kind)
122+
123+
br.BackendRef.Group = &group
124+
br.BackendRef.Kind = &kind
125+
br.BackendRef.Name = gwv1.ObjectName(backendKey.Name)
126+
br.BackendRef.Port = nil // backend controls port
127+
}
128+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: Ingress
3+
metadata:
4+
annotations:
5+
ingress2gateway.kubernetes.io/implementation: kgateway
6+
nginx.ingress.kubernetes.io/service-upstream: "true"
7+
name: ingress-myservicea1
8+
namespace: default
9+
spec:
10+
ingressClassName: nginx
11+
rules:
12+
- host: myservicea.foo.org
13+
http:
14+
paths:
15+
- backend:
16+
service:
17+
name: myservicea
18+
port:
19+
number: 80
20+
path: /
21+
pathType: Prefix
22+
---
23+
# No service-upstream annotation here so a Backend won't be created for this Ingress.
24+
apiVersion: networking.k8s.io/v1
25+
kind: Ingress
26+
metadata:
27+
annotations:
28+
ingress2gateway.kubernetes.io/implementation: kgateway
29+
name: ingress-myservicea2
30+
namespace: default
31+
spec:
32+
ingressClassName: nginx
33+
rules:
34+
- host: myservicea.foo.org
35+
http:
36+
paths:
37+
- backend:
38+
service:
39+
name: myservicea
40+
port:
41+
number: 80
42+
path: /2
43+
pathType: Prefix
44+
---
45+
apiVersion: networking.k8s.io/v1
46+
kind: Ingress
47+
metadata:
48+
annotations:
49+
ingress2gateway.kubernetes.io/implementation: kgateway
50+
nginx.ingress.kubernetes.io/service-upstream: "true"
51+
name: ingress-myserviceb
52+
namespace: default
53+
spec:
54+
ingressClassName: nginx
55+
rules:
56+
- host: myserviceb.foo.org
57+
http:
58+
paths:
59+
- backend:
60+
service:
61+
name: myserviceb
62+
port:
63+
number: 80
64+
path: /
65+
pathType: Prefix
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
apiVersion: gateway.networking.k8s.io/v1
2+
kind: Gateway
3+
metadata:
4+
annotations:
5+
gateway.networking.k8s.io/generator: ingress2gateway-dev
6+
name: nginx
7+
namespace: default
8+
spec:
9+
gatewayClassName: kgateway
10+
listeners:
11+
- hostname: myservicea.foo.org
12+
name: myservicea-foo-org-http
13+
port: 80
14+
protocol: HTTP
15+
- hostname: myserviceb.foo.org
16+
name: myserviceb-foo-org-http
17+
port: 80
18+
protocol: HTTP
19+
status: {}
20+
---
21+
apiVersion: gateway.networking.k8s.io/v1
22+
kind: HTTPRoute
23+
metadata:
24+
annotations:
25+
gateway.networking.k8s.io/generator: ingress2gateway-dev
26+
name: ingress-myservicea1-myservicea-foo-org
27+
namespace: default
28+
spec:
29+
hostnames:
30+
- myservicea.foo.org
31+
parentRefs:
32+
- name: nginx
33+
rules:
34+
# From ingress-myservicea1 (service-upstream=true) so rewritten to Backend
35+
- backendRefs:
36+
- group: gateway.kgateway.dev
37+
kind: Backend
38+
name: myservicea-service-upstream
39+
matches:
40+
- path:
41+
type: PathPrefix
42+
value: /
43+
# From ingress-myservicea2 (no service-upstream) so still core Service
44+
- backendRefs:
45+
- name: myservicea
46+
port: 80
47+
matches:
48+
- path:
49+
type: PathPrefix
50+
value: /2
51+
status:
52+
parents: []
53+
---
54+
apiVersion: gateway.networking.k8s.io/v1
55+
kind: HTTPRoute
56+
metadata:
57+
annotations:
58+
gateway.networking.k8s.io/generator: ingress2gateway-dev
59+
name: ingress-myserviceb-myserviceb-foo-org
60+
namespace: default
61+
spec:
62+
hostnames:
63+
- myserviceb.foo.org
64+
parentRefs:
65+
- name: nginx
66+
rules:
67+
# From ingress-myserviceb (service-upstream=true) so rewritten to Backend
68+
- backendRefs:
69+
- group: gateway.kgateway.dev
70+
kind: Backend
71+
name: myserviceb-service-upstream
72+
matches:
73+
- path:
74+
type: PathPrefix
75+
value: /
76+
status:
77+
parents: []
78+
---
79+
apiVersion: gateway.kgateway.dev/v1alpha1
80+
kind: Backend
81+
metadata:
82+
labels:
83+
ingress2gateway.kubernetes.io/source-ingress: ingress-myservicea1
84+
name: myservicea-service-upstream
85+
namespace: default
86+
spec:
87+
type: Static
88+
static:
89+
hosts:
90+
- host: myservicea.default.svc.cluster.local
91+
port: 80
92+
status: {}
93+
---
94+
apiVersion: gateway.kgateway.dev/v1alpha1
95+
kind: Backend
96+
metadata:
97+
labels:
98+
ingress2gateway.kubernetes.io/source-ingress: ingress-myserviceb
99+
name: myserviceb-service-upstream
100+
namespace: default
101+
spec:
102+
type: Static
103+
static:
104+
hosts:
105+
- host: myserviceb.default.svc.cluster.local
106+
port: 80
107+
status: {}

0 commit comments

Comments
 (0)