Skip to content

Commit

Permalink
Add support for RequestHeaderModifier for HTTPRouteRule objects (#717)
Browse files Browse the repository at this point in the history
  • Loading branch information
ciarams87 authored Jun 22, 2023
1 parent 4d90724 commit a710e0f
Show file tree
Hide file tree
Showing 27 changed files with 1,250 additions and 27 deletions.
4 changes: 4 additions & 0 deletions deploy/manifests/nginx-conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ data:
http {
include /etc/nginx/conf.d/*.conf;
js_import /usr/lib/nginx/modules/njs/httpmatches.js;
proxy_headers_hash_bucket_size 512;
proxy_headers_hash_max_size 1024;
server_names_hash_bucket_size 256;
server_names_hash_max_size 1024;
variables_hash_bucket_size 512;
variables_hash_max_size 1024;
}
3 changes: 2 additions & 1 deletion docs/gateway-api-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ Fields:
* `filters`
* `type` - supported.
* `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.
* `requestHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported.
* `requestHeaderModifier` - supported. If multiple filters with `requestHeaderModifier` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest.
* `responseHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported.
* `backendRefs` - partially supported. Backend ref `filters` are not supported.
* `status`
* `parents`
Expand Down
70 changes: 70 additions & 0 deletions examples/http-header-filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Example

In this example we will deploy NGINX Kubernetes Gateway and configure traffic routing for a simple echo server.
We will use `HTTPRoute` resources to route traffic to the echo server, using the RequestHeaderModifier filter to modify
headers to the request.
## Running the Example

## 1. Deploy NGINX Kubernetes Gateway

1. Follow the [installation instructions](/docs/installation.md) to deploy NGINX Gateway.

1. Save the public IP address of NGINX Kubernetes Gateway into a shell variable:

```
GW_IP=XXX.YYY.ZZZ.III
```

1. Save the port of NGINX Kubernetes Gateway:

```
GW_PORT=<port number>
```

## 2. Deploy the Cafe Application

1. Create the headers Deployment and Service:

```
kubectl apply -f headers.yaml
```

1. Check that the Pod is running in the `default` namespace:

```
kubectl -n default get pods
NAME READY STATUS RESTARTS AGE
headers-6f4b79b975-2sb28 1/1 Running 0 12s
```

## 3. Configure Routing

1. Create the `Gateway`:

```
kubectl apply -f gateway.yaml
```

1. Create the `HTTPRoute` resources:

```
kubectl apply -f echo-route.yaml
```

## 4. Test the Application

To access the application, we will use `curl` to send requests to the `headers` Service, including sending headers with
our request.
Notice our configured header values can be seen in the `requestHeaders` section below, and that the `User-Agent` header
is absent.

```
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"
Headers:
header 'Accept-Encoding' is 'compress'
header 'My-cool-header' is 'my-client-value, this-is-an-appended-value'
header 'My-Overwrite-Header' is 'this-is-the-only-value'
header 'Host' is 'echo.example.com'
header 'Connection' is 'close'
header 'Accept' is '*/*'
```
31 changes: 31 additions & 0 deletions examples/http-header-filter/echo-route.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: headers
spec:
parentRefs:
- name: gateway
sectionName: http
hostnames:
- "echo.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /headers
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: My-Overwrite-Header
value: this-is-the-only-value
add:
- name: Accept-Encoding
value: compress
- name: My-cool-header
value: this-is-an-appended-value
remove:
- User-Agent
backendRefs:
- name: headers
port: 80
12 changes: 12 additions & 0 deletions examples/http-header-filter/gateway.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: gateway
labels:
domain: k8s-gateway.nginx.org
spec:
gatewayClassName: nginx
listeners:
- name: http
port: 80
protocol: HTTP
77 changes: 77 additions & 0 deletions examples/http-header-filter/headers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: headers
spec:
replicas: 1
selector:
matchLabels:
app: headers
template:
metadata:
labels:
app: headers
spec:
containers:
- name: headers
image: nginx
ports:
- containerPort: 8080
volumeMounts:
- name: config-volume
mountPath: /etc/nginx
readOnly: true
volumes:
- name: config-volume
configMap:
name: headers-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: headers-config
data:
nginx.conf: |-
user nginx;
worker_processes 1;
pid /var/run/nginx.pid;
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
events {}
http {
default_type text/plain;
js_import /etc/nginx/headers.js;
js_set $headers headers.getRequestHeaders;
server {
listen 8080;
return 200 "$headers";
}
}
headers.js: |-
function getRequestHeaders(r) {
let s = "Headers:\n";
for (let h in r.headersIn) {
s += ` header '${h}' is '${r.headersIn[h]}'\n`;
}
return s;
}
export default {getRequestHeaders};
---
apiVersion: v1
kind: Service
metadata:
name: headers
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: headers
1 change: 1 addition & 0 deletions internal/nginx/config/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ func getExecuteFuncs() []executeFunc {
executeUpstreams,
executeSplitClients,
executeServers,
executeMaps,
}
}
32 changes: 26 additions & 6 deletions internal/nginx/config/http/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ type Server struct {

// Location holds all configuration for an HTTP location.
type Location struct {
Return *Return
Path string
ProxyPass string
HTTPMatchVar string
Internal bool
Exact bool
Return *Return
Path string
ProxyPass string
HTTPMatchVar string
ProxySetHeaders []Header
Internal bool
Exact bool
}

// Header defines a HTTP header to be passed to the proxied server.
type Header struct {
Name string
Value string
}

// Return represents an HTTP return.
Expand Down Expand Up @@ -66,3 +73,16 @@ type SplitClientDistribution struct {
Percent string
Value string
}

// Map defines an NGINX map.
type Map struct {
Source string
Variable string
Parameters []MapParameter
}

// Parameter defines a Value and Result pair in a Map.
type MapParameter struct {
Value string
Result string
}
72 changes: 72 additions & 0 deletions internal/nginx/config/maps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package config

import (
"strings"
gotemplate "text/template"

"github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http"
"github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane"
)

var mapsTemplate = gotemplate.Must(gotemplate.New("maps").Parse(mapsTemplateText))

func executeMaps(conf dataplane.Configuration) []byte {
maps := createMaps(append(conf.HTTPServers, conf.SSLServers...))
return execute(mapsTemplate, maps)
}

func createMaps(servers []dataplane.VirtualServer) []http.Map {
return buildAddHeaderMaps(servers)
}

func buildAddHeaderMaps(servers []dataplane.VirtualServer) []http.Map {
addHeaderNames := make(map[string]struct{})

for _, s := range servers {
for _, pr := range s.PathRules {
for _, mr := range pr.MatchRules {
if mr.Filters.RequestHeaderModifiers != nil {
for _, addHeader := range mr.Filters.RequestHeaderModifiers.Add {
lowerName := strings.ToLower(addHeader.Name)
if _, ok := addHeaderNames[lowerName]; !ok {
addHeaderNames[lowerName] = struct{}{}
}
}
}
}
}
}

maps := make([]http.Map, 0, len(addHeaderNames))
for m := range addHeaderNames {
maps = append(maps, createAddHeadersMap(m))
}
return maps
}

const (
// In order to prepend any passed client header values to values specified in the add headers field of request
// header modifiers, we need to create a map parameter regex for any string value
anyStringFmt = `~.*`
)

func createAddHeadersMap(name string) http.Map {
underscoreName := convertStringToSafeVariableName(name)
httpVarSource := "${http_" + underscoreName + "}"
mapVarName := generateAddHeaderMapVariableName(name)
params := []http.MapParameter{
{
Value: "default",
Result: "''",
},
{
Value: anyStringFmt,
Result: httpVarSource + ",",
},
}
return http.Map{
Source: httpVarSource,
Variable: "$" + mapVarName,
Parameters: params,
}
}
11 changes: 11 additions & 0 deletions internal/nginx/config/maps_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package config

var mapsTemplateText = `
{{ range $m := . }}
map {{ $m.Source }} {{ $m.Variable }} {
{{ range $p := $m.Parameters }}
{{ $p.Value }} {{ $p.Result }};
{{ end }}
}
{{- end }}
`
Loading

0 comments on commit a710e0f

Please sign in to comment.