Skip to content

Commit b15f217

Browse files
committed
Adds Support for use-regex and rewrite-target Annotations
Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>
1 parent 877c6be commit b15f217

17 files changed

Lines changed: 1181 additions & 0 deletions

pkg/i2gw/implementations/kgateway/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ The command should generate Gateway API and Kgateway resources.
6262
- `nginx.ingress.kubernetes.io/ssl-redirect`: When set to `"true"`, adds a `RequestRedirect` filter to HTTPRoute rules that redirects HTTP to HTTPS with a 301 status code.
6363
- `nginx.ingress.kubernetes.io/force-ssl-redirect`: When set to `"true"`, adds a `RequestRedirect` filter to HTTPRoute rules that redirects HTTP to HTTPS with a 301 status code. Treated identically to `ssl-redirect`.
6464
- `nginx.ingress.kubernetes.io/ssl-passthrough`: When set to `"true"`, enables TLS passthrough mode. Converts the Ingress to a `TLSRoute` with a Gateway listener using `protocol: TLS` and `tls.mode: Passthrough`. The HTTPRoute that would normally be created is removed.
65+
- `nginx.ingress.kubernetes.io/use-regex`: When set to `"true"`, indicates that the paths defined on an Ingress should be treated as regular expressions.
66+
Uses host-group semantics: if any Ingress contributing rules for a given host has `use-regex: "true"`, regex-style path matching is enforced on **all**
67+
paths for that host (across all contributing Ingresses).
68+
- `nginx.ingress.kubernetes.io/rewrite-target`: Rewrites the request path using regex rewrite semantics.
69+
Uses host-group semantics: if any Ingress contributing rules for a given host sets `rewrite-target`, regex-style path matching is enforced on **all**
70+
paths for that host (across all contributing Ingresses), consistent with ingress-nginx behavior.
6571

6672
### Backend Behavior
6773

@@ -71,6 +77,8 @@ The command should generate Gateway API and Kgateway resources.
7177
- `nginx.ingress.kubernetes.io/affinity`: Enables session affinity (only "cookie" type is supported). Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies`.
7278
- `nginx.ingress.kubernetes.io/session-cookie-name`: Specifies the name of the cookie used for session affinity. Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.name`.
7379
- `nginx.ingress.kubernetes.io/session-cookie-path`: Defines the path that will be set on the cookie. Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.path`.
80+
- **Note (regex-mode constraint):** Ingress NGINX session cookie paths do not support regex. If regex-mode is enabled for a host (via `use-regex: "true"` or
81+
`rewrite-target`) and cookie affinity is used, `session-cookie-path` must be set; the provider validates this and emits an error if it is missing.
7482
- `nginx.ingress.kubernetes.io/session-cookie-domain`: Sets the Domain attribute of the sticky cookie. **Note:** This annotation is parsed but not currently mapped to kgateway as the Cookie type doesn't support domain.
7583
- `nginx.ingress.kubernetes.io/session-cookie-samesite`: Applies a SameSite attribute to the sticky cookie. Browser accepted values are None, Lax, and Strict. Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.sameSite`.
7684
- `nginx.ingress.kubernetes.io/session-cookie-expires`: Sets the TTL/expiration time for the cookie. Maps to `BackendConfigPolicy.spec.loadBalancer.ringHash.hashPolicies[].cookie.ttl`.
@@ -99,6 +107,29 @@ The command should generate Gateway API and Kgateway resources.
99107

100108
- `nginx.ingress.kubernetes.io/enable-access-log`: If enabled, will create an HTTPListenerPolicy that will configure a basic policy for envoy access logging. Maps to `HTTPListenerPolicy.spec.accessLog[].fileSink`. This can be further customized as needed, see [docs](https://kgateway.dev/docs/envoy/2.0.x/security/access-logging/).
101109

110+
### Regex Path Matching and Rewrites
111+
112+
- `use-regex` and `rewrite-target` may **mutate HTTPRoute path matching** for a host:
113+
- When regex-mode is enabled for a host, the emitter converts **all** `PathPrefix`/`Exact` matches under that host to `RegularExpression` matches.
114+
- For Ingresses that set `use-regex: "true"`, their contributed path strings are treated as **regex** (not escaped as literals).
115+
- For other Ingresses under the same host (that did not set `use-regex: "true"`), their contributed path strings are treated as **literals** within a regex
116+
match (escaped), to preserve the original non-regex intent.
117+
118+
- `rewrite-target` generates `TrafficPolicy` URL rewrite:
119+
- For each rule covered by an Ingress that sets `rewrite-target`, the emitter creates a **per-rule TrafficPolicy** named:
120+
- `<ingress-name>-rewrite-<rule-index>`
121+
- That policy sets:
122+
123+
```yaml
124+
spec:
125+
urlRewrite:
126+
pathRegex:
127+
pattern: <regex pattern derived from the HTTPRoute rule match>
128+
substitution: <rewrite-target value>
129+
```
130+
131+
- The policy is attached via `ExtensionRef` filters to only the covered backendRefs (partial coverage), rather than using `targetRefs`.
132+
102133
## TrafficPolicy Projection
103134

104135
Annotations in the **Traffic Behavior** category are converted into
@@ -169,6 +200,8 @@ Currently supported:
169200

170201
- Only the **ingress-nginx provider** is currently supported by the Kgateway emitter.
171202
- Some NGINX behaviors cannot be reproduced exactly due to Envoy/Kgateway differences.
203+
- Regex-mode is implemented by converting HTTPRoute path matches to `RegularExpression`. Some ingress-nginx details (such as case-insensitive `~*` behavior)
204+
may not be reproduced exactly depending on the underlying Gateway API/Envoy behavior and the patterns provided.
172205

173206
## Supported but not tranlated Annotations
174207

pkg/i2gw/implementations/kgateway/emitter.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
9393
// One TrafficPolicy per source Ingress name.
9494
tp := map[string]*kgateway.TrafficPolicy{}
9595

96+
// Apply host-wide regex enforcement first (so rule path regex is finalized)
97+
applyRegexPathMatchingForHost(ingx, &httpRouteContext)
98+
99+
// deterministic policy iteration
100+
policyNames := make([]string, 0, len(ingx.Policies))
101+
for name := range ingx.Policies {
102+
policyNames = append(policyNames, name)
103+
}
104+
sort.Strings(policyNames)
105+
106+
// Rewrite-target pass: creates per-rule TPs and attaches filters itself.
107+
for _, name := range policyNames {
108+
pol := ingx.Policies[name]
109+
applyRewriteTargetPolicies(pol, name, httpRouteKey.Namespace, &httpRouteContext, tp)
110+
}
111+
96112
for polSourceIngressName, pol := range ingx.Policies {
97113
// Normalize (rule, backend) coverage to unique pairs to avoid
98114
// generating duplicate filters on the same backendRef.

pkg/i2gw/implementations/kgateway/emitter_integration_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,33 @@ func TestKgatewayIngressNginxIntegration_Golden(t *testing.T) {
270270
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "output", "ssl_passthrough.yaml",
271271
),
272272
},
273+
{
274+
name: "use_regex",
275+
inputRel: filepath.Join(
276+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "input", "use_regex.yaml",
277+
),
278+
goldenRel: filepath.Join(
279+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "output", "use_regex.yaml",
280+
),
281+
},
282+
{
283+
name: "rewrite_target",
284+
inputRel: filepath.Join(
285+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "input", "rewrite_target.yaml",
286+
),
287+
goldenRel: filepath.Join(
288+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "output", "rewrite_target.yaml",
289+
),
290+
},
291+
{
292+
name: "rewrite_target_and_use_regex",
293+
inputRel: filepath.Join(
294+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "input", "rewrite_target_and_use_regex.yaml",
295+
),
296+
goldenRel: filepath.Join(
297+
"pkg", "i2gw", "implementations", "kgateway", "testing", "testdata", "output", "rewrite_target_and_use_regex.yaml",
298+
),
299+
},
273300
}
274301

275302
for _, tt := range tests {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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+
"fmt"
21+
"sort"
22+
23+
"github.com/kgateway-dev/ingress2gateway/pkg/i2gw/intermediate"
24+
"github.com/kgateway-dev/kgateway/v2/api/v1alpha1/kgateway"
25+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
26+
)
27+
28+
// applyRewriteTargetPolicies projects ingress-nginx rewrite-target into *per-rule* Kgateway TrafficPolicies
29+
// and attaches them via ExtensionRef filters to the covered backendRefs.
30+
//
31+
// Why per-rule?
32+
// - The regex rewrite pattern must align with the rule's path regex so capture groups ($1, $2, ...)
33+
// behave like ingress-nginx.
34+
//
35+
// Assumptions:
36+
// - applyRegexPathMatchingForHost(...) has already run (if host-wide regex location mode is enabled),
37+
// so rule path matches will already be RegularExpression where needed.
38+
func applyRewriteTargetPolicies(
39+
pol intermediate.Policy,
40+
sourceIngressName, namespace string,
41+
httpRouteCtx *intermediate.HTTPRouteContext,
42+
tp map[string]*kgateway.TrafficPolicy,
43+
) {
44+
if pol.RewriteTarget == nil || *pol.RewriteTarget == "" {
45+
return
46+
}
47+
if httpRouteCtx == nil {
48+
return
49+
}
50+
51+
// Group covered backendRefs by rule index.
52+
// ruleIdx -> set(backendIdx)
53+
byRule := map[int]map[int]struct{}{}
54+
for _, idx := range pol.RuleBackendSources {
55+
if idx.Rule < 0 || idx.Backend < 0 {
56+
continue
57+
}
58+
if _, ok := byRule[idx.Rule]; !ok {
59+
byRule[idx.Rule] = map[int]struct{}{}
60+
}
61+
byRule[idx.Rule][idx.Backend] = struct{}{}
62+
}
63+
if len(byRule) == 0 {
64+
return
65+
}
66+
67+
// Deterministic iteration for stable goldens.
68+
ruleIdxs := make([]int, 0, len(byRule))
69+
for r := range byRule {
70+
ruleIdxs = append(ruleIdxs, r)
71+
}
72+
sort.Ints(ruleIdxs)
73+
74+
for _, ruleIdx := range ruleIdxs {
75+
if ruleIdx >= len(httpRouteCtx.Spec.Rules) {
76+
continue
77+
}
78+
79+
pattern := deriveRulePathRegexPattern(httpRouteCtx.Spec.Rules[ruleIdx])
80+
81+
// Name is unique per ingress + rule, so we can safely create multiple TPs per ingress.
82+
tpName := fmt.Sprintf("%s-rewrite-%d", sourceIngressName, ruleIdx)
83+
t := ensureTrafficPolicy(tp, tpName, namespace)
84+
85+
t.Spec.UrlRewrite = &kgateway.URLRewrite{
86+
PathRegex: &kgateway.PathRegexRewrite{
87+
Pattern: pattern,
88+
Substitution: *pol.RewriteTarget,
89+
},
90+
}
91+
92+
// Attach this rewrite TP to every covered backendRef in the rule via ExtensionRef filter.
93+
backendSet := byRule[ruleIdx]
94+
backendIdxs := make([]int, 0, len(backendSet))
95+
for b := range backendSet {
96+
backendIdxs = append(backendIdxs, b)
97+
}
98+
sort.Ints(backendIdxs)
99+
100+
for _, backendIdx := range backendIdxs {
101+
if backendIdx >= len(httpRouteCtx.Spec.Rules[ruleIdx].BackendRefs) {
102+
continue
103+
}
104+
105+
httpRouteCtx.Spec.Rules[ruleIdx].BackendRefs[backendIdx].Filters =
106+
append(
107+
httpRouteCtx.Spec.Rules[ruleIdx].BackendRefs[backendIdx].Filters,
108+
gwv1.HTTPRouteFilter{
109+
Type: gwv1.HTTPRouteFilterExtensionRef,
110+
ExtensionRef: &gwv1.LocalObjectReference{
111+
Group: gwv1.Group(TrafficPolicyGVK.Group),
112+
Kind: gwv1.Kind(TrafficPolicyGVK.Kind),
113+
Name: gwv1.ObjectName(t.Name),
114+
},
115+
},
116+
)
117+
}
118+
}
119+
}
120+
121+
// deriveRulePathRegexPattern returns a single regex pattern for the rule if possible.
122+
// If the rule has:
123+
// - exactly one distinct RegularExpression path value -> return it
124+
// - zero or multiple distinct regex values -> fall back to "^(.*)"
125+
//
126+
// Note: If a rule has multiple *different* path regex matches, we can't represent
127+
// match-specific rewrites without splitting the rule, so we choose a safe fallback.
128+
func deriveRulePathRegexPattern(rule gwv1.HTTPRouteRule) string {
129+
patterns := map[string]struct{}{}
130+
131+
for i := range rule.Matches {
132+
m := rule.Matches[i]
133+
if m.Path == nil || m.Path.Type == nil || m.Path.Value == nil {
134+
continue
135+
}
136+
if *m.Path.Type != gwv1.PathMatchRegularExpression {
137+
continue
138+
}
139+
if v := *m.Path.Value; v != "" {
140+
patterns[v] = struct{}{}
141+
}
142+
}
143+
144+
if len(patterns) == 1 {
145+
for p := range patterns {
146+
return p
147+
}
148+
}
149+
150+
return "^(.*)"
151+
}
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+
ingress2gateway.kubernetes.io/implementation: kgateway
6+
nginx.ingress.kubernetes.io/rewrite-target: /rewritten
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+
apiVersion: networking.k8s.io/v1
24+
kind: Ingress
25+
metadata:
26+
annotations:
27+
ingress2gateway.kubernetes.io/implementation: kgateway
28+
name: ingress-myservicea2
29+
namespace: default
30+
spec:
31+
ingressClassName: nginx
32+
rules:
33+
- host: myservicea.foo.org
34+
http:
35+
paths:
36+
- backend:
37+
service:
38+
name: myservicea
39+
port:
40+
number: 80
41+
path: /2
42+
pathType: Prefix
43+
---
44+
apiVersion: networking.k8s.io/v1
45+
kind: Ingress
46+
metadata:
47+
annotations:
48+
ingress2gateway.kubernetes.io/implementation: kgateway
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
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
apiVersion: networking.k8s.io/v1
2+
kind: Ingress
3+
metadata:
4+
annotations:
5+
ingress2gateway.kubernetes.io/implementation: kgateway
6+
nginx.ingress.kubernetes.io/use-regex: "true"
7+
nginx.ingress.kubernetes.io/rewrite-target: /$1
8+
name: ingress-myservicea1
9+
namespace: default
10+
spec:
11+
ingressClassName: nginx
12+
rules:
13+
- host: myservicea.foo.org
14+
http:
15+
paths:
16+
- backend:
17+
service:
18+
name: myservicea
19+
port:
20+
number: 80
21+
path: /foo/(.*)
22+
pathType: Prefix
23+
---
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: /v1.+
43+
pathType: Prefix
44+
---
45+
apiVersion: networking.k8s.io/v1
46+
kind: Ingress
47+
metadata:
48+
annotations:
49+
ingress2gateway.kubernetes.io/implementation: kgateway
50+
name: ingress-myserviceb
51+
namespace: default
52+
spec:
53+
ingressClassName: nginx
54+
rules:
55+
- host: myserviceb.foo.org
56+
http:
57+
paths:
58+
- backend:
59+
service:
60+
name: myserviceb
61+
port:
62+
number: 80
63+
path: /
64+
pathType: Prefix

0 commit comments

Comments
 (0)