Skip to content

Commit 6da92d0

Browse files
authored
Implement request redirect filter in HTTPRoute rule (#218)
This commit implements the request redirect filter as part of the routing rule in the HTTPRoute. A common use-case for a request redirect is redirecting HTTP requests to HTTPS. The commit updates the HTTPS termination example to include HTTPS redirect configuration. Notes: - The experimental 'path' field of 'requestRedirect' is out of scope. - The validation of the fields of `requestRedirect` is not implemented. It is left to be done in a separate component responsible for validation with FIXMEs added to the relevant code locations. - If multiple redirect filters are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest. - NGINX will always redirect a request even if the request has already been redirected. Thus, any backendRefs defined in the routing rule will be ignored. However, that "always redirect" behavior is not specified by the Gateway API. As a result, we might need to change our implementation if different behavior becomes specified by the Gateway API in the future.
1 parent 2eec798 commit 6da92d0

File tree

11 files changed

+530
-121
lines changed

11 files changed

+530
-121
lines changed

docs/gateway-api-compatibility.md.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ Fields:
8787
* `headers` - partially supported. Only `Exact` type.
8888
* `queryParams` - partially supported. Only `Exact` type.
8989
* `method` - supported.
90-
* `filters` - not supported.
90+
* `filters`
91+
* `type` - supported.
92+
* `requestRedirect` - supported except for the experimental `path` field. If multiple filters with `requestRedirect` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest.
93+
* `requestHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported.
9194
* `backendRefs` - partially supported. Only a single backend ref without support for `weight`. Backend ref `filters` are not supported. NGINX Kubernetes Gateway will use the IP of the Service as a backend, not the IPs of the corresponding Pods. Watching for Service updates is not supported.
9295
* `status`
9396
* `parents`

examples/https-termination/README.md

+43-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# HTTPS Termination Example
22

3-
In this example we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes.
3+
In this example, we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes and an HTTPS redirect from port 80 to 443.
44

55
## Running the Example
66

@@ -14,10 +14,11 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin
1414
GW_IP=XXX.YYY.ZZZ.III
1515
```
1616

17-
1. Save the HTTPS port of NGINX Kubernetes Gateway:
17+
1. Save the ports of NGINX Kubernetes Gateway:
1818

1919
```
20-
GW_HTTPS_PORT=port
20+
GW_HTTP_PORT=<http port number>
21+
GW_HTTPS_PORT=<https port number>
2122
```
2223

2324
## 2. Deploy the Cafe Application
@@ -52,26 +53,60 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin
5253
kubectl apply -f gateway.yaml
5354
```
5455

55-
This [gateway](./gateway.yaml) configures an `https` listener is to terminate TLS connections using the `cafe-secret` we created in the step 1.
56+
This [Gateway](./gateway.yaml) configures:
57+
* `http` listener for HTTP traffic
58+
* `https` listener for HTTPS traffic. It terminates TLS connections using the `cafe-secret` we created in step 1.
5659

5760
1. Create the `HTTPRoute` resources:
5861
```
5962
kubectl apply -f cafe-routes.yaml
6063
```
6164

62-
To configure HTTPS termination for our cafe application, we will bind the `https` listener to our `HTTPRoutes` in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentReference`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference) field:
65+
To configure HTTPS termination for our cafe application, we will bind our `coffee` and `tea` HTTPRoutes to the `https` listener in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentReference`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.ParentReference) field:
6366

6467
```yaml
6568
parentRefs:
6669
- name: gateway
67-
namespace: default
6870
sectionName: https
6971
```
7072
73+
To configure an HTTPS redirect from port 80 to 443, we will bind the special `cafe-tls-redirect` HTTPRoute with a [`HTTPRequestRedirectFilter`](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRequestRedirectFilter) to the `http` listener:
74+
75+
```yaml
76+
parentRefs:
77+
- name: gateway
78+
sectionName: http
79+
```
80+
7181
## 4. Test the Application
7282

73-
To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services.
74-
Since our certificate is self-signed, we'll use curl's `--insecure` option to turn off certificate verification.
83+
To access the application, we will use `curl` to send requests to the `coffee` and `tea` Services. First, we will access the application over HTTP to test that the HTTPS redirect works. Then we will use HTTPS.
84+
85+
### 4.1 Test HTTPS Redirect
86+
87+
To test that NGINX sends an HTTPS redirect, we will send requests to the `coffee` and `tea` Services on HTTP port. We will use curl's `--include` option to print the response headers (we are interested in the `Location` header).
88+
89+
To get a redirect for coffee:
90+
```
91+
curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/coffee --include
92+
HTTP/1.1 302 Moved Temporarily
93+
...
94+
Location: https://cafe.example.com:443/coffee
95+
...
96+
```
97+
98+
To get a redirect for tea:
99+
```
100+
curl --resolve cafe.example.com:$GW_HTTP_PORT:$GW_IP http://cafe.example.com:$GW_HTTP_PORT/tea --include
101+
HTTP/1.1 302 Moved Temporarily
102+
...
103+
Location: https://cafe.example.com:443/tea
104+
...
105+
```
106+
107+
### 4.2 Access Coffee and Tea
108+
109+
Now we will access the application over HTTPS. Since our certificate is self-signed, we will use curl's `--insecure` option to turn off certificate verification.
75110

76111
To get coffee:
77112

examples/https-termination/cafe-routes.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
apiVersion: gateway.networking.k8s.io/v1beta1
22
kind: HTTPRoute
3+
metadata:
4+
name: cafe-tls-redirect
5+
spec:
6+
parentRefs:
7+
- name: gateway
8+
sectionName: http
9+
hostnames:
10+
- "cafe.example.com"
11+
rules:
12+
- filters:
13+
- type: RequestRedirect
14+
requestRedirect:
15+
scheme: https
16+
port: 443
17+
---
18+
apiVersion: gateway.networking.k8s.io/v1beta1
19+
kind: HTTPRoute
320
metadata:
421
name: coffee
522
spec:

examples/https-termination/gateway.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ metadata:
77
spec:
88
gatewayClassName: nginx
99
listeners:
10+
- name: http
11+
port: 80
12+
protocol: HTTP
1013
- name: https
1114
port: 443
1215
protocol: HTTPS

internal/helpers/helpers.go

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ func GetStringPointer(s string) *string {
2222
return &s
2323
}
2424

25+
// GetIntPointer takes an int and returns a pointer to it. Useful in unit tests when initializing structs.
26+
func GetIntPointer(i int) *int {
27+
return &i
28+
}
29+
2530
// GetInt32Pointer takes an int32 and returns a pointer to it. Useful in unit tests when initializing structs.
2631
func GetInt32Pointer(i int32) *int32 {
2732
return &i

internal/nginx/config/generator.go

+68-15
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,15 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore
8282

8383
s := server{ServerName: virtualServer.Hostname}
8484

85+
listenerPort := 80
86+
8587
if virtualServer.SSL != nil {
8688
s.SSL = &ssl{
8789
Certificate: virtualServer.SSL.CertificatePath,
8890
CertificateKey: virtualServer.SSL.CertificatePath,
8991
}
92+
93+
listenerPort = 443
9094
}
9195

9296
if len(virtualServer.PathRules) == 0 {
@@ -100,26 +104,41 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore
100104
matches := make([]httpMatch, 0, len(rule.MatchRules))
101105

102106
for ruleIdx, r := range rule.MatchRules {
103-
104-
address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore)
105-
if err != nil {
106-
warnings.AddWarning(r.Source, err.Error())
107-
}
108-
109107
m := r.GetMatch()
110108

109+
var loc location
110+
111111
// handle case where the only route is a path-only match
112112
// generate a standard location block without http_matches.
113113
if len(rule.MatchRules) == 1 && isPathOnlyMatch(m) {
114-
locs = append(locs, location{
115-
Path: rule.Path,
116-
ProxyPass: generateProxyPass(address),
117-
})
114+
loc = location{
115+
Path: rule.Path,
116+
}
118117
} else {
119118
path := createPathForMatch(rule.Path, ruleIdx)
120-
locs = append(locs, generateMatchLocation(path, address))
119+
loc = generateMatchLocation(path)
121120
matches = append(matches, createHTTPMatch(m, path))
122121
}
122+
123+
// FIXME(pleshakov): There could be a case when the filter has the type set but not the corresponding field.
124+
// For example, type is v1beta1.HTTPRouteFilterRequestRedirect, but RequestRedirect field is nil.
125+
// The validation webhook catches that.
126+
// If it doesn't work as expected, such situation is silently handled below in findFirstFilters.
127+
// Consider reporting an error. But that should be done in a separate validation layer.
128+
129+
// RequestRedirect and proxying are mutually exclusive.
130+
if r.Filters.RequestRedirect != nil {
131+
loc.Return = generateReturnValForRedirectFilter(r.Filters.RequestRedirect, listenerPort)
132+
} else {
133+
address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore)
134+
if err != nil {
135+
warnings.AddWarning(r.Source, err.Error())
136+
}
137+
138+
loc.ProxyPass = generateProxyPass(address)
139+
}
140+
141+
locs = append(locs, loc)
123142
}
124143

125144
if len(matches) > 0 {
@@ -150,6 +169,41 @@ func generateProxyPass(address string) string {
150169
return "http://" + address
151170
}
152171

172+
func generateReturnValForRedirectFilter(filter *v1beta1.HTTPRequestRedirectFilter, listenerPort int) *returnVal {
173+
if filter == nil {
174+
return nil
175+
}
176+
177+
hostname := "$host"
178+
if filter.Hostname != nil {
179+
hostname = string(*filter.Hostname)
180+
}
181+
182+
// FIXME(pleshakov): Unknown values here must result in the implementation setting the Attached Condition for
183+
// the Route to `status: False`, with a Reason of `UnsupportedValue`. In that case, all routes of the Route will be
184+
// ignored. NGINX will return 500. This should be implemented in the validation layer.
185+
code := statusFound
186+
if filter.StatusCode != nil {
187+
code = statusCode(*filter.StatusCode)
188+
}
189+
190+
port := listenerPort
191+
if filter.Port != nil {
192+
port = int(*filter.Port)
193+
}
194+
195+
// FIXME(pleshakov): Same as the FIXME about StatusCode above.
196+
scheme := "$scheme"
197+
if filter.Scheme != nil {
198+
scheme = *filter.Scheme
199+
}
200+
201+
return &returnVal{
202+
Code: code,
203+
URL: fmt.Sprintf("%s://%s:%d$request_uri", scheme, hostname, port),
204+
}
205+
}
206+
153207
func getBackendAddress(
154208
refs []v1beta1.HTTPBackendRef,
155209
parentNS string,
@@ -183,11 +237,10 @@ func getBackendAddress(
183237
return fmt.Sprintf("%s:%d", address, *ref.Port), nil
184238
}
185239

186-
func generateMatchLocation(path, address string) location {
240+
func generateMatchLocation(path string) location {
187241
return location{
188-
Path: path,
189-
ProxyPass: generateProxyPass(address),
190-
Internal: true,
242+
Path: path,
243+
Internal: true,
191244
}
192245
}
193246

0 commit comments

Comments
 (0)