Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RequestHeaderModifier for HTTPRouteRule objects #717

Merged
merged 1 commit into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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