diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43af980273..d18911a777 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,9 @@ jobs: - name: Check if generated files are up to date run: make generate && git diff --exit-code + - name: Check if njs-modules yaml is up to date + run: make generate-njs-yaml && git diff --exit-code + unit-tests: name: Unit Tests runs-on: ubuntu-22.04 @@ -149,6 +152,71 @@ jobs: path: ${{ github.workspace }}/dist key: nginx-kubernetes-gateway-${{ github.run_id }}-${{ github.run_number }} + helm-tests: + name: Helm Tests + runs-on: ubuntu-22.04 + needs: [vars, binary] + if: ${{ github.ref_type != 'tag' }} + steps: + - name: Checkout Repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - name: Fetch Cached Artifacts + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + with: + path: ${{ github.workspace }}/dist + key: nginx-kubernetes-gateway-${{ github.run_id }}-${{ github.run_number }} + + - name: Docker Buildx + uses: docker/setup-buildx-action@16c0bc4a6e6ada2cfd8afd41d22d95379cf7c32a # v2.8.0 + + - name: Docker meta + id: meta + uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0 + with: + images: | + name=ghcr.io/nginxinc/nginx-kubernetes-gateway + tags: | + type=semver,pattern={{version}} + type=edge + type=ref,event=pr + type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} + + - name: Build Docker Image + uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + with: + file: build/Dockerfile + tags: ${{ steps.meta.outputs.tags }} + context: "." + target: goreleaser + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + pull: true + + - name: Deploy Kubernetes + id: k8s + run: | + kube_config=${{ github.workspace }}/deploy/helm-chart/kube-${{ github.run_id }}-helm + make create-kind-cluster KIND_KUBE_CONFIG=${kube_config} + echo "KUBECONFIG=${kube_config}" >> "$GITHUB_ENV" + kind load docker-image ${{ steps.meta.outputs.tags }} + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.7.1/standard-install.yaml + kubectl wait --for=condition=complete job/gateway-api-admission-patch job/gateway-api-admission -n gateway-system + + - name: Install Chart + run: > + helm install + helm-$(echo ${{ steps.meta.outputs.tags }} | cut -d ":" -f 2) + . + --wait + --create-namespace + --set controller.image.repository=$(echo ${{ steps.meta.outputs.tags }} | cut -d ":" -f 1) + --set controller.image.tag=$(echo ${{ steps.meta.outputs.tags }} | cut -d ":" -f 2) + --set service.type=NodePort + -n nginx-gateway + working-directory: ${{ github.workspace }}/deploy/helm-chart + build: name: Build Image runs-on: ubuntu-22.04 @@ -235,3 +303,31 @@ jobs: name: "trivy-results-nginx-kubernetes-gateway.sarif" path: "trivy-results-nginx-kubernetes-gateway.sarif" if: always() + + publish-helm: + name: Package and Publish Helm Chart + runs-on: ubuntu-22.04 + needs: [vars, helm-tests] + if: ${{ github.event_name == 'push' && ! startsWith(github.ref, 'refs/heads/release-') }} + steps: + - name: Checkout Repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + path: nkg + + - name: Login to GitHub Container Registry + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Package + id: package + run: | + output=$(helm package ${{ ! startsWith(github.ref, 'refs/tags/') && '--app-version edge --version 0.0.0-edge' || '' }} nkg/deploy/helm-chart) + echo "path=$(basename -- $(echo $output | cut -d: -f2))" >> $GITHUB_OUTPUT + + - name: Push to GitHub Container Registry + run: | + helm push ${{ steps.package.outputs.path }} oci://ghcr.io/nginxinc/charts diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 213e1a77ad..613cffd74c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -88,3 +88,12 @@ jobs: with: config: ${{ github.workspace }}/.markdownlint-cli2.yaml globs: '**/*.md' + + chart-lint: + name: Chart Lint + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - name: Lint chart + run: make lint-helm diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4a3a678fd..00b2928ed1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ repos: - id: end-of-file-fixer - id: check-yaml args: [--allow-multiple-documents] + exclude: (^deploy/helm-chart/templates) - id: check-added-large-files - id: check-merge-conflict - id: check-case-conflict diff --git a/Makefile b/Makefile index 6434357af9..31db88e092 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,15 @@ VERSION = edge GIT_COMMIT = $(shell git rev-parse HEAD || echo "unknown") DATE = $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +MANIFEST_DIR = $(shell pwd)/deploy/manifests +NJS_DIR = $(shell pwd)/internal/mode/static/nginx/modules/src +CHART_DIR = $(shell pwd)/deploy/helm-chart # variables that can be overridden by the user PREFIX ?= nginx-kubernetes-gateway## The name of the image. For example, nginx-kubernetes-gateway TAG ?= $(VERSION:v%=%)## The tag of the image. For example, 0.3.0 TARGET ?= local## The target of the build. Possible values: local and container -KIND_KUBE_CONFIG_FOLDER = $${HOME}/.kube/kind## The folder where the kind kubeconfig is stored +KIND_KUBE_CONFIG=$${HOME}/.kube/kind/config## The location of the kind kubeconfig OUT_DIR ?= $(shell pwd)/build/out## The folder where the binary will be stored ARCH ?= amd64## The architecture of the image and/or binary. For example: amd64 or arm64 override DOCKER_BUILD_OPTIONS += --build-arg VERSION=$(VERSION) --build-arg GIT_COMMIT=$(GIT_COMMIT) --build-arg DATE=$(DATE)## The options for the docker build command. For example, --pull @@ -56,7 +59,7 @@ deps: ## Add missing and remove unused modules, verify deps and download them to create-kind-cluster: ## Create a kind cluster $(eval KIND_IMAGE=$(shell grep -m1 'FROM kindest/node' $(strip $(MANIFEST_DIR))/njs-modules.yaml + +.PHONY: lint-helm +lint-helm: ## Run the helm chart linter + helm lint $(CHART_DIR) + .PHONY: dev-all dev-all: deps fmt njs-fmt vet lint unit-test njs-unit-test ## Run all the development checks diff --git a/conformance/Makefile b/conformance/Makefile index e083c2710e..11641e5d0b 100644 --- a/conformance/Makefile +++ b/conformance/Makefile @@ -51,7 +51,8 @@ prepare-nkg-dependencies: ## Install NKG dependencies on configured kind cluster ./scripts/install-gateway.sh $(GW_API_VERSION) kubectl wait --for=condition=available --timeout=60s deployment gateway-api-admission-server -n gateway-system kubectl apply -f ../deploy/manifests/namespace.yaml - kubectl create configmap njs-modules --from-file=../internal/mode/static/nginx/modules/src/httpmatches.js -n nginx-gateway + cd .. && make generate-njs-yaml && cd - + kubectl apply -f ../deploy/manifests/njs-modules.yaml -n nginx-gateway kubectl apply -f ../deploy/manifests/nginx-conf.yaml kubectl apply -f ../deploy/manifests/rbac.yaml kubectl apply -f ../deploy/manifests/gatewayclass.yaml diff --git a/deploy/helm-chart/.helmignore b/deploy/helm-chart/.helmignore new file mode 100644 index 0000000000..c1347c2c27 --- /dev/null +++ b/deploy/helm-chart/.helmignore @@ -0,0 +1,2 @@ +# Patterns to ignore when building packages. +*.png diff --git a/deploy/helm-chart/Chart.yaml b/deploy/helm-chart/Chart.yaml new file mode 100644 index 0000000000..56269135a6 --- /dev/null +++ b/deploy/helm-chart/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: nginx-kubernetes-gateway +description: NGINX Kubernetes Gateway +type: application +version: 0.1.0 +appVersion: "0.4.0" +home: https://github.com/nginxinc/nginx-kubernetes-gateway +icon: https://raw.githubusercontent.com/nginxinc/nginx-kubernetes-gateway/tree/main/deploy/helm-chart/chart-icon.png +sources: + - https://github.com/nginxinc/nginx-kubernetes-gateway/tree/main/deploy/helm-chart +keywords: + - kubernetes + - gateway + - nginx +maintainers: + - name: nginxinc + email: kubernetes@nginx.com diff --git a/deploy/helm-chart/README.md b/deploy/helm-chart/README.md new file mode 100644 index 0000000000..07a5100cf8 --- /dev/null +++ b/deploy/helm-chart/README.md @@ -0,0 +1,128 @@ +# NGINX Kubernetes Gateway Helm Chart + +## Introduction + +This chart deploys the NGINX Kubernetes Gateway in your Kubernetes cluster. + +## Prerequisites + +- [Helm 3.0+](https://helm.sh/docs/intro/install/) +- [kubectl](https://kubernetes.io/docs/tasks/tools/) + +> Note: NGINX Kubernetes Gateway can only run in the `nginx-gateway` namespace. This limitation will be addressed in +the future releases. + +### Installing the Gateway API resources + +> Note: The Gateway API resources from the standard channel (the CRDs and the validating webhook) must be installed +before deploying NGINX Kubernetes Gateway. If they are already installed in your cluster, please ensure they are the +correct version as supported by the NGINX Kubernetes Gateway - +[see the Technical Specifications](../../README.md#technical-specifications). + +To install the Gateway resources from [the Gateway API repo](https://github.com/kubernetes-sigs/gateway-api), run: + +```shell +kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.7.1/standard-install.yaml +``` + +## Installing the Chart + +### Installing the Chart from the OCI Registry + +To install the chart with the release name `my-release` (`my-release` is the name that you choose) into the +nginx-gateway namespace (with optional `--create-namespace` flag - you can omit if the namespace already exists): + +```shell +helm install my-release oci://ghcr.io/nginxinc/charts/nginx-kubernetes-gateway --version 0.0.0-edge --create-namespace --wait -n nginx-gateway +``` + +### Installing the Chart via Sources + +#### Pulling the Chart + +```shell +helm pull oci://ghcr.io/nginxinc/charts/nginx-kubernetes-gateway --untar --version 0.0.0-edge +cd nginx-gateway +``` + +#### Installing the Chart + +To install the chart with the release name `my-release` (`my-release` is the name that you choose) into the +nginx-gateway namespace (with optional `--create-namespace` flag - you can omit if the namespace already exists): + +```shell +helm install my-release . --create-namespace --wait -n nginx-gateway +``` + +## Upgrading the Chart +### Upgrading the Gateway Resources +Before you upgrade a release, ensure the Gateway API resources are the correct version as supported by the NGINX +Kubernetes Gateway - [see the Technical Specifications](../../README.md#technical-specifications).: + +To upgrade the Gateway resources from [the Gateway API repo](https://github.com/kubernetes-sigs/gateway-api), run: + +```shell +kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.7.1/standard-install.yaml +``` + +### Upgrading the Chart from the OCI Registry +To upgrade the release `my-release`, run: + +```shell +helm upgrade my-release oci://ghcr.io/nginxinc/charts/nginx-kubernetes-gateway --version 0.0.0-edge -n nginx-gateway +``` + +### Upgrading the Chart from the Sources + +Pull the chart sources as described in [Pulling the Chart](#pulling-the-chart), if not already present. Then, to upgrade +the release `my-release`, run: + +```shell +helm upgrade my-release . -n nginx-gateway +``` + +## Uninstalling the Chart + +To uninstall/delete the release `my-release`: + +```shell +helm uninstall my-release -n nginx-gateway +``` + +The command removes all the Kubernetes components associated with the release and deletes the release. + +### Uninstalling the Gateway Resources + +>**Warning: This command will delete all the corresponding custom resources in your cluster across all namespaces! +Please ensure there are no custom resources that you want to keep and there are no other Gateway API implementations +running in the cluster!** + +To delete the Gateway resources using [the Gateway API repo](https://github.com/kubernetes-sigs/gateway-api), run: + +```shell +kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.7.1/standard-install.yaml +``` + +## Configuration + +The following tables lists the configurable parameters of the NGINX Kubernetes Gateway chart and their default values. + +|Parameter | Description | Default Value | +| --- | --- | --- | +|`nginxGateway.image.repository` | The repository for the NGINX Kubernetes Gateway image. | ghcr.io/nginxinc/nginx-kubernetes-gateway | +|`nginxGateway.image.tag` | The tag for the NGINX Kubernetes Gateway image. | edge | +|`nginxGateway.image.pullPolicy` | The `imagePullPolicy` for the NGINX Kubernetes Gateway image. | Always | +|`nginxGateway.gatewayClassName` | The name of the GatewayClass for the NGINX Kubernetes Gateway deployment. | nginx | +|`nginxGateway.gatewayControllerName` | The name of the Gateway controller. The controller name must be of the form: DOMAIN/PATH. The controller's domain is k8s-gateway.nginx.org. | k8s-gateway.nginx.org/nginx-gateway-controller | +|`nginx.image.repository` | The repository for the NGINX image. | nginx | +|`nginx.image.tag` | The tag for the NGINX image. | 1.25 | +|`nginx.image.pullPolicy` | The `imagePullPolicy` for the NGINX image. | Always | +|`initContainer.image.repository` | The repository for the `initContainer` image. | busybox | +|`initContainer.image.tag` | The tag for the `initContainer` image. | 1.36 | +|`serviceAccount.annotations` | The `annotations` for the ServiceAccount used by the NGINX Kubernetes Gateway deployment. | {} | +|`serviceAccount.name` | Name of the ServiceAccount used by the NGINX Kubernetes Gateway deployment. | Autogenerated | +|`service.create` | Creates a service to expose the NGINX Kubernetes Gateway pods. | true | +|`service.type` | The type of service to create for the NGINX Kubernetes Gateway. | Loadbalancer | +|`service.externalTrafficPolicy` | The `externalTrafficPolicy` of the service. The value `Local` preserves the client source IP. | Local | +|`service.annotations` | The `annotations` of the NGINX Kubernetes Gateway service. | {} | +|`service.ports` | A list of ports to expose through the NGINX Kubernetes Gateway service. Update it to match the listener ports from your Gateway resource. Follows the conventional Kubernetes yaml syntax for service ports. | [ port: 80, targetPort: 80, protocol: TCP, name: http; port: 443, targetPort: 443, protocol: TCP, name: https ] | diff --git a/deploy/helm-chart/chart-icon.png b/deploy/helm-chart/chart-icon.png new file mode 100644 index 0000000000..52961c9a6f Binary files /dev/null and b/deploy/helm-chart/chart-icon.png differ diff --git a/deploy/helm-chart/templates/_helpers.tpl b/deploy/helm-chart/templates/_helpers.tpl new file mode 100644 index 0000000000..3d687344c5 --- /dev/null +++ b/deploy/helm-chart/templates/_helpers.tpl @@ -0,0 +1,72 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "nginx-gateway.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "nginx-gateway.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "nginx-gateway.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "nginx-gateway.labels" -}} +helm.sh/chart: {{ include "nginx-gateway.chart" . }} +{{ include "nginx-gateway.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "nginx-gateway.selectorLabels" -}} +app.kubernetes.io/name: {{ include "nginx-gateway.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the ServiceAccount to use +*/}} +{{- define "nginx-gateway.serviceAccountName" -}} +{{- default (include "nginx-gateway.fullname" .) .Values.serviceAccount.name }} +{{- end }} + +{{/* +Expand default NGINX conf ConfigMap name. +*/}} +{{- define "nginx-gateway.nginx-conf" -}} +{{- printf "%s-%s" (include "nginx-gateway.fullname" .) "conf" -}} +{{- end -}} + +{{/* +Expand default njs-modules ConfigMap name. +*/}} +{{- define "nginx-gateway.njs-modules" -}} +{{- printf "%s-%s" (include "nginx-gateway.fullname" .) "njs-modules" -}} +{{- end -}} diff --git a/deploy/helm-chart/templates/deployment.yaml b/deploy/helm-chart/templates/deployment.yaml new file mode 100644 index 0000000000..a98d20a3ce --- /dev/null +++ b/deploy/helm-chart/templates/deployment.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nginx-gateway.fullname" . }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +spec: + # We only support a single replica for now + replicas: 1 + selector: + matchLabels: + app: nginx-gateway + {{- include "nginx-gateway.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app: nginx-gateway + {{- include "nginx-gateway.selectorLabels" . | nindent 8 }} + spec: + containers: + - args: + - static-mode + - --gateway-ctlr-name={{ .Values.nginxGateway.gatewayControllerName }} + - --gatewayclass={{ .Values.nginxGateway.gatewayClassName }} + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + image: {{ .Values.nginxGateway.image.repository }}:{{ .Values.nginxGateway.image.tag | default .Chart.AppVersion }} + imagePullPolicy: {{ .Values.nginxGateway.imagePullPolicy }} + name: nginx-gateway + securityContext: + capabilities: + add: + - KILL + drop: + - ALL + runAsUser: 1001 + volumeMounts: + - mountPath: /etc/nginx + name: nginx + - image: {{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }} + imagePullPolicy: {{ .Values.nginx.imagePullPolicy }} + name: nginx + ports: + - containerPort: 80 + name: http + - containerPort: 443 + name: https + securityContext: + capabilities: + add: + - CHOWN + - NET_BIND_SERVICE + - SETGID + - SETUID + - DAC_OVERRIDE + drop: + - ALL + volumeMounts: + - mountPath: /etc/nginx + name: nginx + - mountPath: /etc/nginx/nginx.conf + name: nginx-conf + subPath: nginx.conf + - mountPath: /var/lib/nginx + name: var-lib-nginx + - mountPath: /usr/lib/nginx/modules/njs + name: njs-modules + initContainers: + - command: + - sh + - -c + - rm -r /etc/nginx/conf.d /etc/nginx/secrets; mkdir /etc/nginx/conf.d /etc/nginx/secrets + && chown 1001:0 /etc/nginx/conf.d /etc/nginx/secrets + image: {{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }} + name: set-permissions + volumeMounts: + - mountPath: /etc/nginx + name: nginx + serviceAccountName: {{ include "nginx-gateway.serviceAccountName" . }} + shareProcessNamespace: true + volumes: + - emptyDir: {} + name: nginx + - configMap: + name: {{ include "nginx-gateway.nginx-conf" . }} + name: nginx-conf + - emptyDir: {} + name: var-lib-nginx + - configMap: + name: {{ include "nginx-gateway.njs-modules" . }} + name: njs-modules diff --git a/deploy/helm-chart/templates/gatewayclass.yaml b/deploy/helm-chart/templates/gatewayclass.yaml new file mode 100644 index 0000000000..b1288ba38b --- /dev/null +++ b/deploy/helm-chart/templates/gatewayclass.yaml @@ -0,0 +1,8 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: GatewayClass +metadata: + name: {{ .Values.nginxGateway.gatewayClassName }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +spec: + controllerName: {{ .Values.nginxGateway.gatewayControllerName }} diff --git a/deploy/helm-chart/templates/nginx-conf.yaml b/deploy/helm-chart/templates/nginx-conf.yaml new file mode 100644 index 0000000000..4574b11d6f --- /dev/null +++ b/deploy/helm-chart/templates/nginx-conf.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nginx-gateway.nginx-conf" . }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +data: + nginx.conf: |- + load_module /usr/lib/nginx/modules/ngx_http_js_module.so; + events {} + pid /etc/nginx/nginx.pid; + error_log stderr debug; + 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; + } diff --git a/deploy/helm-chart/templates/njs-modules.yaml b/deploy/helm-chart/templates/njs-modules.yaml new file mode 100644 index 0000000000..00ac33ec41 --- /dev/null +++ b/deploy/helm-chart/templates/njs-modules.yaml @@ -0,0 +1,215 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "nginx-gateway.njs-modules" . }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +data: + httpmatches.js: | + const MATCHES_VARIABLE = 'http_matches'; + const HTTP_CODES = { + notFound: 404, + internalServerError: 500, + }; + + function redirect(r) { + let matches; + + try { + matches = extractMatchesFromRequest(r); + } catch (e) { + r.error(e.message); + r.return(HTTP_CODES.internalServerError); + return; + } + + // Matches is a list of http matches in order of precedence. + // We will accept the first match that the request satisfies. + // If there's a match, redirect request to internal location block. + // If an exception occurs, return 500. + // If no matches are found, return 404. + let match; + try { + match = findWinningMatch(r, matches); + } catch (e) { + r.error(e.message); + r.return(HTTP_CODES.internalServerError); + return; + } + + if (!match) { + r.return(HTTP_CODES.notFound); + return; + } + + if (!match.redirectPath) { + r.error( + `cannot redirect the request; the match ${JSON.stringify( + match, + )} does not have a redirectPath set`, + ); + r.return(HTTP_CODES.internalServerError); + return; + } + + r.internalRedirect(match.redirectPath); + } + + function extractMatchesFromRequest(r) { + if (!r.variables[MATCHES_VARIABLE]) { + throw Error( + `cannot redirect the request; the variable ${MATCHES_VARIABLE} is not defined on the request object`, + ); + } + + let matches; + + try { + matches = JSON.parse(r.variables[MATCHES_VARIABLE]); + } catch (e) { + throw Error( + `cannot redirect the request; error parsing ${r.variables[MATCHES_VARIABLE]} into a JSON object: ${e}`, + ); + } + + if (!Array.isArray(matches)) { + throw Error(`cannot redirect the request; expected a list of matches, got ${matches}`); + } + + if (matches.length === 0) { + throw Error(`cannot redirect the request; matches is an empty list`); + } + + return matches; + } + + function findWinningMatch(r, matches) { + for (let i = 0; i < matches.length; i++) { + try { + let found = testMatch(r, matches[i]); + if (found) { + return matches[i]; + } + } catch (e) { + throw e; + } + } + + return null; + } + + function testMatch(r, match) { + // check for any + if (match.any) { + return true; + } + + // check method + if (match.method && r.method !== match.method) { + return false; + } + + // check headers + if (match.headers) { + try { + let found = headersMatch(r.headersIn, match.headers); + if (!found) { + return false; + } + } catch (e) { + throw e; + } + } + + // check params + if (match.params) { + try { + let found = paramsMatch(r.args, match.params); + if (!found) { + return false; + } + } catch (e) { + throw e; + } + } + + // all match conditions are satisfied so return true + return true; + } + + function headersMatch(requestHeaders, headers) { + for (let i = 0; i < headers.length; i++) { + const h = headers[i]; + const kv = h.split(':'); + + if (kv.length !== 2) { + throw Error(`invalid header match: ${h}`); + } + // Header names are compared in a case-insensitive manner, meaning header name "FOO" is equivalent to "foo". + // The NGINX request's headersIn object lookup is case-insensitive as well. + // This means that requestHeaders['FOO'] is equivalent to requestHeaders['foo']. + let val = requestHeaders[kv[0]]; + + if (!val) { + return false; + } + + // split on comma because nginx uses commas to delimit multiple header values + const values = val.split(','); + if (!values.includes(kv[1])) { + return false; + } + } + + return true; + } + + function paramsMatch(requestParams, params) { + for (let i = 0; i < params.length; i++) { + let p = params[i]; + // We store query parameter matches as strings with the format "key=value"; however, there may be more than one + // instance of "=" in the string. + // To recover the key and value, we need to find the first occurrence of "=" in the string. + const idx = params[i].indexOf('='); + // Check for an improperly constructed query parameter match. There are three possible error cases: + // (1) if the index is -1, then there are no "=" in the string (e.g. "keyvalue") + // (2) if the index is 0, then there is no value in the string (e.g. "key="). + // (3) if the index is equal to length -1, then there is no key in the string (e.g. "=value"). + if (idx === -1 || (idx === 0) | (idx === p.length - 1)) { + throw Error(`invalid query parameter: ${p}`); + } + + // Divide string into key value using the index. + let kv = [p.slice(0, idx), p.slice(idx + 1)]; + + // val can either be a string or an array of strings. + // Also, the NGINX request's args object lookup is case-sensitive. + // For example, 'a=1&b=2&A=3&b=4' will be parsed into {a: "1", b: ["2", "4"], A: "3"} + let val = requestParams[kv[0]]; + if (!val) { + return false; + } + + // If val is an array, we will match against the first element in the array according to the Gateway API spec. + if (Array.isArray(val)) { + val = val[0]; + } + + if (val !== kv[1]) { + return false; + } + } + + return true; + } + + export default { + redirect, + testMatch, + findWinningMatch, + headersMatch, + paramsMatch, + extractMatchesFromRequest, + HTTP_CODES, + MATCHES_VARIABLE, + }; diff --git a/deploy/helm-chart/templates/rbac.yaml b/deploy/helm-chart/templates/rbac.yaml new file mode 100644 index 0000000000..8b7f70b4d7 --- /dev/null +++ b/deploy/helm-chart/templates/rbac.yaml @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "nginx-gateway.serviceAccountName" . }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} + annotations: + {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "nginx-gateway.fullname" . }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - namespaces + - services + - secrets + verbs: + - list + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + - gateways + - httproutes + - referencegrants + verbs: + - list + - watch +- apiGroups: + - gateway.nginx.org + resources: + - gatewayconfigs + verbs: + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/status + - gateways/status + - gatewayclasses/status + verbs: + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "nginx-gateway.fullname" . }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "nginx-gateway.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "nginx-gateway.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/helm-chart/templates/service.yaml b/deploy/helm-chart/templates/service.yaml new file mode 100644 index 0000000000..0ef5c0e6fc --- /dev/null +++ b/deploy/helm-chart/templates/service.yaml @@ -0,0 +1,26 @@ +{{- if .Values.service.create }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nginx-gateway.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "nginx-gateway.labels" . | nindent 4 }} +{{- if .Values.service.annotations }} + annotations: +{{ toYaml .Values.service.annotations | indent 4 }} +{{- end }} +spec: +{{- if or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") }} + {{- if .Values.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} +{{- end }} + type: {{ .Values.service.type }} + ports: +{{- if .Values.service.ports }} +{{ toYaml .Values.service.ports | indent 2 }} +{{ end }} + selector: + {{- include "nginx-gateway.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/deploy/helm-chart/values.yaml b/deploy/helm-chart/values.yaml new file mode 100644 index 0000000000..afd7192a7f --- /dev/null +++ b/deploy/helm-chart/values.yaml @@ -0,0 +1,54 @@ +nginxGateway: + ## gatewayClassName is the name of the GatewayClass that will be created as part of this release. Every NGINX Gateway + ## must have a unique corresponding GatewayClass resource. NGINX Kubernetes Gateway only processes resources that + ## belong to its class - i.e. have the "gatewayClassName" field resource equal to the class. + gatewayClassName: nginx + ## The name of the Gateway controller. The controller name must be of the form: DOMAIN/PATH. The controller's domain + ## is k8s-gateway.nginx.org. + gatewayControllerName: k8s-gateway.nginx.org/nginx-gateway-controller + image: + ## The NGINX Kubernetes Gateway image to use + repository: ghcr.io/nginxinc/nginx-kubernetes-gateway + tag: edge + pullPolicy: Always + +nginx: + ## The NGINX image to use + image: + repository: nginx + tag: "1.25" + pullPolicy: Always + +initContainer: + image: + ## The image the init container should use. + repository: busybox + tag: "1.36" + +serviceAccount: + annotations: {} + ## The name of the service account of the NGINX Kubernetes Gateway pods. Used for RBAC. + ## Autogenerated if not set or set to "". + # name: nginx-gateway + +service: + ## Creates a service to expose the NGINX Kubernetes Gateway pods. + create: true + ## The type of service to create for the NGINX Kubernetes Gateway. + type: LoadBalancer + ## The externalTrafficPolicy of the service. The value Local preserves the client source IP. + externalTrafficPolicy: Local + ## The annotations of the NGINX Kubernetes Gateway service. + annotations: {} + + ## A list of ports to expose through the NGINX Kubernetes Gateway service. Update it to match the listener ports from + ## your Gateway resource. Follows the conventional Kubernetes yaml syntax for service ports. + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + - port: 443 + targetPort: 443 + protocol: TCP + name: https diff --git a/deploy/manifests/njs-modules.yaml b/deploy/manifests/njs-modules.yaml new file mode 100644 index 0000000000..881a0f045f --- /dev/null +++ b/deploy/manifests/njs-modules.yaml @@ -0,0 +1,214 @@ +apiVersion: v1 +data: + httpmatches.js: | + const MATCHES_VARIABLE = 'http_matches'; + const HTTP_CODES = { + notFound: 404, + internalServerError: 500, + }; + + function redirect(r) { + let matches; + + try { + matches = extractMatchesFromRequest(r); + } catch (e) { + r.error(e.message); + r.return(HTTP_CODES.internalServerError); + return; + } + + // Matches is a list of http matches in order of precedence. + // We will accept the first match that the request satisfies. + // If there's a match, redirect request to internal location block. + // If an exception occurs, return 500. + // If no matches are found, return 404. + let match; + try { + match = findWinningMatch(r, matches); + } catch (e) { + r.error(e.message); + r.return(HTTP_CODES.internalServerError); + return; + } + + if (!match) { + r.return(HTTP_CODES.notFound); + return; + } + + if (!match.redirectPath) { + r.error( + `cannot redirect the request; the match ${JSON.stringify( + match, + )} does not have a redirectPath set`, + ); + r.return(HTTP_CODES.internalServerError); + return; + } + + r.internalRedirect(match.redirectPath); + } + + function extractMatchesFromRequest(r) { + if (!r.variables[MATCHES_VARIABLE]) { + throw Error( + `cannot redirect the request; the variable ${MATCHES_VARIABLE} is not defined on the request object`, + ); + } + + let matches; + + try { + matches = JSON.parse(r.variables[MATCHES_VARIABLE]); + } catch (e) { + throw Error( + `cannot redirect the request; error parsing ${r.variables[MATCHES_VARIABLE]} into a JSON object: ${e}`, + ); + } + + if (!Array.isArray(matches)) { + throw Error(`cannot redirect the request; expected a list of matches, got ${matches}`); + } + + if (matches.length === 0) { + throw Error(`cannot redirect the request; matches is an empty list`); + } + + return matches; + } + + function findWinningMatch(r, matches) { + for (let i = 0; i < matches.length; i++) { + try { + let found = testMatch(r, matches[i]); + if (found) { + return matches[i]; + } + } catch (e) { + throw e; + } + } + + return null; + } + + function testMatch(r, match) { + // check for any + if (match.any) { + return true; + } + + // check method + if (match.method && r.method !== match.method) { + return false; + } + + // check headers + if (match.headers) { + try { + let found = headersMatch(r.headersIn, match.headers); + if (!found) { + return false; + } + } catch (e) { + throw e; + } + } + + // check params + if (match.params) { + try { + let found = paramsMatch(r.args, match.params); + if (!found) { + return false; + } + } catch (e) { + throw e; + } + } + + // all match conditions are satisfied so return true + return true; + } + + function headersMatch(requestHeaders, headers) { + for (let i = 0; i < headers.length; i++) { + const h = headers[i]; + const kv = h.split(':'); + + if (kv.length !== 2) { + throw Error(`invalid header match: ${h}`); + } + // Header names are compared in a case-insensitive manner, meaning header name "FOO" is equivalent to "foo". + // The NGINX request's headersIn object lookup is case-insensitive as well. + // This means that requestHeaders['FOO'] is equivalent to requestHeaders['foo']. + let val = requestHeaders[kv[0]]; + + if (!val) { + return false; + } + + // split on comma because nginx uses commas to delimit multiple header values + const values = val.split(','); + if (!values.includes(kv[1])) { + return false; + } + } + + return true; + } + + function paramsMatch(requestParams, params) { + for (let i = 0; i < params.length; i++) { + let p = params[i]; + // We store query parameter matches as strings with the format "key=value"; however, there may be more than one + // instance of "=" in the string. + // To recover the key and value, we need to find the first occurrence of "=" in the string. + const idx = params[i].indexOf('='); + // Check for an improperly constructed query parameter match. There are three possible error cases: + // (1) if the index is -1, then there are no "=" in the string (e.g. "keyvalue") + // (2) if the index is 0, then there is no value in the string (e.g. "key="). + // (3) if the index is equal to length -1, then there is no key in the string (e.g. "=value"). + if (idx === -1 || (idx === 0) | (idx === p.length - 1)) { + throw Error(`invalid query parameter: ${p}`); + } + + // Divide string into key value using the index. + let kv = [p.slice(0, idx), p.slice(idx + 1)]; + + // val can either be a string or an array of strings. + // Also, the NGINX request's args object lookup is case-sensitive. + // For example, 'a=1&b=2&A=3&b=4' will be parsed into {a: "1", b: ["2", "4"], A: "3"} + let val = requestParams[kv[0]]; + if (!val) { + return false; + } + + // If val is an array, we will match against the first element in the array according to the Gateway API spec. + if (Array.isArray(val)) { + val = val[0]; + } + + if (val !== kv[1]) { + return false; + } + } + + return true; + } + + export default { + redirect, + testMatch, + findWinningMatch, + headersMatch, + paramsMatch, + extractMatchesFromRequest, + HTTP_CODES, + MATCHES_VARIABLE, + }; +kind: ConfigMap +metadata: + creationTimestamp: null + name: njs-modules diff --git a/docs/developer/implementing-a-feature.md b/docs/developer/implementing-a-feature.md index 51c2651776..371b83af3b 100644 --- a/docs/developer/implementing-a-feature.md +++ b/docs/developer/implementing-a-feature.md @@ -40,6 +40,8 @@ practices to ensure a successful feature development process. > For security, a Docker image used in an example must be either managed by F5/NGINX or be an [official image](https://docs.docker.com/docker-hub/official_images/). - **Installation Changes**: If your feature involves changes to the installation process of NKG, update the [installation](/docs/installation.md) documentation. + - **Helm Changes**: If your feature introduces or changes any values of the NKG Helm Chart, update the + [Helm README](/deploy/helm-chart/README.md). - **Command-line Changes**: If your feature introduces or changes a command-line flag or subcommand, update the [cli help](/docs/cli-help.md) documentation. - **Other Documentation Updates**: For any other changes that affect the behavior, usage, or configuration of NKG, @@ -47,13 +49,16 @@ practices to ensure a successful feature development process. up to date with the latest changes. 11. **Lint code**: See the [run the linter](/docs/developer/quickstart.md#run-the-linter) section of the quickstart guide for instructions. -12. **Open pull request**: Open a pull request targeting the `main` branch of +12. **Run generators**: See the [Run go generate](/docs/developer/quickstart.md#run-go-generate) and the + [Update NJS module ConfigMaps](/docs/developer/quickstart.md#update-njs-module-configmaps) sections of the + quickstart guide for instructions. +13. **Open pull request**: Open a pull request targeting the `main` branch of the [nginx-kubernetes-gateway](https://github.com/nginxinc/nginx-kubernetes-gateway/tree/main) repository. The entire `nginx-kubernetes-gateway` group will be automatically requested for review. If you have a specific or different reviewer in mind, you can request them as well. Refer to the [pull request](/docs/developer/pull-request.md) documentation for expectations and guidelines. -13. **Obtain the necessary approvals**: Work with code reviewers to maintain the required number of approvals. -14. **Squash and merge**: Squash your commits locally, or use the GitHub UI to squash and merge. Only one commit per +14. **Obtain the necessary approvals**: Work with code reviewers to maintain the required number of approvals. +15. **Squash and merge**: Squash your commits locally, or use the GitHub UI to squash and merge. Only one commit per pull request should be merged. Make sure the first line of the final commit message includes the pull request number. For example, Fix supported gateway conditions in compatibility doc (#674). > **Note**: diff --git a/docs/developer/quickstart.md b/docs/developer/quickstart.md index 5fc16f1c20..91ccde545b 100644 --- a/docs/developer/quickstart.md +++ b/docs/developer/quickstart.md @@ -12,6 +12,7 @@ Follow these steps to set up your development environment. - [Go](https://golang.org/doc/install) - [Docker](https://docs.docker.com/get-docker/) v18.09+ - [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/) + - [Helm](https://helm.sh/docs/intro/quickstart/#install-helm) - [git](https://git-scm.com/) - [GNU Make](https://www.gnu.org/software/software.html) - [yq](https://github.com/mikefarah/yq/#install) @@ -124,3 +125,31 @@ make lint > **Note** > fieldalignment errors can be fixed by running: `fieldalignment -fix ` + +## Run the Helm Linter + +Run the following make command from the project's root directory to lint the Helm Chart code: + +```shell +make lint-helm +``` + +## Run go generate + +To ensure all the generated code is up to date, run the following make command from the project's root directory: + +```shell +make generate +``` + +## Update NJS module ConfigMaps + +To update the NJS ConfigMap yaml, run the following make command from the project's root directory: + +```shell +make generate-njs-yaml +``` + +Additionally, the [NJS ConfigMap Helm template](/deploy/helm-chart/templates/njs-modules.yaml) will need to be updated. +This is currently a manual process - ensure the content in the `data` field matches that in the +[NJS ConfigMap manifest](/deploy/manifests/njs-modules.yaml) `data` field. diff --git a/docs/installation.md b/docs/installation.md index 8bb3b4bed3..c3186f7e44 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,12 @@ This guide walks you through how to install NGINX Kubernetes Gateway on a generi - [kubectl](https://kubernetes.io/docs/tasks/tools/) -## Deploy NGINX Kubernetes Gateway +## Deploy NGINX Kubernetes Gateway using Helm + +To deploy NGINX Kubernetes Gateway using Helm, please follow the instructions on [this](/deploy/helm-chart/README.md) +page. + +## Deploy NGINX Kubernetes Gateway from Manifests > Note: NGINX Kubernetes Gateway can only run in the `nginx-gateway` namespace. > This limitation will be addressed in the future releases. @@ -33,7 +38,7 @@ This guide walks you through how to install NGINX Kubernetes Gateway on a generi 1. Create the njs-modules ConfigMap: ```shell - kubectl create configmap njs-modules --from-file=internal/mode/static/nginx/modules/src/httpmatches.js -n nginx-gateway + kubectl apply -f deploy/manifests/njs-modules.yaml -n nginx-gateway ``` 1. Create the ConfigMap with the main NGINX configuration file: diff --git a/docs/release-process.md b/docs/release-process.md index 8cc31206cd..e5cf15754d 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -33,18 +33,25 @@ To create a new release, follow these steps: 5. Create a release branch with a name that follows the `release-X.Y` format. 6. Prepare and merge a PR into the release branch to update the repo files for the release: 1. Update the tag of NKG container images used in the installation manifests (both the - [deployment manifest](../deploy/manifests/deployment.yaml) and the - [provisioner manifest](../conformance/provisioner/provisioner.yaml)) and docs to `X.Y.Z`. - 2. Ensure that the `imagePullPolicy` is `IfNotPresent` in the installation manifests. + [deployment manifest](../deploy/manifests/deployment.yaml) and the + [provisioner manifest](../conformance/provisioner/provisioner.yaml)), + the Helm [values.yaml](../deploy/helm-chart/values.yaml) file, and docs to `X.Y.Z`. + 2. Ensure that the `imagePullPolicy` is `IfNotPresent` in the installation manifests and the Helm + [values.yaml](../deploy/helm-chart/values.yaml) file. 3. Modify any `git clone` instructions to use `vX.Y.Z` tag. - 4. Adjust the `VERSION` variable in the [Makefile](../Makefile) and the `NKG_TAG` in the - [conformance tests Makefile](../conformance/Makefile) to `X.Y.Z`. - 5. Update the [README](../README.md) to include information about the release. - 6. Update the [changelog](../CHANGELOG.md). The changelog includes only important (from the user perspective) + 4. Update the Helm [Chart.yaml](../deploy/helm-chart/Chart.yaml): the `appVersion` to `X.Y.Z`, the icon and source + URLs to point at `vX.Y.Z`, and bump the `version`. + 5. Update the Helm [README](../deploy/helm-chart/README.md) `--version` flags in the helm commands to use the stable + `appVersion` from the previous step. + 6. Adjust the `VERSION` variable in the [Makefile](../Makefile) and the `NKG_TAG` in the + [conformance tests Makefile](../conformance/Makefile) to `X.Y.Z`. + 7. Update the [README](../README.md) to include information about the release. + 8. Update the [changelog](../CHANGELOG.md). The changelog includes only important (from the user perspective) changes to NKG. This is in contrast with the autogenerated full changelog, which is created in the next step. Use the previous changelog entries for formatting and content guidance. 7. Create and push the release tag in the format `vX.Y.Z`. As a result, the CI/CD pipeline will: - Build NKG container images with the release tag `X.Y.Z` and push it to the registry. + - Package and publish the Helm chart to the registry. - Create a GitHub release with an autogenerated changelog and attached release artifacts. 8. Prepare and merge a PR into the main branch to update the [README](../README.md) to include the information about the latest release and also the [changelog](../CHANGELOG.md). diff --git a/internal/mode/static/nginx/modules/README.md b/internal/mode/static/nginx/modules/README.md index d75409df83..4efb1395a7 100644 --- a/internal/mode/static/nginx/modules/README.md +++ b/internal/mode/static/nginx/modules/README.md @@ -111,3 +111,10 @@ This project uses [prettier](https://prettier.io/) to lint and format the JavaSc ```shell npm run format ``` + +## Generate the ConfigMap yaml + +The NJS modules are mounted to the NGINX container using a ConfigMap. Once you have completed your code changes, ensure +the [njs-modules.yaml](/deploy/manifests/njs-modules.yaml) and +[NJS ConfigMap Helm template](/deploy/helm-chart/templates/njs-modules.yaml) are updated to reflect the new code. See +[Update NJS module ConfigMaps](/docs/developer/quickstart.md#update-njs-module-configmaps).