Skip to content

Commit a710e0f

Browse files
authored
Add support for RequestHeaderModifier for HTTPRouteRule objects (#717)
1 parent 4d90724 commit a710e0f

27 files changed

+1250
-27
lines changed

deploy/manifests/nginx-conf.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ data:
1515
http {
1616
include /etc/nginx/conf.d/*.conf;
1717
js_import /usr/lib/nginx/modules/njs/httpmatches.js;
18+
proxy_headers_hash_bucket_size 512;
19+
proxy_headers_hash_max_size 1024;
1820
server_names_hash_bucket_size 256;
1921
server_names_hash_max_size 1024;
22+
variables_hash_bucket_size 512;
23+
variables_hash_max_size 1024;
2024
}

docs/gateway-api-compatibility.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ Fields:
111111
* `filters`
112112
* `type` - supported.
113113
* `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.
114-
* `requestHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported.
114+
* `requestHeaderModifier` - supported. If multiple filters with `requestHeaderModifier` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest.
115+
* `responseHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported.
115116
* `backendRefs` - partially supported. Backend ref `filters` are not supported.
116117
* `status`
117118
* `parents`

examples/http-header-filter/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Example
2+
3+
In this example we will deploy NGINX Kubernetes Gateway and configure traffic routing for a simple echo server.
4+
We will use `HTTPRoute` resources to route traffic to the echo server, using the RequestHeaderModifier filter to modify
5+
headers to the request.
6+
## Running the Example
7+
8+
## 1. Deploy NGINX Kubernetes Gateway
9+
10+
1. Follow the [installation instructions](/docs/installation.md) to deploy NGINX Gateway.
11+
12+
1. Save the public IP address of NGINX Kubernetes Gateway into a shell variable:
13+
14+
```
15+
GW_IP=XXX.YYY.ZZZ.III
16+
```
17+
18+
1. Save the port of NGINX Kubernetes Gateway:
19+
20+
```
21+
GW_PORT=<port number>
22+
```
23+
24+
## 2. Deploy the Cafe Application
25+
26+
1. Create the headers Deployment and Service:
27+
28+
```
29+
kubectl apply -f headers.yaml
30+
```
31+
32+
1. Check that the Pod is running in the `default` namespace:
33+
34+
```
35+
kubectl -n default get pods
36+
NAME READY STATUS RESTARTS AGE
37+
headers-6f4b79b975-2sb28 1/1 Running 0 12s
38+
```
39+
40+
## 3. Configure Routing
41+
42+
1. Create the `Gateway`:
43+
44+
```
45+
kubectl apply -f gateway.yaml
46+
```
47+
48+
1. Create the `HTTPRoute` resources:
49+
50+
```
51+
kubectl apply -f echo-route.yaml
52+
```
53+
54+
## 4. Test the Application
55+
56+
To access the application, we will use `curl` to send requests to the `headers` Service, including sending headers with
57+
our request.
58+
Notice our configured header values can be seen in the `requestHeaders` section below, and that the `User-Agent` header
59+
is absent.
60+
61+
```
62+
curl -s --resolve echo.example.com:$GW_PORT:$GW_IP http://echo.example.com:$GW_PORT/headers -H "My-Cool-Header:my-client-value" -H "My-Overwrite-Header:dont-see-this"
63+
Headers:
64+
header 'Accept-Encoding' is 'compress'
65+
header 'My-cool-header' is 'my-client-value, this-is-an-appended-value'
66+
header 'My-Overwrite-Header' is 'this-is-the-only-value'
67+
header 'Host' is 'echo.example.com'
68+
header 'Connection' is 'close'
69+
header 'Accept' is '*/*'
70+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: HTTPRoute
3+
metadata:
4+
name: headers
5+
spec:
6+
parentRefs:
7+
- name: gateway
8+
sectionName: http
9+
hostnames:
10+
- "echo.example.com"
11+
rules:
12+
- matches:
13+
- path:
14+
type: PathPrefix
15+
value: /headers
16+
filters:
17+
- type: RequestHeaderModifier
18+
requestHeaderModifier:
19+
set:
20+
- name: My-Overwrite-Header
21+
value: this-is-the-only-value
22+
add:
23+
- name: Accept-Encoding
24+
value: compress
25+
- name: My-cool-header
26+
value: this-is-an-appended-value
27+
remove:
28+
- User-Agent
29+
backendRefs:
30+
- name: headers
31+
port: 80
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: Gateway
3+
metadata:
4+
name: gateway
5+
labels:
6+
domain: k8s-gateway.nginx.org
7+
spec:
8+
gatewayClassName: nginx
9+
listeners:
10+
- name: http
11+
port: 80
12+
protocol: HTTP
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: headers
5+
spec:
6+
replicas: 1
7+
selector:
8+
matchLabels:
9+
app: headers
10+
template:
11+
metadata:
12+
labels:
13+
app: headers
14+
spec:
15+
containers:
16+
- name: headers
17+
image: nginx
18+
ports:
19+
- containerPort: 8080
20+
volumeMounts:
21+
- name: config-volume
22+
mountPath: /etc/nginx
23+
readOnly: true
24+
volumes:
25+
- name: config-volume
26+
configMap:
27+
name: headers-config
28+
---
29+
apiVersion: v1
30+
kind: ConfigMap
31+
metadata:
32+
name: headers-config
33+
data:
34+
nginx.conf: |-
35+
user nginx;
36+
worker_processes 1;
37+
38+
pid /var/run/nginx.pid;
39+
40+
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
41+
42+
events {}
43+
44+
http {
45+
default_type text/plain;
46+
47+
js_import /etc/nginx/headers.js;
48+
js_set $headers headers.getRequestHeaders;
49+
50+
server {
51+
listen 8080;
52+
return 200 "$headers";
53+
}
54+
}
55+
headers.js: |-
56+
function getRequestHeaders(r) {
57+
let s = "Headers:\n";
58+
for (let h in r.headersIn) {
59+
s += ` header '${h}' is '${r.headersIn[h]}'\n`;
60+
}
61+
return s;
62+
}
63+
export default {getRequestHeaders};
64+
65+
---
66+
apiVersion: v1
67+
kind: Service
68+
metadata:
69+
name: headers
70+
spec:
71+
ports:
72+
- port: 80
73+
targetPort: 8080
74+
protocol: TCP
75+
name: http
76+
selector:
77+
app: headers

internal/nginx/config/generator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ func getExecuteFuncs() []executeFunc {
4242
executeUpstreams,
4343
executeSplitClients,
4444
executeServers,
45+
executeMaps,
4546
}
4647
}

internal/nginx/config/http/config.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ type Server struct {
1212

1313
// Location holds all configuration for an HTTP location.
1414
type Location struct {
15-
Return *Return
16-
Path string
17-
ProxyPass string
18-
HTTPMatchVar string
19-
Internal bool
20-
Exact bool
15+
Return *Return
16+
Path string
17+
ProxyPass string
18+
HTTPMatchVar string
19+
ProxySetHeaders []Header
20+
Internal bool
21+
Exact bool
22+
}
23+
24+
// Header defines a HTTP header to be passed to the proxied server.
25+
type Header struct {
26+
Name string
27+
Value string
2128
}
2229

2330
// Return represents an HTTP return.
@@ -66,3 +73,16 @@ type SplitClientDistribution struct {
6673
Percent string
6774
Value string
6875
}
76+
77+
// Map defines an NGINX map.
78+
type Map struct {
79+
Source string
80+
Variable string
81+
Parameters []MapParameter
82+
}
83+
84+
// Parameter defines a Value and Result pair in a Map.
85+
type MapParameter struct {
86+
Value string
87+
Result string
88+
}

internal/nginx/config/maps.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package config
2+
3+
import (
4+
"strings"
5+
gotemplate "text/template"
6+
7+
"github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http"
8+
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane"
9+
)
10+
11+
var mapsTemplate = gotemplate.Must(gotemplate.New("maps").Parse(mapsTemplateText))
12+
13+
func executeMaps(conf dataplane.Configuration) []byte {
14+
maps := createMaps(append(conf.HTTPServers, conf.SSLServers...))
15+
return execute(mapsTemplate, maps)
16+
}
17+
18+
func createMaps(servers []dataplane.VirtualServer) []http.Map {
19+
return buildAddHeaderMaps(servers)
20+
}
21+
22+
func buildAddHeaderMaps(servers []dataplane.VirtualServer) []http.Map {
23+
addHeaderNames := make(map[string]struct{})
24+
25+
for _, s := range servers {
26+
for _, pr := range s.PathRules {
27+
for _, mr := range pr.MatchRules {
28+
if mr.Filters.RequestHeaderModifiers != nil {
29+
for _, addHeader := range mr.Filters.RequestHeaderModifiers.Add {
30+
lowerName := strings.ToLower(addHeader.Name)
31+
if _, ok := addHeaderNames[lowerName]; !ok {
32+
addHeaderNames[lowerName] = struct{}{}
33+
}
34+
}
35+
}
36+
}
37+
}
38+
}
39+
40+
maps := make([]http.Map, 0, len(addHeaderNames))
41+
for m := range addHeaderNames {
42+
maps = append(maps, createAddHeadersMap(m))
43+
}
44+
return maps
45+
}
46+
47+
const (
48+
// In order to prepend any passed client header values to values specified in the add headers field of request
49+
// header modifiers, we need to create a map parameter regex for any string value
50+
anyStringFmt = `~.*`
51+
)
52+
53+
func createAddHeadersMap(name string) http.Map {
54+
underscoreName := convertStringToSafeVariableName(name)
55+
httpVarSource := "${http_" + underscoreName + "}"
56+
mapVarName := generateAddHeaderMapVariableName(name)
57+
params := []http.MapParameter{
58+
{
59+
Value: "default",
60+
Result: "''",
61+
},
62+
{
63+
Value: anyStringFmt,
64+
Result: httpVarSource + ",",
65+
},
66+
}
67+
return http.Map{
68+
Source: httpVarSource,
69+
Variable: "$" + mapVarName,
70+
Parameters: params,
71+
}
72+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package config
2+
3+
var mapsTemplateText = `
4+
{{ range $m := . }}
5+
map {{ $m.Source }} {{ $m.Variable }} {
6+
{{ range $p := $m.Parameters }}
7+
{{ $p.Value }} {{ $p.Result }};
8+
{{ end }}
9+
}
10+
{{- end }}
11+
`

0 commit comments

Comments
 (0)