Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ jobs:
with:
args: --timeout ${{ env.GOLANGCI_TIMEOUT }}

njs-lint:
name: NJS Lint
runs-on: ubuntu-20.04
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run Prettier on NJS code
id: prettier-run
uses: rutajdash/[email protected]
with:
config_path: ${{ github.workspace }}/internal/nginx/modules/.prettierrc
file_pattern: ${{ github.workspace }}/internal/nginx/modules/*.js
prettier_version: 2.6.2
- name: Prettier Output
if: ${{ failure() }}
shell: bash
run: |
echo "The following files are not formatted:"
echo "${{steps.prettier-run.outputs.prettier_output}}"
echo "Run \"make njs-fmt\" locally to format the code"

unit-tests:
name: Unit Tests
runs-on: ubuntu-20.04
Expand All @@ -89,6 +110,19 @@ jobs:
path: ${{ github.workspace }}/cover.html
if: always()

njs-unit-tests:
name: NJS Unit Tests
runs-on: ubuntu-20.04
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup Node.js Environment
uses: actions/setup-node@v3
with:
node_version: 18
- run: npm install mocha@^8.2 esm chai
- run: npx mocha -r esm ${{ github.workspace }}/internal/nginx/modules/httpmatches_test.js

binary:
name: Build Binary
runs-on: ubuntu-20.04
Expand Down
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ delete-kind-cluster:
fmt: ## Run go fmt against code.
go fmt ./...

.PHONY: njs-fmt
njs-fmt: ## Run prettier against the njs httpmatches module.
docker run --rm \
-v $(PWD)/internal/nginx/modules/:/njs-modules/ \
node:18 \
npx [email protected] --write njs-modules/ --config=njs-modules/.prettierrc

.PHONY: vet
vet: ## Run go vet against code.
go vet ./...
Expand All @@ -74,9 +81,15 @@ lint: ## Run golangci-lint against code.
docker run --pull always --rm -v $(shell pwd):/nginx-kubernetes-gateway -w /nginx-kubernetes-gateway -v $(shell go env GOCACHE):/cache/go -e GOCACHE=/cache/go -e GOLANGCI_LINT_CACHE=/cache/go -v $(shell go env GOPATH)/pkg:/go/pkg golangci/golangci-lint:latest golangci-lint --color always run

.PHONY: unit-test
unit-test:
unit-test: ## Run unit tests for the go code
go test ./... -race -coverprofile cover.out
go tool cover -html=cover.out -o cover.html

njs-unit-test: ## Run unit tests for the njs httpmatches module.
docker run --rm -w /src \
-v $(PWD)/internal/nginx/modules/:/src/njs-modules/ \
node:18 \
/bin/bash -c "npm install mocha@^8.2 esm chai && npx mocha -r esm njs-modules/httpmatches_test.js"

.PHONY: dev-all
dev-all: deps fmt vet lint unit-test
dev-all: deps fmt njs-fmt vet lint unit-test njs-unit-test
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ You can deploy NGINX Kubernetes Gateway on an existing Kubernetes 1.16+ cluster.
kubectl apply -k "github.com/kubernetes-sigs/gateway-api/config/crd?ref=v0.4.2"
```

1. Create the nginx-gateway namespace:

```
kubectl apply -f deploy/manifests/namespace.yaml
```

1. Create the njs-modules configmap:

```
kubectl create configmap njs-modules --from-file=internal/nginx/modules/httpmatches.js -n nginx-gateway
```

1. Deploy the NGINX Kubernetes Gateway:

Before deploying, make sure to update the Deployment spec in `nginx-gateway.yaml` to reference the image you built.
Expand Down
2 changes: 1 addition & 1 deletion cmd/gateway/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func GatewayControllerParam(domain string, namespace string) ValidatorContext {
return ValidatorContext{
name,
func(flagset *flag.FlagSet) error {
// FIXME(yacobucci) this does not provide the same regex validation as
// FIXME(f5yacobucci) this does not provide the same regex validation as
// GatewayClass.ControllerName. provide equal and then specific validation
param, err := flagset.GetString(name)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions deploy/manifests/namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: nginx-gateway
12 changes: 6 additions & 6 deletions deploy/manifests/nginx-gateway.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: nginx-gateway
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-gateway
Expand Down Expand Up @@ -77,10 +72,13 @@ spec:
volumes:
- name: nginx-config
emptyDir: { }
- name: njs-modules
configMap:
name: njs-modules
initContainers:
- image: busybox:1.34 # FIXME(pleshakov): use gateway container to init the Config with proper main config
name: nginx-config-initializer
command: [ 'sh', '-c', 'echo "events {} pid /etc/nginx/nginx.pid; http { include /etc/nginx/conf.d/*.conf; server { default_type text/html; return 404; } }" > /etc/nginx/nginx.conf && mkdir /etc/nginx/conf.d && chown 1001:0 /etc/nginx/conf.d' ]
command: [ 'sh', '-c', 'echo "load_module /usr/lib/nginx/modules/ngx_http_js_module.so; events {} pid /etc/nginx/nginx.pid; http { include /etc/nginx/conf.d/*.conf; js_import /usr/lib/nginx/modules/njs/httpmatches.js; server { default_type text/html; return 404; } }" > /etc/nginx/nginx.conf && mkdir /etc/nginx/conf.d && chown 1001:0 /etc/nginx/conf.d' ]
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx
Expand All @@ -107,3 +105,5 @@ spec:
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx
- name: njs-modules
mountPath: /usr/lib/nginx/modules/njs
98 changes: 98 additions & 0 deletions examples/advanced-routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Advanced Routing

In this example we will deploy NGINX Kubernetes Gateway and configure advanced routing rules for a simple cafe application.
We will use `HTTPRoute` resources to route traffic to the cafe application based on a combination of the request method, headers, and query parameters.

## Running the Example

## 1. Deploy NGINX Kubernetes Gateway

1. Follow the [installation instructions](https://github.com/nginxinc/nginx-kubernetes-gateway/blob/main/README.md#run-nginx-gateway) 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 coffee and the tea deployments and services:

```
kubectl apply -f cafe.yaml
```

1. Check that the Pods are running in the `default` namespace:

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

## 3. Configure Routing

1. Create the `HTTPRoute` resources:

```
kubectl apply -f cafe-routes.yaml
```

## 4. Test the Application

We will use `curl` to send requests to the `coffee` and `tea` services.

### 4.1 Access coffee

Send a `POST` request to the path `/coffee` with the headers `x-demo-header:demo-x1` and `version:v1`:

```
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -X POST -H "x-demo-header:demo-x1" -H "version:v1"
Server address: 10.12.0.18:80
Server name: coffee-7586895968-r26zn
```

Header keys and values are case-insensitive, so we can also access coffee with the following request:

```
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -X POST -H "X-DEMO-HEADER:DEMO-X1" -H "VERSION:V1"
Server address: 10.12.0.18:80
Server name: coffee-7586895968-r26zn
```

Only `POST` requests to the path `/coffee` with the headers `x-demo-header:demo-x1` and `version:v1` will be able to access coffee.
For example, try sending the following `GET` request:
```
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "X-DEMO-HEADER:DEMO-X1" -H "VERSION:V1"
```

NGINX Kubernetes Gateway returns a 405 since the request method does not match the method defined in the routing rule for `/coffee`.

### 4.2 Access tea

Send a request to the path `/tea` with the query parameter `Great=Example`:

```
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea?Great=Example
Server address: 10.12.0.19:80
Server name: tea-7cd44fcb4d-xfw2x
```

Query parameters are case-sensitive, so the case must match what you specify in the `HTTPRoute` resource.

Only requests to the path `/tea` with the query parameter `Great=Example` will be able to access tea.
For example, try sending the following request:

```
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea
```

NGINX Kubernetes Gateway returns a 404 since the request does not satisfy the routing rule configured for `/tea`.
52 changes: 52 additions & 0 deletions examples/advanced-routing/cafe-routes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: cafe
spec:
hostnames:
- "cafe.example.com"
rules:
- backendRefs:
- name: main
port: 80
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: coffee
spec:
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /coffee
method: POST
headers:
- name: X-Demo-Header # header names and values are case-insensitive
value: Demo-X1
- name: version
value: v1
backendRefs:
- name: coffee
port: 80
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
name: tea
spec:
hostnames:
- "cafe.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /tea
queryParams:
- name: Great # query params and values are case-sensitive
value: Example
backendRefs:
- name: tea
port: 80
65 changes: 65 additions & 0 deletions examples/advanced-routing/cafe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: coffee
spec:
replicas: 1
selector:
matchLabels:
app: coffee
template:
metadata:
labels:
app: coffee
spec:
containers:
- name: coffee
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: coffee
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: coffee
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tea
spec:
replicas: 1
selector:
matchLabels:
app: tea
template:
metadata:
labels:
app: tea
spec:
containers:
- name: tea
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: tea
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: tea
1 change: 0 additions & 1 deletion examples/cafe-example/cafe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ apiVersion: v1
kind: Service
metadata:
name: tea
labels:
spec:
ports:
- port: 80
Expand Down
20 changes: 19 additions & 1 deletion internal/helpers/helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package helpers

import "github.com/google/go-cmp/cmp"
import (
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
)

// Diff prints the diff between two structs.
// It is useful in testing to compare two structs when they are large. In such a case, without Diff it will be difficult
Expand All @@ -23,3 +26,18 @@ func GetStringPointer(s string) *string {
func GetInt32Pointer(i int32) *int32 {
return &i
}

// GetHTTPMethodPointer takes an HTTPMethod and returns a pointer to it. Useful in unit tests when initializing structs.
func GetHTTPMethodPointer(m v1alpha2.HTTPMethod) *v1alpha2.HTTPMethod {
return &m
}

// GetHeaderMatchTypePointer takes an HeaderMatchType and returns a pointer to it. Useful in unit tests when initializing structs.
func GetHeaderMatchTypePointer(t v1alpha2.HeaderMatchType) *v1alpha2.HeaderMatchType {
return &t
}

// GetQueryParamMatchTypePointer takes an QueryParamMatchType and returns a pointer to it. Useful in unit tests when initializing structs.
func GetQueryParamMatchTypePointer(t v1alpha2.QueryParamMatchType) *v1alpha2.QueryParamMatchType {
return &t
}
Loading