diff --git a/.gitignore b/.gitignore
index 38f2524..2123d4a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,5 +21,5 @@ Thumbs.db
/.idea/
/src/__pycache__/
/src/v3_1/__pycache__/
-/contrib/devportal/src/__pycache__/
+/contrib/devportal/redocly/src/__pycache__/
/venv/
diff --git a/FEATURES.md b/FEATURES.md
index cb20192..baf0182 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -2,34 +2,40 @@
### NGINX `http` and `stream` servers
-| Feature | API v3.1 | API v4.0 | Notes |
-|----------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Upstreams | CRUD | CRUD |
Snippets supported: static and from source of truth |
-| HTTP servers | CRUD | CRUD | Snippets supported (`http`, `servers`, `locations`): static and from source of truth |
-| TCP/UDP servers | CRUD | CRUD | Snippets supported (`streams`, `servers`): static and from source of truth |
-| TLS | CRUD | CRUD | Certificates and keys can be dynamically fetched from source of truth |
-| mTLS | CRUD | CRUD | Certificates and keys can be dynamically fetched from source of truth |
-| JWT client authentication | | X | JWT key can be hardwired or fetched from source of truth |
-| Rate limiting | X | X | |
-| Active healthchecks | X | X | |
-| Cookie-based stickiness | X | X | |
-| Maps | X | X | |
-| NGINX Plus REST API access | X | X | |
-| NGINX App Protect WAF | X | X | Per-policy CRUD at `server` and `location` levelSupport for dataplane-based bundle compilationSecurity policies can be fetched from source of truth |
-
+| Feature | API v3.1 | API v4.0 | API v4.1 | Notes |
+|----------------------------|----------|----------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Upstreams | CRUD | CRUD | CRUD | Snippets supported: static and from source of truth |
+| HTTP servers | CRUD | CRUD | CRUD | Snippets supported (`http`, `servers`, `locations`): static and from source of truth |
+| TCP/UDP servers | CRUD | CRUD | CRUD | Snippets supported (`streams`, `servers`): static and from source of truth |
+| TLS | CRUD | CRUD | CRUD | Certificates and keys can be dynamically fetched from source of truth |
+| mTLS | CRUD | CRUD | CRUD | Certificates and keys can be dynamically fetched from source of truth |
+| JWT client authentication | | X | X | JWT key can be hardwired or fetched from source of truth |
+| Upstream authentication | | | X | Bearer tokenHTTP header |
+| Rate limiting | X | X | X | |
+| Active healthchecks | X | X | X | |
+| Cookie-based stickiness | X | X | X | |
+| Maps | X | X | X | |
+| NGINX Plus REST API access | X | X | X | |
+| NGINX App Protect WAF | X | X | X | Per-policy CRUD at `server` and `location` levelSupport for dataplane-based bundle compilationSecurity policies can be fetched from source of truth |
### API Gateway
-| Feature | API v3.1 | API v4.0 | Notes |
-|----------------------------------------------|----------|----------|----------------------------------------------------------|
-| Configuration generation from OpenAPI schema | X | X | |
-| HTTP methods enforcement | X | X | |
-| per-URI rate limiting | X | X | |
-| per-URI JWT authentication | X | X | JWT key can be hardwired or fetched from source of truth |
-
+| Feature | API v3.1 | API v4.0 | API v4.1 | Notes |
+|----------------------------------------------|----------|----------|----------|---------------------------------------------------------------------------|
+| Configuration generation from OpenAPI schema | X | X | X | |
+| HTTP methods enforcement | X | X | X | |
+| per-URI rate limiting | X | X | X | |
+| per-URI JWT authentication | X | X | X | Static JWT keyJWT fetched from URLBearer token |
### API Gateway - Developer Portal
-| Feature | API v3.1 | API v4.0 | Notes |
-|-------------------------------------------------|----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Developer Portal generation from OpenAPI schema | X | X | Based on Redocly |
+| Feature | API v3.1 | API v4.0 | API v4.1 | Notes |
+|-------------------------------------------------|----------|----------|----------|---------------------------|
+| Developer Portal generation from OpenAPI schema | X | X | X | Based on Redocly |
+
+### Source of truth
+
+| Feature | API v3.1 | API v4.0 | API v4.1 | Notes |
+|--------------------------------------|----------|----------|----------|-------|
+| HTTP header-based authentication | | | X | |
+| Bearer token authentication | | | X | |
diff --git a/README.md b/README.md
index 3022045..d7bbfc1 100644
--- a/README.md
+++ b/README.md
@@ -132,7 +132,7 @@ end
- [X] POST to Generic REST API endpoint
- [X] Output to NGINX Instance Manager 2.14+ imperative REST API
-## Supported NGINX Plus features
+## Supported features
See the [features list](/FEATURES.md)
@@ -140,7 +140,8 @@ See the [features list](/FEATURES.md)
Usage details and JSON schema are available here:
-- [API v4.0](/USAGE-v4.0.md) - latest
+- [API v4.1](/USAGE-v4.1.md) - latest
+- [API v4.0](/USAGE-v4.0.md)
- [API v3.1](/USAGE-v3.1.md)
A sample Postman collection and usage instructions can be found [here](/contrib/postman)
@@ -159,14 +160,10 @@ Docker images can be built and run using:
docker build --no-cache -t nginx-declarative-api -f ./Dockerfile .
docker run --name nginx-declarative-api -d -p 5000:5000 nginx-declarative-api
- cd contrib/devportal
+ cd contrib/redocly/devportal
docker build --no-cache -t nginx-declarative-api-devportal .
docker run --name devportal -d -p 5001:5000 nginx-declarative-api-devportal
-Pre-built docker images are available on Docker Hub at
-- https://hub.docker.com/repository/docker/fiorucci/nginx-declarative-api/general
-- https://hub.docker.com/repository/docker/fiorucci/nginx-declarative-api-devportal/general
-
Configuration can be customized mounting `config.toml` as a volume `nginx-declarative-api` docker image as a volume to customize
## REST API documentation
diff --git a/USAGE-v4.1.md b/USAGE-v4.1.md
new file mode 100644
index 0000000..4b5e1be
--- /dev/null
+++ b/USAGE-v4.1.md
@@ -0,0 +1,257 @@
+# Usage for API v4.1
+
+Version 4.1 API requires:
+
+- NGINX Instance Manager 2.14+
+- NGINX Plus R30+
+
+If NGINX App Protect declarations are used:
+- NGINX App Protect Policy Compiler 4.2.0+
+- NGINX Plus instances running App Protect WAF 4.2.0+ using `precompiled_publication: true` in `/etc/nginx-agent/nginx-agent.conf`
+
+The JSON schema is self explanatory. See also the [sample Postman collection](/contrib/postman)
+
+- `.output.type` defines how NGINX configuration will be returned:
+ - *plaintext* - plaintext format
+ - *json* - JSON-wrapped, base64-encoded
+ - *configmap* - Kubernetes Configmap in YAML format.
+ - `.output.configmap.name` must be set to the ConfigMap name
+ - `.output.configmap.filename` must be set to the NGINX configuration filename
+ - `.output.configmap.namespace` the optional namespace for the ConfigMap
+ - *http* - NGINX configuration is POSTed to custom url
+ - `.output.http.url` the URL to POST the configuration to
+ - *nms* - NGINX configuration is published as a Staged Config to NGINX Instance Manager
+ - `.output.nms.url` the NGINX Instance Manager URL
+ - `.output.nms.username` the NGINX Instance Manager authentication username
+ - `.output.nms.password` the NGINX Instance Manager authentication password
+ - `.output.nms.instancegroup` the NGINX Instance Manager instance group to publish the configuration to
+ - `.output.nms.synctime` **optional**, used for GitOps autosync. When specified and the declaration includes HTTP(S) references to NGINX App Protect policies, TLS certificates/keys/chains, the HTTP(S) endpoints will be checked every `synctime` seconds and if external contents have changed, the updated configuration will automatically be published to NGINX Instance Manager
+ - `.output.nms.modules` an optional array of NGINX module names (ie. 'ngx_http_app_protect_module', 'ngx_http_js_module','ngx_stream_js_module')
+ - `.output.nms.certificates` an optional array of TLS certificates/keys/chains to be published
+ - `.output.nms.certificates[].type` the item type ('certificate', 'key', 'chain')
+ - `.output.nms.certificates[].name` the certificate/key/chain name with no path/extension (ie. 'test-application')
+ - `.output.nms.certificates[].contents` the content: this can be either base64-encoded or be a HTTP(S) URL that will be fetched dynamically from a source of truth
+ - `.output.nms.policies[]` an optional array of NGINX App Protect security policies
+ - `.output.nms.policies[].type` the policy type ('app_protect')
+ - `.output.nms.policies[].name` the policy name (ie. 'prod-policy')
+ - `.output.nms.policies[].active_tag` the policy tag to enable among all available versions (ie. 'v1')
+ - `.output.nms.policies[].versions[]` array with all available policy versions
+ - `.output.nms.policies[].versions[].tag` the policy version's tag name
+ - `.output.nms.policies[].versions[].displayName` the policy version's display name
+ - `.output.nms.policies[].versions[].description` the policy version's description
+ - `.output.nms.policies[].versions[].contents` this can be either base64-encoded or be a HTTP(S) URL that will be fetched dynamically from a source of truth
+- `.declaration` describes the NGINX configuration to be created.
+
+### Locations ###
+
+Locations `.declaration.http.servers[].locations[].uri` match modifiers in `.declaration.http.servers[].locations[].urimatch` can be:
+
+- *prefix* - prefix URI matching
+- *exact* - exact URI matching
+- *regex* - case sensitive regex matching
+- *iregex* - case insensitive regex matching
+- *best* - case sensitive regex matching that halts any other location matching once a match is made
+
+### API Gateway ###
+
+Swagger files and OpenAPI schemas can be used to automatically configure NGINX as an API Gateway. Developer portal creation is supported through [Redocly](https://redocly.com/)
+
+Declaration path `.declaration.http.servers[].locations[].apigateway` defines the API Gateway configuration:
+
+- `openapi_schema` - the base64-encoded schema, or the schema URL. YAML and JSON are supported
+- `api_gateway.enabled` - enable/disable API Gateway provisioning
+- `api_gateway.strip_uri` - removes the `.declaration.http.servers[].locations[].uri` part of the URI before forwarding requests to the upstream
+- `api_gateway.server_url` - the base URL of the upstream server
+- `developer_portal.enabled` - enable/disable Developer portal provisioning
+- `developer_portal.uri` - the trailing part of the Developer portal URI, this is appended to `.declaration.http.servers[].locations[].uri`. If omitted it defaults to `devportal.html`
+- `authentication` - optional, used to enforce JWT authentication at the API Gateway level
+- `authentication.client` - JWT authentication profile name
+- `authentication.enforceOnPaths` - if set to `true` JWT authentication is enforced on all API endpoints listed under `authentication.paths`. if set to `false` JWT authentication is enforced on all API endpoints but those listed under `authentication.paths`
+- `rate_limit` - optional, used to enforce rate limiting at the API Gateway level
+- `rate_limit.enforceOnPaths` - if set to `true` rate limiting is enforced on all API endpoints listed under `rate_limit.paths`. if set to `false` rate limiting is enforced on all API endpoints but those listed under `rate_limit.paths`
+
+A sample API Gateway declaration to publish the `https://petstore.swagger.io` REST API and enforce:
+
+- REST API endpoint URIs
+- HTTP Methods
+- Rate limiting on `/user/login` and `/user/logout`
+- JWT authentication on `/user/login` and `/usr/logout`
+
+is:
+
+```commandline
+{
+ "output": {
+ "type": "nms",
+ "nms": {
+ "url": "{{nim_host}}",
+ "username": "{{nim_username}}",
+ "password": "{{nim_password}}",
+ "instancegroup": "{{nim_instancegroup}}",
+ "synctime": 0,
+ "modules": [
+ "ngx_http_js_module",
+ "ngx_stream_js_module"
+ ]
+ }
+ },
+ "declaration": {
+ "http": {
+ "servers": [
+ {
+ "name": "Petstore API",
+ "names": [
+ "apigw.nginx.lab"
+ ],
+ "resolver": "8.8.8.8",
+ "listen": {
+ "address": "80"
+ },
+ "log": {
+ "access": "/var/log/nginx/apigw.nginx.lab-access_log",
+ "error": "/var/log/nginx/apigw.nginx.lab-error_log"
+ },
+ "locations": [
+ {
+ "uri": "/petstore",
+ "urimatch": "prefix",
+ "apigateway": {
+ "openapi_schema": {
+ "content": "http://petstore.swagger.io/v2/swagger.json",
+ "authentication": [
+ {
+ "profile": "Source of truth authentication profile using HTTP header token authentication"
+ }
+ ]
+ },
+ "api_gateway": {
+ "enabled": true,
+ "strip_uri": true,
+ "server_url": "https://petstore.swagger.io/v2"
+ },
+ "developer_portal": {
+ "enabled": false,
+ "uri": "/petstore-devportal.html"
+ },
+ "authentication": {
+ "client": [
+ {
+ "profile": "Petstore JWT Authentication"
+ }
+ ],
+ "enforceOnPaths": true,
+ "paths": [
+ "/user/login",
+ "/user/logout"
+ ]
+ },
+ "rate_limit": [
+ {
+ "profile": "petstore_ratelimit",
+ "httpcode": 429,
+ "burst": 0,
+ "delay": 0,
+ "enforceOnPaths": true,
+ "paths": [
+ "/user/login",
+ "/user/logout"
+ ]
+ }
+ ]
+ },
+ "log": {
+ "access": "/var/log/nginx/petstore-access_log",
+ "error": "/var/log/nginx/petstore-error_log"
+ }
+ }
+ ]
+ }
+ ],
+ "rate_limit": [
+ {
+ "name": "petstore_ratelimit",
+ "key": "$binary_remote_addr",
+ "size": "10m",
+ "rate": "2r/s"
+ }
+ ],
+ "authentication": {
+ "client": [
+ {
+ "name": "Petstore JWT Authentication",
+ "type": "jwt",
+ "jwt": {
+ "realm": "Petstore Authentication",
+ "key": "{\"keys\": [{\"k\":\"ZmFudGFzdGljand0\",\"kty\":\"oct\",\"kid\":\"0001\"}]}",
+ "cachetime": 5
+ }
+ }
+ ],
+ "server": [
+ {
+ "name": "Source of truth authentication profile using HTTP header token authentication",
+ "type": "token",
+ "token": {
+ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU",
+ "type": "header",
+ "location": "X-AUTH-TOKEN"
+ }
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
+It can be tested using:
+
+```
+curl -iH "Host: apigw.nginx.lab" http:///petstore/store/inventory
+```
+
+Authentication failed:
+
+```
+curl -i http://apigw.nginx.lab/petstore/user/login
+```
+
+Authentication Succeeded:
+
+```
+curl -i http://apigw.nginx.lab/petstore/user/login -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU"
+```
+
+The API Developer portal can be accessed at:
+
+ http:///petstore/petstore-devportal.html
+
+### Maps ###
+
+Map entries `.declaration.maps[].entries.keymatch` can be:
+
+- *exact* - exact variable matching
+- *regex* - case sensitive regex matching
+- *iregex* - case insensitive regex matching
+
+### Snippets ###
+
+Snippets for http, upstream, server and location can be specified as:
+- base64-encoded content
+- HTTP(S) URL of a source of truth to fetch snippet content from. Content on the source of truth must be plaintext, it will be automatically base64-encoded
+
+### Methods ###
+
+- `POST /v4.1/config/` - Publish a new declaration
+- `PATCH /v4.1/config/{config_uid}` - Update an existing declaration
+ - Per-HTTP server CRUD
+ - Per-HTTP upstream CRUD
+ - Per-Stream server CRUD
+ - Per-Stream upstream CRUD
+ - Per-NGINX App Protect WAF policy CRUD
+- `GET /v4.1/config/{config_uid}` - Retrieve an existing declaration
+- `DELETE /v4.1/config/{config_uid}` - Delete an existing declaration
+
+### Usage Examples ###
+
+A sample Postman collection is available [here](/contrib/postman)
\ No newline at end of file
diff --git a/contrib/devportal/Dockerfile b/contrib/devportal/redocly/Dockerfile
similarity index 96%
rename from contrib/devportal/Dockerfile
rename to contrib/devportal/redocly/Dockerfile
index 8ea587b..5894a3f 100644
--- a/contrib/devportal/Dockerfile
+++ b/contrib/devportal/redocly/Dockerfile
@@ -2,7 +2,7 @@ FROM redocly/cli
WORKDIR /deployment
-COPY src/ src/
+COPY src src/
RUN apk update && \
apk add --update --no-cache bash python3 && \
diff --git a/contrib/devportal/README.md b/contrib/devportal/redocly/README.md
similarity index 100%
rename from contrib/devportal/README.md
rename to contrib/devportal/redocly/README.md
diff --git a/contrib/devportal/src/requirements.txt b/contrib/devportal/redocly/src/requirements.txt
similarity index 100%
rename from contrib/devportal/src/requirements.txt
rename to contrib/devportal/redocly/src/requirements.txt
diff --git a/contrib/devportal/src/server.py b/contrib/devportal/redocly/src/server.py
similarity index 98%
rename from contrib/devportal/src/server.py
rename to contrib/devportal/redocly/src/server.py
index 42a1eab..916ba56 100644
--- a/contrib/devportal/src/server.py
+++ b/contrib/devportal/redocly/src/server.py
@@ -10,7 +10,7 @@
from fastapi.responses import PlainTextResponse, Response, JSONResponse
app = FastAPI(
- title="Redoc connector",
+ title="Redocly connector",
version="1.0.0",
contact={"name": "GitHub", "url": "https://github.com/f5devcentral/NGINX-Declarative-API"}
)
diff --git a/contrib/devportal/src/start.sh b/contrib/devportal/redocly/src/start.sh
similarity index 100%
rename from contrib/devportal/src/start.sh
rename to contrib/devportal/redocly/src/start.sh
diff --git a/contrib/docker-compose/docker-compose.yaml b/contrib/docker-compose/docker-compose.yaml
index 4996313..273f8f0 100644
--- a/contrib/docker-compose/docker-compose.yaml
+++ b/contrib/docker-compose/docker-compose.yaml
@@ -20,7 +20,7 @@ services:
#image: fiorucci/nginx-declarative-api-devportal:latest
image: nginx-declarative-api-devportal
build:
- context: ../devportal/
+ context: ../devportal/redocly
dockerfile: Dockerfile
user: "${USERID}:${USERGROUP}"
container_name: "devportal"
diff --git a/contrib/gitops-examples/v4.0/www.online-boutique.local.crt b/contrib/gitops-examples/v4.0/www.online-boutique.local.crt
new file mode 100644
index 0000000..062f3d8
--- /dev/null
+++ b/contrib/gitops-examples/v4.0/www.online-boutique.local.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDRjCCAi6gAwIBAgIUccT8pYiNoy0AQnkrUZrVCXhnm70wDQYJKoZIhvcNAQEL
+BQAwSDEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEiMCAGA1UE
+AwwZd3d3Lm9ubGluZS1ib3V0aXF1ZS5sb2NhbDAeFw0yMzAyMTQxNTA2NDRaFw0z
+MzAyMTExNTA2NDRaMEgxIjAgBgkqhkiG9w0BCQEWE215QGVtYWlsYWRkcmVzcy5j
+b20xIjAgBgNVBAMMGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDasqlTnb5WlPHkjiLnjFX27QBm2BgDcA+x
+Y37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3ywanBRTMVaBUCKBduET/o
+N+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh2tvSg+a/3wwmIPoScXIu
+R1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/OlCdaUrShbraKg6oVHK1
++2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+YN6MfMhYvQei537569N9
+wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8gt/dQMfZAgMBAAGjKDAm
+MCQGA1UdEQQdMBuCGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZIhvcN
+AQELBQADggEBAKLCxBakmK43tbf+2pwgbRLYuOvz4A8GCh/EK2F1OKeuw0oIat7M
+Nn5SxSgNFj45XswaRsR3/ObLovrYmXzZnV9IPNHIsB0/sBsyqEYMA55hJsb/8HEd
++bWv2UJdcAimhbLMi1bpgSlSCNMn/CWkX09e3IjRrYkZxT50/ahOBBfBshoY+jwe
+sA1VDwZ4neXfrTUXJ4ixiTOfna2TpADLNxk6pncr6uiIpJSmLAq9jQwPBNlrpRN3
+TCyKKcadGdUPkALXm0+dMn1OHphy7ZQADMxPwxLySuoCK8kcslpGLg18zPXYQkoD
+C1KAguWu/VU4EBT39aC5/9askw+zV4XE9hw=
+-----END CERTIFICATE-----
diff --git a/contrib/gitops-examples/v4.0/www.online-boutique.local.key b/contrib/gitops-examples/v4.0/www.online-boutique.local.key
new file mode 100644
index 0000000..5a759e7
--- /dev/null
+++ b/contrib/gitops-examples/v4.0/www.online-boutique.local.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDasqlTnb5WlPHk
+jiLnjFX27QBm2BgDcA+xY37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3y
+wanBRTMVaBUCKBduET/oN+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh
+2tvSg+a/3wwmIPoScXIuR1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/
+OlCdaUrShbraKg6oVHK1+2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+
+YN6MfMhYvQei537569N9wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8
+gt/dQMfZAgMBAAECggEBAIcP85baQqSCE+mNBm1ts+FteOQK7LmiyW5J/hxjIktX
+XyVV0qCwr9twtwV7RR/6eYq7155FpIqZHdDgXZAoxmWgA+lzDRVCve8SI2MbjNks
+MBTnW0CinlyRfGQbsFvJpp2GM2/YOrdSJuIPIdce8rDodT5O+2HMgjGmiHepOCaH
+4XS42Av9KDbcbo2rSfAhSulsXJetd9G7luD3dY3Lsp2TC6ihzwZOtGSHJjoY29uS
+D+MJvAq2MzZxRnyE6vP88NY45rZug1YS7EHs0v6NAnI6/Ew6M3sAjss+qhBL/k6j
+AMWAibn1oTgbN6ifXapXpynQOSU1IvEJqXNHA+YQMEkCgYEA/t7TihpRoJRcMzx6
+4XtKMMDS/ixv7QUmOnJ1AO1Je3hjJK+2U1wARYLHoVdlZYTb9GRRlH6UHPL3lh2m
+ZWfDHRjKzQ+E4jlYFvQMAmAhbSqCAPtyFadQf7qLDKZAT/1WUj+Ynh0vjUUigqKj
+VsukDJkwqSt3jCk2ECtddcTicCcCgYEA26rLSaDx/+bDYVlJW9nXt8/N2ZYqtBev
+I+AV1qgVYjlskbcMch3apIVELiOEVfBaIGJDoq1N3e25cTvNNO/mA2rDSYLQuz7L
+bwlVs6ix02/tFd1R0N5nGKMhziue1l3BOjADHzf/UkOc4UIJGkmUQHrVxpby6FMY
+cT+HJ3+OB/8CgYAHtDqWuRbwkKt8zP415KEoehumnTdA5d/y8lTBE2seNVRh3oHX
+YTeM4lggc2DYQbzYVVP19iGKAnojaoAGHq3SKlau/iIZKHyWLQhT5g38m8VUPEWF
+jAot4jijyD63bEP4tn1pgh5W2dkiM8JWNE+gJd1Hr82sSe6dbIIlti7WDwKBgA3n
+PX6nhSmhPXSH5jC+FP05VdoTuxgy947ZvAgeE1xoLgr6/vqqERCgrrQM429dCxdJ
+oOZG+cq2JSqZkl9rX6+PrlSUxwlS7CVW/emlH7w2NVdQ7sC9kuDoUlduQ1tmC7jX
+GRt8u9hFF0TanSDgz1VVcPpky3MQ71cboj5JwH+ZAoGAAcDSlHuOu2xnbdnaTdE1
+MK0q/Yt3EOCGp0FRBkResPhhoV/TXadk0sJJbWFnleRFDkmgsw2QAwVhKQHCRWOu
+b4G8EnEXY/91IoRSevWtZTNf+/9+y5w4iOWI3rGJNpXCEgzN7Yc60z1sIsC/fu+Q
+YxXNiLIYUB4Pk4pDNRNLMZo=
+-----END PRIVATE KEY-----
diff --git a/contrib/gitops-examples/v4.0/www2.online-boutique.local.crt b/contrib/gitops-examples/v4.0/www2.online-boutique.local.crt
new file mode 100644
index 0000000..76e8157
--- /dev/null
+++ b/contrib/gitops-examples/v4.0/www2.online-boutique.local.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSTCCAjGgAwIBAgIUbzaZNPdYel80IQ81PCNfMllLnYgwDQYJKoZIhvcNAQEL
+BQAwSTEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEjMCEGA1UE
+Awwad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwHhcNMjMwMjE0MTUwNzMxWhcN
+MzMwMjExMTUwNzMxWjBJMSIwIAYJKoZIhvcNAQkBFhNteUBlbWFpbGFkZHJlc3Mu
+Y29tMSMwIQYDVQQDDBp3d3cyLm9ubGluZS1ib3V0aXF1ZS5sb2NhbDCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2OHfrfxO6hhg4dGMi0Q4+iwuuNGv2J
+eh+t8g9A+dGB4zUkU0SEa2EFLjyAw2P7F1XAtiClHmS9nA0uTuj9nuw9Xk+wFUlm
+ahMkdkLBRNNsH3O1gTYUQSOK7xm8FOMBnOYQqJOHILXsXgM2z8QHpX//wl15aGXf
+9vPm4CQUqqsEGNWDfpOQ8jfhrA4T8W1WuAXzJAQmZfhW1qMWxoj1iTCk+Q6nH/8a
+MWLWPjTes0bTSRXRTpuQCyeHCaqS8oPPshcd52/FXChnYbNAG1s9Q+35MrgDztL6
+dPkNiA9UaUY7pumNip93ZnsYwDmOPnDBPqfvy9Dk1zQuaAsJ1FXwRNsCAwEAAaMp
+MCcwJQYDVR0RBB4wHIIad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZI
+hvcNAQELBQADggEBADT2SchC4VhWqCYRsw/3nqrVh0JQmD+/x9JjiNsY6fLnG6uW
+8bs8/714qoghJff67H60B6NbrS3lpTZ3bmyotcGtwNNsY4QSFHRu/x4OyIrTfjKb
+VIdhRM2Atwc1s1YA6c+2JBquBDhniqABKG9u+j1aa2ElXSalCj+Kozm8ma0yduVw
+TX8zS6XZl57vSk/Qo/PZvbmbs8EMOwTUCLn6WQldAARCLughjd9LI9prNpBlYon6
+jmZ8oi59arK16cKe6i6tQ3QExT2kQsLlrK/jFw0xqrFnveKgUTeevViT4WZnqsCm
+awrPhrbDn0F7HmFSrgenrW8BIuJ2crBHy0230Lc=
+-----END CERTIFICATE-----
diff --git a/contrib/gitops-examples/v4.0/www2.online-boutique.local.key b/contrib/gitops-examples/v4.0/www2.online-boutique.local.key
new file mode 100644
index 0000000..b1e2db4
--- /dev/null
+++ b/contrib/gitops-examples/v4.0/www2.online-boutique.local.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdjh3638TuoYYO
+HRjItEOPosLrjRr9iXofrfIPQPnRgeM1JFNEhGthBS48gMNj+xdVwLYgpR5kvZwN
+Lk7o/Z7sPV5PsBVJZmoTJHZCwUTTbB9ztYE2FEEjiu8ZvBTjAZzmEKiThyC17F4D
+Ns/EB6V//8JdeWhl3/bz5uAkFKqrBBjVg36TkPI34awOE/FtVrgF8yQEJmX4Vtaj
+FsaI9YkwpPkOpx//GjFi1j403rNG00kV0U6bkAsnhwmqkvKDz7IXHedvxVwoZ2Gz
+QBtbPUPt+TK4A87S+nT5DYgPVGlGO6bpjYqfd2Z7GMA5jj5wwT6n78vQ5Nc0LmgL
+CdRV8ETbAgMBAAECggEBAJkAy010eWC+5YAbsgDxFHM+WNQo90m+RjtYegD0w1Ff
+HNSXSHXZ6PnwhPS9i7IhNgU/d8dloG67zuf/YflfJQBKhTYNCTZOZtTfalhBdlF3
+quTSpO/+3tk32lVwYRBADeWH/ZMcT4ezO12G3can7TBPJSA+ds1b9QSNHZ9tMku0
+2J2pzT8C8V0vHn49oKO9D4Q+QwRvisT6FKNaOW/xKZjtUIvyjVvjENnBW7MQ5y5z
+IHsUNkwGgZ2V2IbDouM4+A42J9iDsseM2euSsaDfum3vj9MPTqXE8zUUdX+/miMq
+RJTXLxDdkPaCbQftQ7y/cIAFLDZS8Fvvsa5pX69jl1ECgYEA8sQ6S7kcyYgBLX4T
+KVYPg1sFTgCofW5nTgCND4kiVvIDz/uk9vsfuMj7FMS3fcCfZorPdYEKa+Zwi4wZ
+NMCSArtezbwFBeaOiZTpjh2k8CBrtumdcU/EfzQOvrVcSzVc27ditwRAmONeBXVw
+GSvqSfjQKLDh/4Y191qOmgyIBDMCgYEA6aHjQr43XM811VQhTPBtLlBccD8hCS23
+NcujPMa85Vz+RES3OqHt2nH9NGuYrexGbrSt3ckjxnO+TdsS4f94bKhquXvv7AkX
+7qr8KZEwHncyjcL3uTChbjWr2xJR4lSnS5PAbnWfyR48HXkHO9IVPe/IcYnvuGDN
+AeMJkCgp1LkCgYEArvD/PXAxMX1js9/FeSU+Wp8t7G8G/BSiNxColkhxSYxveOJT
+l3OSAXw7i1TTEbjMZX2kUH3j/6t48ObNhzk6PuO9Rq62Q/FISBbaU4JDSJNka9Rf
+k7cy16Ow+HcDAmN6/g5iAZb74fD+4Rom5MzDsfiuMJR+179khlJortRW9AcCgYAD
+4AKD9eG3MVykOCwBOa+l6AFQf0uN+msigkkn1egGKd+xxC4B0/O8/s0DVJGIuPWG
+GosTtaVZQkwywGJ0yyb1Lmnuv6aAFLqH4+Ag1F6m8rUs8sHnGW5kBJHgJVKkXWEU
++NNlQaAv1seKeZpsHJTrnRGHCJGoTjq4QErFUFU5SQKBgEYMGdnPP4ZDAeezPCML
+42RVHOAG9IsMl4mV7+fwcEHcmuvb8nNgKNfayjG/NbiQWtT4v7iQH4GHYzUobaqr
+3xnQlec3jBrLEsWR8ItRWKxKO2GGLlbmQ99Kn3UGh+6FtXbxwB0hhkFfKXJzM10N
+Y9ruUdyYpjGO/j9ROjoBepUH
+-----END PRIVATE KEY-----
diff --git a/contrib/gitops-examples/v4.1/nap-policy-gitops.json b/contrib/gitops-examples/v4.1/nap-policy-gitops.json
new file mode 100644
index 0000000..1fbaaf8
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/nap-policy-gitops.json
@@ -0,0 +1,23 @@
+{
+ "policy": {
+ "name": "prod-policy",
+ "template": {
+ "name": "POLICY_TEMPLATE_NGINX_BASE"
+ },
+ "applicationLanguage": "utf-8",
+ "enforcementMode": "blocking",
+ "signature-sets": [
+ {
+ "name": "All Signatures",
+ "block": true,
+ "alarm": true
+ }
+ ],
+ "signatures": [
+ {
+ "signatureId": 200001834,
+ "enabled": false
+ }
+ ]
+ }
+}
diff --git a/contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json b/contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json
new file mode 100644
index 0000000..2fe91c8
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/nap-policy-xss-allowed.json
@@ -0,0 +1,39 @@
+{
+ "policy": {
+ "name": "prod-policy",
+ "template": {
+ "name": "POLICY_TEMPLATE_NGINX_BASE"
+ },
+ "applicationLanguage": "utf-8",
+ "enforcementMode": "blocking",
+ "signature-sets": [
+ {
+ "name": "All Signatures",
+ "block": true,
+ "alarm": true
+ }
+ ],
+ "signatures": [
+ {
+ "signatureId": 200001834,
+ "enabled": false
+ },
+ {
+ "signatureId": 200001475,
+ "enabled": false
+ },
+ {
+ "signatureId": 200000098,
+ "enabled": false
+ },
+ {
+ "signatureId": 200001088,
+ "enabled": false
+ },
+ {
+ "signatureId": 200101609,
+ "enabled": false
+ }
+ ]
+ }
+}
diff --git a/contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json b/contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json
new file mode 100644
index 0000000..1fbaaf8
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/nap-policy-xss-blocked.json
@@ -0,0 +1,23 @@
+{
+ "policy": {
+ "name": "prod-policy",
+ "template": {
+ "name": "POLICY_TEMPLATE_NGINX_BASE"
+ },
+ "applicationLanguage": "utf-8",
+ "enforcementMode": "blocking",
+ "signature-sets": [
+ {
+ "name": "All Signatures",
+ "block": true,
+ "alarm": true
+ }
+ ],
+ "signatures": [
+ {
+ "signatureId": 200001834,
+ "enabled": false
+ }
+ ]
+ }
+}
diff --git a/contrib/gitops-examples/v4.1/www.online-boutique.local.chain b/contrib/gitops-examples/v4.1/www.online-boutique.local.chain
new file mode 100644
index 0000000..062f3d8
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/www.online-boutique.local.chain
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDRjCCAi6gAwIBAgIUccT8pYiNoy0AQnkrUZrVCXhnm70wDQYJKoZIhvcNAQEL
+BQAwSDEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEiMCAGA1UE
+AwwZd3d3Lm9ubGluZS1ib3V0aXF1ZS5sb2NhbDAeFw0yMzAyMTQxNTA2NDRaFw0z
+MzAyMTExNTA2NDRaMEgxIjAgBgkqhkiG9w0BCQEWE215QGVtYWlsYWRkcmVzcy5j
+b20xIjAgBgNVBAMMGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDasqlTnb5WlPHkjiLnjFX27QBm2BgDcA+x
+Y37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3ywanBRTMVaBUCKBduET/o
+N+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh2tvSg+a/3wwmIPoScXIu
+R1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/OlCdaUrShbraKg6oVHK1
++2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+YN6MfMhYvQei537569N9
+wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8gt/dQMfZAgMBAAGjKDAm
+MCQGA1UdEQQdMBuCGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZIhvcN
+AQELBQADggEBAKLCxBakmK43tbf+2pwgbRLYuOvz4A8GCh/EK2F1OKeuw0oIat7M
+Nn5SxSgNFj45XswaRsR3/ObLovrYmXzZnV9IPNHIsB0/sBsyqEYMA55hJsb/8HEd
++bWv2UJdcAimhbLMi1bpgSlSCNMn/CWkX09e3IjRrYkZxT50/ahOBBfBshoY+jwe
+sA1VDwZ4neXfrTUXJ4ixiTOfna2TpADLNxk6pncr6uiIpJSmLAq9jQwPBNlrpRN3
+TCyKKcadGdUPkALXm0+dMn1OHphy7ZQADMxPwxLySuoCK8kcslpGLg18zPXYQkoD
+C1KAguWu/VU4EBT39aC5/9askw+zV4XE9hw=
+-----END CERTIFICATE-----
diff --git a/contrib/gitops-examples/v4.1/www.online-boutique.local.crt b/contrib/gitops-examples/v4.1/www.online-boutique.local.crt
new file mode 100644
index 0000000..062f3d8
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/www.online-boutique.local.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDRjCCAi6gAwIBAgIUccT8pYiNoy0AQnkrUZrVCXhnm70wDQYJKoZIhvcNAQEL
+BQAwSDEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEiMCAGA1UE
+AwwZd3d3Lm9ubGluZS1ib3V0aXF1ZS5sb2NhbDAeFw0yMzAyMTQxNTA2NDRaFw0z
+MzAyMTExNTA2NDRaMEgxIjAgBgkqhkiG9w0BCQEWE215QGVtYWlsYWRkcmVzcy5j
+b20xIjAgBgNVBAMMGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDasqlTnb5WlPHkjiLnjFX27QBm2BgDcA+x
+Y37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3ywanBRTMVaBUCKBduET/o
+N+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh2tvSg+a/3wwmIPoScXIu
+R1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/OlCdaUrShbraKg6oVHK1
++2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+YN6MfMhYvQei537569N9
+wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8gt/dQMfZAgMBAAGjKDAm
+MCQGA1UdEQQdMBuCGXd3dy5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZIhvcN
+AQELBQADggEBAKLCxBakmK43tbf+2pwgbRLYuOvz4A8GCh/EK2F1OKeuw0oIat7M
+Nn5SxSgNFj45XswaRsR3/ObLovrYmXzZnV9IPNHIsB0/sBsyqEYMA55hJsb/8HEd
++bWv2UJdcAimhbLMi1bpgSlSCNMn/CWkX09e3IjRrYkZxT50/ahOBBfBshoY+jwe
+sA1VDwZ4neXfrTUXJ4ixiTOfna2TpADLNxk6pncr6uiIpJSmLAq9jQwPBNlrpRN3
+TCyKKcadGdUPkALXm0+dMn1OHphy7ZQADMxPwxLySuoCK8kcslpGLg18zPXYQkoD
+C1KAguWu/VU4EBT39aC5/9askw+zV4XE9hw=
+-----END CERTIFICATE-----
diff --git a/contrib/gitops-examples/v4.1/www.online-boutique.local.key b/contrib/gitops-examples/v4.1/www.online-boutique.local.key
new file mode 100644
index 0000000..5a759e7
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/www.online-boutique.local.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDasqlTnb5WlPHk
+jiLnjFX27QBm2BgDcA+xY37biaMp+I1bTUd57hDM1X+kBeLZc98xe7hc/HaO5+3y
+wanBRTMVaBUCKBduET/oN+DdBAj8c64vov9a064sjXwYSeeh8lPqX52R5DYWxpHh
+2tvSg+a/3wwmIPoScXIuR1Wq7RY//dsshIHQkxVa9VAPYjLnusQjH9zOzJGZOxQ/
+OlCdaUrShbraKg6oVHK1+2h3M2S7RFjXFr8mbpxSgez1w9oN1YI+RM/kLPgI+Kq+
+YN6MfMhYvQei537569N9wJ9bl/i34EbM5gDf8BxhJJbo+ypEC9h0H8FmSXbqXcb8
+gt/dQMfZAgMBAAECggEBAIcP85baQqSCE+mNBm1ts+FteOQK7LmiyW5J/hxjIktX
+XyVV0qCwr9twtwV7RR/6eYq7155FpIqZHdDgXZAoxmWgA+lzDRVCve8SI2MbjNks
+MBTnW0CinlyRfGQbsFvJpp2GM2/YOrdSJuIPIdce8rDodT5O+2HMgjGmiHepOCaH
+4XS42Av9KDbcbo2rSfAhSulsXJetd9G7luD3dY3Lsp2TC6ihzwZOtGSHJjoY29uS
+D+MJvAq2MzZxRnyE6vP88NY45rZug1YS7EHs0v6NAnI6/Ew6M3sAjss+qhBL/k6j
+AMWAibn1oTgbN6ifXapXpynQOSU1IvEJqXNHA+YQMEkCgYEA/t7TihpRoJRcMzx6
+4XtKMMDS/ixv7QUmOnJ1AO1Je3hjJK+2U1wARYLHoVdlZYTb9GRRlH6UHPL3lh2m
+ZWfDHRjKzQ+E4jlYFvQMAmAhbSqCAPtyFadQf7qLDKZAT/1WUj+Ynh0vjUUigqKj
+VsukDJkwqSt3jCk2ECtddcTicCcCgYEA26rLSaDx/+bDYVlJW9nXt8/N2ZYqtBev
+I+AV1qgVYjlskbcMch3apIVELiOEVfBaIGJDoq1N3e25cTvNNO/mA2rDSYLQuz7L
+bwlVs6ix02/tFd1R0N5nGKMhziue1l3BOjADHzf/UkOc4UIJGkmUQHrVxpby6FMY
+cT+HJ3+OB/8CgYAHtDqWuRbwkKt8zP415KEoehumnTdA5d/y8lTBE2seNVRh3oHX
+YTeM4lggc2DYQbzYVVP19iGKAnojaoAGHq3SKlau/iIZKHyWLQhT5g38m8VUPEWF
+jAot4jijyD63bEP4tn1pgh5W2dkiM8JWNE+gJd1Hr82sSe6dbIIlti7WDwKBgA3n
+PX6nhSmhPXSH5jC+FP05VdoTuxgy947ZvAgeE1xoLgr6/vqqERCgrrQM429dCxdJ
+oOZG+cq2JSqZkl9rX6+PrlSUxwlS7CVW/emlH7w2NVdQ7sC9kuDoUlduQ1tmC7jX
+GRt8u9hFF0TanSDgz1VVcPpky3MQ71cboj5JwH+ZAoGAAcDSlHuOu2xnbdnaTdE1
+MK0q/Yt3EOCGp0FRBkResPhhoV/TXadk0sJJbWFnleRFDkmgsw2QAwVhKQHCRWOu
+b4G8EnEXY/91IoRSevWtZTNf+/9+y5w4iOWI3rGJNpXCEgzN7Yc60z1sIsC/fu+Q
+YxXNiLIYUB4Pk4pDNRNLMZo=
+-----END PRIVATE KEY-----
diff --git a/contrib/gitops-examples/v4.1/www2.online-boutique.local.chain b/contrib/gitops-examples/v4.1/www2.online-boutique.local.chain
new file mode 100644
index 0000000..76e8157
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/www2.online-boutique.local.chain
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSTCCAjGgAwIBAgIUbzaZNPdYel80IQ81PCNfMllLnYgwDQYJKoZIhvcNAQEL
+BQAwSTEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEjMCEGA1UE
+Awwad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwHhcNMjMwMjE0MTUwNzMxWhcN
+MzMwMjExMTUwNzMxWjBJMSIwIAYJKoZIhvcNAQkBFhNteUBlbWFpbGFkZHJlc3Mu
+Y29tMSMwIQYDVQQDDBp3d3cyLm9ubGluZS1ib3V0aXF1ZS5sb2NhbDCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2OHfrfxO6hhg4dGMi0Q4+iwuuNGv2J
+eh+t8g9A+dGB4zUkU0SEa2EFLjyAw2P7F1XAtiClHmS9nA0uTuj9nuw9Xk+wFUlm
+ahMkdkLBRNNsH3O1gTYUQSOK7xm8FOMBnOYQqJOHILXsXgM2z8QHpX//wl15aGXf
+9vPm4CQUqqsEGNWDfpOQ8jfhrA4T8W1WuAXzJAQmZfhW1qMWxoj1iTCk+Q6nH/8a
+MWLWPjTes0bTSRXRTpuQCyeHCaqS8oPPshcd52/FXChnYbNAG1s9Q+35MrgDztL6
+dPkNiA9UaUY7pumNip93ZnsYwDmOPnDBPqfvy9Dk1zQuaAsJ1FXwRNsCAwEAAaMp
+MCcwJQYDVR0RBB4wHIIad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZI
+hvcNAQELBQADggEBADT2SchC4VhWqCYRsw/3nqrVh0JQmD+/x9JjiNsY6fLnG6uW
+8bs8/714qoghJff67H60B6NbrS3lpTZ3bmyotcGtwNNsY4QSFHRu/x4OyIrTfjKb
+VIdhRM2Atwc1s1YA6c+2JBquBDhniqABKG9u+j1aa2ElXSalCj+Kozm8ma0yduVw
+TX8zS6XZl57vSk/Qo/PZvbmbs8EMOwTUCLn6WQldAARCLughjd9LI9prNpBlYon6
+jmZ8oi59arK16cKe6i6tQ3QExT2kQsLlrK/jFw0xqrFnveKgUTeevViT4WZnqsCm
+awrPhrbDn0F7HmFSrgenrW8BIuJ2crBHy0230Lc=
+-----END CERTIFICATE-----
diff --git a/contrib/gitops-examples/v4.1/www2.online-boutique.local.crt b/contrib/gitops-examples/v4.1/www2.online-boutique.local.crt
new file mode 100644
index 0000000..76e8157
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/www2.online-boutique.local.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSTCCAjGgAwIBAgIUbzaZNPdYel80IQ81PCNfMllLnYgwDQYJKoZIhvcNAQEL
+BQAwSTEiMCAGCSqGSIb3DQEJARYTbXlAZW1haWxhZGRyZXNzLmNvbTEjMCEGA1UE
+Awwad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwHhcNMjMwMjE0MTUwNzMxWhcN
+MzMwMjExMTUwNzMxWjBJMSIwIAYJKoZIhvcNAQkBFhNteUBlbWFpbGFkZHJlc3Mu
+Y29tMSMwIQYDVQQDDBp3d3cyLm9ubGluZS1ib3V0aXF1ZS5sb2NhbDCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2OHfrfxO6hhg4dGMi0Q4+iwuuNGv2J
+eh+t8g9A+dGB4zUkU0SEa2EFLjyAw2P7F1XAtiClHmS9nA0uTuj9nuw9Xk+wFUlm
+ahMkdkLBRNNsH3O1gTYUQSOK7xm8FOMBnOYQqJOHILXsXgM2z8QHpX//wl15aGXf
+9vPm4CQUqqsEGNWDfpOQ8jfhrA4T8W1WuAXzJAQmZfhW1qMWxoj1iTCk+Q6nH/8a
+MWLWPjTes0bTSRXRTpuQCyeHCaqS8oPPshcd52/FXChnYbNAG1s9Q+35MrgDztL6
+dPkNiA9UaUY7pumNip93ZnsYwDmOPnDBPqfvy9Dk1zQuaAsJ1FXwRNsCAwEAAaMp
+MCcwJQYDVR0RBB4wHIIad3d3Mi5vbmxpbmUtYm91dGlxdWUubG9jYWwwDQYJKoZI
+hvcNAQELBQADggEBADT2SchC4VhWqCYRsw/3nqrVh0JQmD+/x9JjiNsY6fLnG6uW
+8bs8/714qoghJff67H60B6NbrS3lpTZ3bmyotcGtwNNsY4QSFHRu/x4OyIrTfjKb
+VIdhRM2Atwc1s1YA6c+2JBquBDhniqABKG9u+j1aa2ElXSalCj+Kozm8ma0yduVw
+TX8zS6XZl57vSk/Qo/PZvbmbs8EMOwTUCLn6WQldAARCLughjd9LI9prNpBlYon6
+jmZ8oi59arK16cKe6i6tQ3QExT2kQsLlrK/jFw0xqrFnveKgUTeevViT4WZnqsCm
+awrPhrbDn0F7HmFSrgenrW8BIuJ2crBHy0230Lc=
+-----END CERTIFICATE-----
diff --git a/contrib/gitops-examples/v4.1/www2.online-boutique.local.key b/contrib/gitops-examples/v4.1/www2.online-boutique.local.key
new file mode 100644
index 0000000..b1e2db4
--- /dev/null
+++ b/contrib/gitops-examples/v4.1/www2.online-boutique.local.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdjh3638TuoYYO
+HRjItEOPosLrjRr9iXofrfIPQPnRgeM1JFNEhGthBS48gMNj+xdVwLYgpR5kvZwN
+Lk7o/Z7sPV5PsBVJZmoTJHZCwUTTbB9ztYE2FEEjiu8ZvBTjAZzmEKiThyC17F4D
+Ns/EB6V//8JdeWhl3/bz5uAkFKqrBBjVg36TkPI34awOE/FtVrgF8yQEJmX4Vtaj
+FsaI9YkwpPkOpx//GjFi1j403rNG00kV0U6bkAsnhwmqkvKDz7IXHedvxVwoZ2Gz
+QBtbPUPt+TK4A87S+nT5DYgPVGlGO6bpjYqfd2Z7GMA5jj5wwT6n78vQ5Nc0LmgL
+CdRV8ETbAgMBAAECggEBAJkAy010eWC+5YAbsgDxFHM+WNQo90m+RjtYegD0w1Ff
+HNSXSHXZ6PnwhPS9i7IhNgU/d8dloG67zuf/YflfJQBKhTYNCTZOZtTfalhBdlF3
+quTSpO/+3tk32lVwYRBADeWH/ZMcT4ezO12G3can7TBPJSA+ds1b9QSNHZ9tMku0
+2J2pzT8C8V0vHn49oKO9D4Q+QwRvisT6FKNaOW/xKZjtUIvyjVvjENnBW7MQ5y5z
+IHsUNkwGgZ2V2IbDouM4+A42J9iDsseM2euSsaDfum3vj9MPTqXE8zUUdX+/miMq
+RJTXLxDdkPaCbQftQ7y/cIAFLDZS8Fvvsa5pX69jl1ECgYEA8sQ6S7kcyYgBLX4T
+KVYPg1sFTgCofW5nTgCND4kiVvIDz/uk9vsfuMj7FMS3fcCfZorPdYEKa+Zwi4wZ
+NMCSArtezbwFBeaOiZTpjh2k8CBrtumdcU/EfzQOvrVcSzVc27ditwRAmONeBXVw
+GSvqSfjQKLDh/4Y191qOmgyIBDMCgYEA6aHjQr43XM811VQhTPBtLlBccD8hCS23
+NcujPMa85Vz+RES3OqHt2nH9NGuYrexGbrSt3ckjxnO+TdsS4f94bKhquXvv7AkX
+7qr8KZEwHncyjcL3uTChbjWr2xJR4lSnS5PAbnWfyR48HXkHO9IVPe/IcYnvuGDN
+AeMJkCgp1LkCgYEArvD/PXAxMX1js9/FeSU+Wp8t7G8G/BSiNxColkhxSYxveOJT
+l3OSAXw7i1TTEbjMZX2kUH3j/6t48ObNhzk6PuO9Rq62Q/FISBbaU4JDSJNka9Rf
+k7cy16Ow+HcDAmN6/g5iAZb74fD+4Rom5MzDsfiuMJR+179khlJortRW9AcCgYAD
+4AKD9eG3MVykOCwBOa+l6AFQf0uN+msigkkn1egGKd+xxC4B0/O8/s0DVJGIuPWG
+GosTtaVZQkwywGJ0yyb1Lmnuv6aAFLqH4+Ag1F6m8rUs8sHnGW5kBJHgJVKkXWEU
++NNlQaAv1seKeZpsHJTrnRGHCJGoTjq4QErFUFU5SQKBgEYMGdnPP4ZDAeezPCML
+42RVHOAG9IsMl4mV7+fwcEHcmuvb8nNgKNfayjG/NbiQWtT4v7iQH4GHYzUobaqr
+3xnQlec3jBrLEsWR8ItRWKxKO2GGLlbmQ99Kn3UGh+6FtXbxwB0hhkFfKXJzM10N
+Y9ruUdyYpjGO/j9ROjoBepUH
+-----END PRIVATE KEY-----
diff --git a/contrib/postman/NGINX Declarative API.postman_collection.json b/contrib/postman/NGINX Declarative API.postman_collection.json
index 89a1812..5d6ecc6 100644
--- a/contrib/postman/NGINX Declarative API.postman_collection.json
+++ b/contrib/postman/NGINX Declarative API.postman_collection.json
@@ -1,10 +1,10 @@
{
"info": {
- "_postman_id": "2e2d1a74-49b7-4f4f-8c46-decd87287609",
+ "_postman_id": "fc1e8cd7-6c83-4877-8095-50f28e9bb468",
"name": "NGINX Declarative API",
"description": "Declarative REST API and GitOps automation layer for NGINX Instance Manager",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
- "_exporter_id": "1667416"
+ "_exporter_id": "30973250"
},
"item": [
{
@@ -3806,6 +3806,1989 @@
]
}
]
+ },
+ {
+ "name": "v4.1",
+ "item": [
+ {
+ "name": "Configuration generation",
+ "item": [
+ {
+ "name": "Basic - LB",
+ "item": [
+ {
+ "name": "Basic - LB - plaintext output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"plaintext\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Basic - LB - json b64 encoded output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"json\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Basic - LB - ConfigMap output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"configmap\",\n \"configmap\": {\n \"name\": \"nginx.test\",\n \"filename\": \"testservice.conf\",\n \"namespace\": \"test-namespace\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Basic - LB - HTTP output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"http\",\n \"http\": {\n \"url\": \"http://192.168.2.19:8080/path/service\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample L4 service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"udp\"\n },\n \"upstream\": \"l4_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"A sample HTTP service\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Advanced",
+ "item": [
+ {
+ "name": "Advanced LB - plaintext output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"plaintext\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n }\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": {\n \"content\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Advanced LB - json b64 encoded output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"json\"\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Advanced LB - ConfigMap output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"configmap\",\n \"configmap\": {\n \"name\": \"nginx.test\",\n \"filename\": \"testservice.conf\",\n \"namespace\": \"test-namespace\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Advanced LB - HTTP output",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"http\",\n \"http\": {\n \"url\": \"http://192.168.1.19:8080/path/service\"\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"sample_layer4_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\",\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"upstream\": \"l4_upstream\",\n \"snippet\": \"IyBUaGlzIGlzIGEgbDQgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"l4_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n }\n ]\n }\n ]\n },\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"server_8080.nginx.lab\",\n \"server_8081.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/test\",\n \"urimatch\": \"exact\",\n \"upstream\": \"http://test_upstream\",\n \"health_check\": {\n \"enabled\": true,\n \"uri\": \"/healthcheck\",\n \"interval\": 5,\n \"fails\": 3,\n \"passes\": 2\n },\n \"rate_limit\": {\n \"profile\": \"test_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 10,\n \"delay\": 3\n },\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_illegal\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgbG9jYXRpb24gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"test_policy\",\n \"log\": {\n \"profile_name\": \"log_blocked\",\n \"enabled\": true,\n \"destination\": \"192.168.1.5:514\"\n }\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgc2VydmVyIHNuaXBwZXQgY29tbWVudAo=\"\n },\n {\n \"name\": \"another HTTP test application\",\n \"names\": [\n \"server_443\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"10.0.0.2:80\",\n \"backup\": true\n }\n ],\n \"sticky\": {\n \"cookie\": \"cookie_name\",\n \"expires\": \"1h\",\n \"domain\": \".testserver\",\n \"path\": \"/\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgdXBzdHJlYW0gc25pcHBldCBjb21tZW50Cg==\"\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"test_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n }\n ],\n \"maps\": [\n {\n \"match\": \"$host$request_uri\",\n \"variable\": \"$backend\",\n \"entries\": [\n {\n \"key\": \"www.test.lab/app1/\",\n \"keymatch\": \"iregex\",\n \"value\": \"upstream_1\"\n },\n {\n \"key\": \"(.*).test.lab/app2/\",\n \"keymatch\": \"regex\",\n \"value\": \"upstream_2\"\n }\n ]\n }\n ],\n \"nginx_plus_api\": {\n \"write\": true,\n \"listen\": \"127.0.0.1:8080\",\n \"allow_acl\": \"0.0.0.0/0\"\n },\n \"snippet\": \"IyBUaGlzIGlzIGEgSFRUUCBzbmlwcGV0IGNvbW1lbnQK\"\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Declarative automation examples",
+ "item": [
+ {
+ "name": "API Gateway",
+ "item": [
+ {
+ "name": "Ergast API",
+ "item": [
+ {
+ "name": "Ergast API Gateway and DevPortal",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Ergast API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"8.8.8.8\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/ergast\",\n \"urimatch\": \"prefix\",\n \"snippet\": {\n \"content\": \"IyBUZXN0IFNOSVBQRVQK\"\n },\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"https://raw.githubusercontent.com/adampax/ergast-f1-openapi-doc/e558eea18e176e4f78a8765ac7eccc804b5157ff/ergast-openapi-doc.yaml\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/ergast-devportal.html\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/ergast-access_log\",\n \"error\": \"/var/log/nginx/ergast-error_log\"\n }\n }\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Petstore API",
+ "item": [
+ {
+ "name": "Petstore API Gateway RateLimit",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Petstore API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"8.8.8.8\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/petstore\",\n \"urimatch\": \"prefix\",\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"http://petstore.swagger.io/v2/swagger.json\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true,\n \"server_url\": \"https://petstore.swagger.io/v2\"\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/petstore-devportal.html\"\n },\n \"rate_limit\": [\n {\n \"profile\": \"petstore_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0,\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n }\n ]\n },\n \"log\": {\n \"access\": \"/var/log/nginx/petstore-access_log\",\n \"error\": \"/var/log/nginx/petstore-error_log\"\n }\n }\n ]\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"petstore_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"2r/s\"\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Petstore API Gateway RateLimit + JWT Authentication",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Petstore API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"8.8.8.8\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/petstore\",\n \"urimatch\": \"prefix\",\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"http://petstore.swagger.io/v2/swagger.json\",\n \"authentication\": [\n {\n \"profile\": \"Source of truth authentication profile using HTTP header token authentication\"\n }\n ]\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true,\n \"server_url\": \"https://petstore.swagger.io/v2\"\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/petstore-devportal.html\"\n },\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"Petstore JWT Authentication\"\n }\n ],\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n },\n \"rate_limit\": [\n {\n \"profile\": \"petstore_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0,\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n }\n ]\n },\n \"log\": {\n \"access\": \"/var/log/nginx/petstore-access_log\",\n \"error\": \"/var/log/nginx/petstore-error_log\"\n }\n }\n ]\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"petstore_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"2r/s\"\n }\n ],\n \"authentication\": {\n \"client\": [\n {\n \"name\": \"Petstore JWT Authentication\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Petstore Authentication\",\n \"key\": \"{\\\"keys\\\": [{\\\"k\\\":\\\"ZmFudGFzdGljand0\\\",\\\"kty\\\":\\\"oct\\\",\\\"kid\\\":\\\"0001\\\"}]}\",\n \"cachetime\": 5\n }\n }\n ],\n \"server\": [\n {\n \"name\": \"Source of truth authentication profile using bearer token authentication\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"bearer\"\n }\n },\n {\n \"name\": \"Source of truth authentication profile using HTTP header token authentication\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"header\",\n \"location\": \"X-AUTH-TOKEN\"\n }\n }\n ]\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Petstore & Ergast API Gateway all in one",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Petstore and Ergast API\",\n \"names\": [\n \"apigw.nginx.lab\"\n ],\n \"resolver\": \"192.168.2.13\",\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/apigw.nginx.lab-access_log\",\n \"error\": \"/var/log/nginx/apigw.nginx.lab-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/petstore\",\n \"urimatch\": \"prefix\",\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"http://petstore.swagger.io/v2/swagger.json\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true,\n \"server_url\": \"https://petstore.swagger.io/v2\"\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/petstore-devportal.html\"\n },\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"Petstore JWT Authentication\"\n }\n ],\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n },\n \"rate_limit\": [\n {\n \"profile\": \"petstore_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0,\n \"enforceOnPaths\": true,\n \"paths\": [\n \"/user/login\",\n \"/user/logout\"\n ]\n }\n ],\n \"log\": {\n \"access\": \"/var/log/nginx/petstore-access_log\",\n \"error\": \"/var/log/nginx/petstore-error_log\"\n }\n }\n },\n {\n \"uri\": \"/ergast\",\n \"urimatch\": \"prefix\",\n \"snippet\": {\n \"content\": \"IyBUZXN0IFNOSVBQRVQK\"\n },\n \"apigateway\": {\n \"openapi_schema\": {\n \"content\": \"https://raw.githubusercontent.com/adampax/ergast-f1-openapi-doc/e558eea18e176e4f78a8765ac7eccc804b5157ff/ergast-openapi-doc.yaml\"\n },\n \"api_gateway\": {\n \"enabled\": true,\n \"strip_uri\": true\n },\n \"developer_portal\": {\n \"enabled\": true,\n \"uri\": \"/ergast-devportal.html\"\n },\n \"rate_limit\": [\n {\n \"profile\": \"ergast_ratelimit\",\n \"httpcode\": 429,\n \"burst\": 0,\n \"delay\": 0\n }\n ],\n \"log\": {\n \"access\": \"/var/log/nginx/ergast-access_log\",\n \"error\": \"/var/log/nginx/ergast-error_log\"\n }\n }\n }\n ]\n }\n ],\n \"rate_limit\": [\n {\n \"name\": \"ergast_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"1r/s\"\n },\n {\n \"name\": \"petstore_ratelimit\",\n \"key\": \"$binary_remote_addr\",\n \"size\": \"10m\",\n \"rate\": \"2r/s\"\n }\n ],\n \"authentication\": {\n \"client\": [\n {\n \"name\": \"Petstore JWT Authentication\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Petstore Authentication\",\n \"key\": \"{\\\"keys\\\": [{\\\"k\\\":\\\"ZmFudGFzdGljand0\\\",\\\"kty\\\":\\\"oct\\\",\\\"kid\\\":\\\"0001\\\"}]}\",\n \"cachetime\": 5\n }\n }\n ]\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "CRUD automation",
+ "item": [
+ {
+ "name": "Create initial NGINX configuration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"patched_server.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_log\",\n \"error\": \"/var/log/nginx/error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update HTTP upstream",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n },\n {\n \"server\": \"10.0.0.3:80\"\n },\n {\n \"server\": \"10.0.0.4:80\"\n },\n {\n \"server\": \"10.0.0.5:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Modify and add HTTP upstream",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:80\"\n },\n {\n \"server\": \"10.0.0.2:80\"\n }\n ]\n },\n {\n \"name\": \"test_upstream_added\",\n \"origin\": [\n {\n \"server\": \"192.168.1.1:80\"\n },\n {\n \"server\": \"192.168.1.2:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Modify and add HTTP server and upstream",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\",\n \"names\": [\n \"patched_server_v2.nginx.lab\"\n ],\n \"listen\": {\n \"address\": \"127.0.0.1:8080\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/access_v2_log\",\n \"error\": \"/var/log/nginx/error_v2_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream_added\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream_added\",\n \"origin\": [\n {\n \"server\": \"192.168.1.100:80\",\n \"weight\": 5,\n \"max_fails\": 2,\n \"fail_timeout\": \"30s\",\n \"max_conns\": 3,\n \"slow_start\": \"30s\"\n },\n {\n \"server\": \"192.168.1.101:80\"\n },\n {\n \"server\": \"192.168.1.102:80\"\n },\n {\n \"server\": \"192.168.1.103:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Remove HTTP server and upstream",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"HTTP test application\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream_added\"\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Modify Stream server and upstream",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_10053_service\",\n \"listen\": {\n \"address\": \"10053\",\n \"protocol\": \"tcp\"\n },\n \"upstream\": \"TCP_10053_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_10053_upstream\",\n \"origin\": [\n {\n \"server\": \"10.0.0.1:53\"\n },\n {\n \"server\": \"10.0.0.2:53\"\n },\n {\n \"server\": \"10.0.0.3:53\"\n },\n {\n \"server\": \"10.0.0.4:53\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Add stream server and upstream",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_15432_service\",\n \"listen\": {\n \"address\": \"15432\",\n \"protocol\": \"tcp\"\n },\n \"upstream\": \"TCP_15432_upstream\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_15432_upstream\",\n \"origin\": [\n {\n \"server\": \"172.16.10.1:5432\"\n },\n {\n \"server\": \"172.16.10.1:5432\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Remove stream server and upstream #1",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_10053_service\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_10053_upstream\"\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Remove stream server and upstream #2",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"layer4\": {\n \"servers\": [\n {\n \"name\": \"TCP_15432_service\"\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"TCP_15432_upstream\"\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration status",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}",
+ "status"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "GitOps autosync",
+ "item": [
+ {
+ "name": "NGINX Plus and GitOps",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 5,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.key\"\n }\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.2.200:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "NGINX App Protect WAF and GitOps",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 5,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.key\"\n }\n }\n ],\n \"policies\": [\n {\n \"type\": \"app_protect\",\n \"name\": \"production-policy\",\n \"active_tag\": \"gitops\",\n \"versions\": [\n {\n \"tag\": \"gitops\",\n \"displayName\": \"Production Policy - GitOps\",\n \"description\": \"This is a production-ready policy - Managed by GitOps\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/nap-policy-gitops.json\"\n }\n }\n ]\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"production-policy\",\n \"log\": {\n \"profile_name\": \"secops_dashboard\",\n \"enabled\": true,\n \"destination\": \"127.0.0.1:514\"\n }\n }\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.1.200:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration status",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}",
+ "status"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Housekeeping - common endpoints",
+ "item": [
+ {
+ "name": "Clean NGINX configuration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration status",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}",
+ "status"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "JWT Client Authentication",
+ "item": [
+ {
+ "name": "JWT Client Authentication - local JWT key",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": []\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online-boutique.lan-access_log\",\n \"error\": \"/var/log/nginx/online-boutique.lan-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://onlineboutique_upstream\",\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"online_boutique_jwt_authentication_local\"\n }\n ]\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"onlineboutique_upstream\",\n \"origin\": [\n {\n \"server\": \"192.168.2.200:80\"\n }\n ]\n }\n ],\n \"authentication\": {\n \"client\": [\n {\n \"name\": \"online_boutique_jwt_authentication_local\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Online Boutique Authentication\",\n \"key\": \"{\\\"keys\\\": [{\\\"k\\\":\\\"ZmFudGFzdGljand0\\\",\\\"kty\\\":\\\"oct\\\",\\\"kid\\\":\\\"0001\\\"}]}\",\n \"cachetime\": 5\n }\n },\n {\n \"name\": \"online_boutique_jwt_authentication_key_from_url\",\n \"type\": \"jwt\",\n \"jwt\": {\n \"realm\": \"Online Boutique Authentication GitOps\",\n \"key\": \"http://192.168.2.19/jwks.json\",\n \"cachetime\": 5\n }\n }\n ]\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Change to use JWT key stored on external URL",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": []\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online-boutique.lan-access_log\",\n \"error\": \"/var/log/nginx/online-boutique.lan-error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://onlineboutique_upstream\",\n \"authentication\": {\n \"client\": [\n {\n \"profile\": \"online_boutique_jwt_authentication_key_from_url\"\n }\n ]\n }\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "mTLS Client Authentication",
+ "item": [
+ {
+ "name": "HTTPS server with mTLS, OCSP, SSL Stapling",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"server_cert\",\n \"contents\": {\n \"content\": \"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdSRENDQkN5Z0F3SUJBZ0lVTTNJQVZIRmxhSTVsY1d0TjZxOUVhcnlka0w4d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNTVlF4RFRBTEJnTlZCQWdNQkVGemRHa3hFVEFQQmdOVkJBb01DRlJsYzNRZwpUR0ZpTVJnd0ZnWURWUVFEREE5MmJTMWliR0Z1YXk1bVppNXNZVzR3SGhjTk1qTXdOakE0TVRBd01qTTVXaGNOCk1qUXdOakEzTVRBd01qTTVXakJKTVFzd0NRWURWUVFHRXdKSlZERU5NQXNHQTFVRUNBd0VRWE4wYVRFUk1BOEcKQTFVRUNnd0lWR1Z6ZENCTVlXSXhHREFXQmdOVkJBTU1EM1p0TFdKc1lXNXJMbVptTG14aGJqQ0NBaUl3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFMRFQ2bnZleVZlNi9VZlk2aUtHVC9oV1A0cktDSGR0ClloZWU3RGVZR29QWGhGVjB6a3grVWExanBEZ21WUE1kVEJBdnoxODg5NzlEcHBqdmNYeFhsRmpnaUhjWDhpWVgKSXovSUVMc3dKRUNITWNsNkxmelA5eDVUY1gxTEdFblFOTWhHRzA2MjlxU2NCQmQyUUNiWlY0UWE1TkxlQnQ4cQpHQ2lXY3JiQnR3YlpiSGo1dk9aenJrdHBtRFBGS1V4bXR5b2dBQnNaTllnL0F3Y1l2RXdBOEQ0QTN0VEgxcGhvCkdYY3ZvZWpJelhRMUdmYys5azR3OFhHYWFQOGd2bTdOMXN2MnU2Yld4SHRGZHpWQk9udzJyaHUvWGYyY0N0dW4KUnIxSENKQXRRSDlkbDhzZks1czBSRlVuTlVYbFBiNTFBTjBjVFVGbEYrZlVUVmVON3dNMTdmeVZVY3IydTltSwo0UGdoWjkvMml0ZUpZV3hjK3k4V2NEQzBUV3hwZ2paVEw5Tk1GK2t6SXV2TjJOWFFybjcvSU5UQTMvNFlmWGRPCloxelpTdTlkclRMcG5DZHRpOWxuRHBKODd3bW41cVZSTlZiTlZRbldEeW5yZnoyTU1DY21jLzcvdkJFN2dDemQKNFJLWHJLdHloenlQSitycmh3NmpxYVA4QytaZGRvKzkvak9QVDFTSnUxZ21VbzFuZ2hBMWh2N0M5RUYrM2xQVApYSk5WV3dtYkdWK0p4cUdKSjJSa2toMlIrZTVIREdRY2hGWjJIcXBGTGVQN0trTHJBR2RkZFZQWEZhQ0RiU0R6ClJQd0I5WFlhakg5Zm5QWEtFT3ZpVEJhQVNjWUZwTXB5cm02UkxHUGRSVnE2RUNYVlB4MDdHdGFCaEVvVWIwK2YKVkZnNExtQkx4MldQQWdNQkFBR2pnZ0VpTUlJQkhqQUpCZ05WSFJNRUFqQUFNQkVHQ1dDR1NBR0crRUlCQVFRRQpBd0lHUURBekJnbGdoa2dCaHZoQ0FRMEVKaFlrVDNCbGJsTlRUQ0JIWlc1bGNtRjBaV1FnVTJWeWRtVnlJRU5sCmNuUnBabWxqWVhSbE1CMEdBMVVkRGdRV0JCVHZFZWJGK1JDV0JhcGVPWUdpQ0YyVHZxbExYekNCaEFZRFZSMGoKQkgwd2U0QVVFdW9Db3kvcmhMQmxzcm5KdXE2QzFJczQxbFNoVGFSTE1Fa3hDekFKQmdOVkJBWVRBa2xVTVEwdwpDd1lEVlFRSURBUkJjM1JwTVJFd0R3WURWUVFLREFoVVpYTjBJRXhoWWpFWU1CWUdBMVVFQXd3UGRtMHRZbXhoCmJtc3VabVl1YkdGdWdoUld4QjhCa3lmK1RkQXc2Q3dPZE1aT0k0NlZ2REFPQmdOVkhROEJBZjhFQkFNQ0JhQXcKRXdZRFZSMGxCQXd3Q2dZSUt3WUJCUVVIQXdFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dJQkFHUDR6ZkdseTI1RwpneTBSeC9SSTNpNzJDVlIrSXY3SW5WTUVGWDZqRHRNV3hSblFtRGZsMWtTOVF1Y3hNb0tnOE9URStMcnlzdGJsClF4WGZiakZQekNoNHB1UGtGTmNBeG1mVmR4b20xR1lodWpoYTBQOUswUURZSDZycGlUaFdSQ2greUovQm1qZ2wKTlJabks4WGRqME85Ui9XKzJrTFRac2VFbS9hZHFVQ3dkYzNBWWlNWGh4QXkvQlh3bFRQeDMyMHZCcXYxZGFyVgp5ZlVoRlM1Rkg3enV2bGtGQ1p6M3lpOGYvYXMwbkRTUkFrY3dPRFQvN1diQlN4QTk3ZzJmRk1EMEI3WlUvbndGCmU4VnRzNDl3YmZ6QWJRMk40RUc2OEVhODE1VlFRM2N6YWthdjBCdkxHL2UwT0habGxYcUVhV1ZlWFJtSWFFOHcKWko5OEhUaDJMbUlFV2Jpdm94Kyt2UXd3bVhKTm1DRFVXNnVmcHdBOVdKQ0VhYmhxeXdGVzh1dFVENzRTVXE3SApEUDhNamtJZ0o3ekl2Tkd1RkFsSzd6c2xpV2pzeUN1OGVNamhvN2pVRFhGR1R0R0ZMUGtVa08vSysrSGVVRFg0Cm1OWDJ2aHI3NGRqRkNBTTEvOTYxWnB5NUFYUzZkd2g3MFlJL2dMdldSL0J1ejBnNEp6YUI2UFo4M1ErYm9QVHYKM1ZIS2xOWjlKQlhRTmtSc3N6U0dYWG5MYmtOTmNwVFg2cnAyZ1pUSS9NNDhGTnBxanAxOXRpQVg3bWN0cTl2SgpNejhvemhEcHZmSTlnMjFsNFZlRGdpbWEwTDVBc1pQbFdIQlZjcy9yL3dMU2YzWFVYZEs0UHpCQUdIRFBidXYrCnpKOVNqS0NFVll2bHRhMHlUUVBCSFJPa2Y2MG1sVmh6Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"server_key\",\n \"contents\": {\n \"content\": \"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBc05QcWU5N0pWN3I5UjlqcUlvWlArRlkvaXNvSWQyMWlGNTdzTjVnYWc5ZUVWWFRPClRINVJyV09rT0NaVTh4MU1FQy9QWHp6M3YwT21tTzl4ZkZlVVdPQ0lkeGZ5SmhjalA4Z1F1ekFrUUljeHlYb3QKL00vM0hsTnhmVXNZU2RBMHlFWWJUcmIycEp3RUYzWkFKdGxYaEJyazB0NEczeW9ZS0paeXRzRzNCdGxzZVBtOAo1bk91UzJtWU04VXBUR2EzS2lBQUd4azFpRDhEQnhpOFRBRHdQZ0RlMU1mV21HZ1pkeStoNk1qTmREVVo5ejcyClRqRHhjWnBvL3lDK2JzM1d5L2E3cHRiRWUwVjNOVUU2ZkRhdUc3OWQvWndLMjZkR3ZVY0lrQzFBZjEyWHl4OHIKbXpSRVZTYzFSZVU5dm5VQTNSeE5RV1VYNTlSTlY0M3ZBelh0L0pWUnl2YTcyWXJnK0NGbjMvYUsxNGxoYkZ6NwpMeFp3TUxSTmJHbUNObE12MDB3WDZUTWk2ODNZMWRDdWZ2OGcxTURmL2hoOWQwNW5YTmxLNzEydE11bWNKMjJMCjJXY09rbnp2Q2FmbXBWRTFWczFWQ2RZUEtldC9QWXd3Snlaei92KzhFVHVBTE4zaEVwZXNxM0tIUEk4bjZ1dUgKRHFPcG8vd0w1bDEyajczK000OVBWSW03V0NaU2pXZUNFRFdHL3NMMFFYN2VVOU5jazFWYkNac1pYNG5Hb1lrbgpaR1NTSFpINTdrY01aQnlFVm5ZZXFrVXQ0L3NxUXVzQVoxMTFVOWNWb0lOdElQTkUvQUgxZGhxTWYxK2M5Y29RCjYrSk1Gb0JKeGdXa3luS3VicEVzWTkxRldyb1FKZFUvSFRzYTFvR0VTaFJ2VDU5VVdEZ3VZRXZIWlk4Q0F3RUEKQVFLQ0FnQVBUR1pQRFRsU004VlIvL3hSdkZrUzNUTm1LSkNPOUpHMkJYUGVZM1IzejUrTlhTdTBCb0craEk1aQpwVDVZUWtLZ2ErSi9GT0ZDVlBJRzdVQmVSNTE0Q3dVRGVMamtmci8zOXJFcjRNQmlMTkFyNUR3eVVUUEtGZUlOCnV2K0E4MWg5czBNTmpsck1ad3NibElsOFV2VjFZblpGb0J2c0Z0SThRTGZ3QTlaMzZ6dXRRNzRLR2h3TVBqaUMKMGgzK2xDeG9vcGdmd0JDWGx3d0dBeWZYVTRWMWQ5SFBpdktRQVFHakJDWDM0OWVTcEQxNDNLT21wQ2xmY01LQQp3QzU1bTZsbndCTUFIamlsaVo4RXBuNE8zUlEzSmxsVlpiaXl4RWdrZkE3TG1uNm9Ca3Jwc2VxdDVObThuRVhKCnBFbXhQcUl5Znc1WUNBMEhhNkM5WUhRN1RPRW9BbHBmWld4azAxSnpoVi9aK3FmVHM1YlMwQWNaTzFOVDRaeDgKWlF2eHQ0TDJINVcrK2R6RjhReTlidzQ2M3lKb1dydWxtNy9uQ3YvL1FpNGl0eHRnYyt0N2lwVXZzaUdTVktVWQpPelhCSXNWTUlnd0F6eUtTSEhPL21rMkEwVkgxaHB3emY2L0RzR2wxSjM4TU9pVGo4dEx1RWt3cFY4WGh5MnZwCkd0cXpsT21DS1hodlVDam9iZWlYSWJwSlIzeEM1NmliRjVadk0vQUdONzI5K0xKRFNwbHJtWVJRVHh1UTJWSE8KQWFXQ01SQWFBdUtCVnBxYTRjd25WRy9POEpkN2ZPSi9tMFlIN3FpRlJHREdvdVNOdHZJUUVtaXVkK3dRWjJ6dwpUcmFNVWk0SENtNEFPa0ZNVXBsRmt1ajA2ZHRqM2RIWUtPQkdMK25vaUp4WmJxb3kwUUtDQVFFQTFiZUl6WHh6CnRFRlp2OGRlOXljOWdCUUtNNUNIbHp6NUNMZXVkTitvemxxeDNCMW1PRStxbFkyaEd3RklIWVBJajFLYS83RlkKbExmNFpiUEJRMFhiNUo5VzQzSGIyTnEydXdRQ3ZiSXhVMW9zaGJVWlhZc2FUaE15azc2VzQ5YjU3UC9HdFE3NwpTbkVZTXNrTzRUQndyS3lBdVhDVHRtTk1Qa2J1NFBxT05PeVFQY3o3Yi92VEU1eERjMENMVS9oUXM3NWFHeCs1Citld2VjeEZNa0JKTVo2c2N5TzcySEdSNHZwTHduRXUvcU5uN2JmUElSaUx1T3BwTTdHNlUwQlBPL2todHJ5ZmQKV3U3MHJYZGJSdGRJUHlsQWxSOG9zczJqWWsrRHNPUnNESm9pbkk5WU1Va3dmdHdCNTRQbytGRGtGOHBzV202RQpSaklpenFBK0piWDlTd0tDQVFFQTA4Ly9oM0NabDg2M2xUZHNrU1JKRUZKc0RtdkZkUStzMWtlNUFwMjdnWTBXCmZJbEFGZFlRR3RORUVlTk9xS3EwdTFtS0lqWHFacWNTdU9DNzZIYTE5Tk9waHVoK1dwV0t2Ni9BTWtQSjE5SUIKQ3RqS0lkc2s0U2M3WG02MnNOV1pnQm5XT1Z3QVdzU0VzTHRac1NvWUJUVTJJS1pBOVJOWHhkSEQreGZ2SWJkNApZYngzTzk4WklNQzNlVFFiOW9jVHZab0RNWGdLaHRtTy9iMnlSeEVDSGpGRmxzYlhhc1RPeG5XOWZSVXJtdGVqCk9pdVlXaEZOM2R6dmpuVEdLY0xieWY0MWpHaUVUeFViUHVpei9ZMmk5NldCNVN6MW9zaGorRU1OaFhtRzZSYXUKQUIvelhwNldtSUJ2bDNpU0lzOGJRNkh3Qm1DTjc1R2VVVG1GUUlyaVRRS0NBUUVBbTkzWVN5MXA0VndNRGI5bApObElMRzM4Q0ZhdGlDRjR5cmpYd2FWSzVkWTVWeTFneHRmMzhSa2hkNkNrZUpGQjVsSFhGajVnVEo1dW84TnVSCnB2T3JOT2swNEhxb3dWWjZFSmtUT3JCY0l4TlFCMUFXS05BTHBrZUFDcHJreDFTQlFHVW0wZVFVUjYyRjNYd2YKZXdMdUdqRlJURzJiZlZpY1FZdFFLd3J4YmczZUFRU2ZtSU9MNVBDQmpPdlU4YS9YZzgvZlBZcjlBeFkrK3VMeAorTjB2bGlnSXZVN3lkYkNkRXpodGZVQU5qeU16cVhRemExdU1iWGNkaFEzOVFHaEIvZGhyRG1TL250Tko1YjEzCjk0bUpLbTkycDR0ckRrVEYxU3h5dWk5TjBqOFQ0U1QyU0RPOXg3ZkROOHRQdk5LYUYvUE01SU5YdXk1VGptajIKQ21EWlV3S0NBUUFOUVJYSFh1ZHRsWFR0ZEhOcHZiQ0l3ZStiRTJsZXd1VlkzMUlYZE5GWDhRRTROOHAzMDFaYwpwMTI2Rk5SR1A3QmhqTi9VOWpTOXliU2xOd0xyTUFxQTBJSHFQRUF6NE9tMnh3T3E0WTBPNFVoSmFubHpsdWYrCjR0cVhOU3hmY201UmtzeFIrSXpaSVRVQWJpalZxa0dvaWNUaVZDVDZjUVJzRDQxSStCMXhxYTV4eHo1YTA4SVoKeDVWemt5d3d5QkVYS3owSjZtNFdOQ1Q3Z2RSWEdCeGUwVXgrZStEZEFJWEQ2M2c1RElzVy9HbHRhVzcySytFSQpnaHZIZVUweExjMWRIWGd5V2hQMWN1ZXFqeHM4UVpHeUYzeENZQWJhOGRrM250S0l5S3NGaVBMSWRUZGdjMklQCkZ2SmtzeG5KN2RYUjdKODlkdXRLMDN6cHJrVEZYaXQ5QW9JQkFDcjhkb2ZCcFlFL1JuTlFwbVNET29DRm1sdTkKQlozN3h5K0puZ2FrQ2RSdHFyR1lDdkZMSnI2QnpGdXE0SHpsM0piTkRCM1BkYSs4Z2VNd2cxU1htTEhrRVFrTQpXV2ptNHpmU3hiTUtKamx3REdoeUlwSU9nQ2FQL1hyT2hxTGl4bnJ6UHFHZmM4R0FZTDE2Rm1PeGVqbVk5aERtCmNibkFqZlNwUjF1WEt2S2d6d1NLQ0VWdzc0VjJSRmRqQXBLVDl3bkpOQTZiWHQ5SXFkaS96d3BYbDQ0OVczdVMKNjRjVVpaK3luYnQ5QUlxbFNjMDdNRHl1TUtueExMbDFLeEJYenNxZlVsYWtlRGVoVmdGS05OOTNXQWJJc09ieAp1d1hTd0hXa1B6RGFHeE9wdzlSMHo2S2t2N25YZnBIYW1RWENBZEdsRjkyc1QwYW80Y3FuejFJSmJ2bz0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K\"\n }\n },\n {\n \"type\": \"certificate\",\n \"name\": \"cacert\",\n \"contents\": {\n \"content\": \"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZjekNDQTF1Z0F3SUJBZ0lVVnNRZkFaTW4vazNRTU9nc0RuVEdUaU9PbGJ3d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNTVlF4RFRBTEJnTlZCQWdNQkVGemRHa3hFVEFQQmdOVkJBb01DRlJsYzNRZwpUR0ZpTVJnd0ZnWURWUVFEREE5MmJTMWliR0Z1YXk1bVppNXNZVzR3SGhjTk1qTXdOakE0TURrMU5EQTRXaGNOCk1qUXdOakEzTURrMU5EQTRXakJKTVFzd0NRWURWUVFHRXdKSlZERU5NQXNHQTFVRUNBd0VRWE4wYVRFUk1BOEcKQTFVRUNnd0lWR1Z6ZENCTVlXSXhHREFXQmdOVkJBTU1EM1p0TFdKc1lXNXJMbVptTG14aGJqQ0NBaUl3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFLMlQwWXpkcjB0dWQzaVJRNGNzaGNhRVJTRzVjTDE2CkhRblhoYWw4emlUL1VRQUNIUGdzZDYwcWlEaldvQTJXb0lGWFFpUHkzOG1vZGtWRlR4Qmt5U2VldndOOFJiLzEKOFhaMS8yS1RnVmRDcHkvNm11WE15bXZYODJad05CVkV3QnoxUk5kbklUSk44cVh3a0d4bHozbDBib1loRkFyUQpNdmkxcW1RaHpDa2Zpb041MVkrYlBXOXpTQlFQdXNrcXJYYzRqTTJ0VENNQ2pTcFlvd1hXM1ppRmc5WEJ1Z09aCjFmdWd1Zmw4K1FJYzNZSEFoL1Z1NloraXFEOGxQeGRKODlBeDZaazVtOGdkVG9JdUhBbUNWaHFpUXBGRjkzSTgKbkYrSnRuYnBaNTRJUTZBbWYrYiswakMxdmY4Kzg0WUppaEVzWExyaGMxZTRTZ2dwdzEvcWpDb21QblhGVjEzUwpsUG5kVlhVR0taa1ZKdXdZTjJyZElmd3YrdCs5MGhwUVBmNmFBTjRCamRxOXdkdkQzSXVnS2JYZG5CQ0FUTEY4ClYyRTFTSE9VZGdRY3duK1d1WDVVOGdPa3B2b2VFN0g1REJ6Rks1WTZ2SHZlaTRlNkp3RTRDK3FJL1BmbTgreTEKNEpsOFBSOW5JQmdGQ3hrZWpwa2tRQ0I5U0dvMVZidzZhWmdZd0VQNHh6YXFYYXV3L3F4c0oxNUkrRTBndEs1OApuWUtkM0hqelk5Slh6V0NVNTdXbmc2SzNvTTIzNXpyRzJnNm1FaHQ4SStDckVMUFNuZURjZU8zVlJkc2dlblBCCis4U1JxVU8vWG9LWHNEU3I5amoxdWluVzYwTG5MZ0Zmc3JQeGlQVlZlMFh1TFZESlhCSlNoRDZDeGRyMnBSOGQKS25SRDZrTFpZZEtMQWdNQkFBR2pVekJSTUIwR0ExVWREZ1FXQkJRUzZnS2pMK3VFc0dXeXVjbTZyb0xVaXpqVwpWREFmQmdOVkhTTUVHREFXZ0JRUzZnS2pMK3VFc0dXeXVjbTZyb0xVaXpqV1ZEQVBCZ05WSFJNQkFmOEVCVEFECkFRSC9NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUNBUUJad3B4Z2Z4N2thZFhvRHNyT1hUVXJ6dEFPMkFQRVJNaTAKaTkyNk9DTGFPbVVYZW1uKytXSUU1K2tUSE0wcS8vbUZCTURzSmdZSFVLUlNvRGNsNmh4TnVFNUNzS2trRVFTSgpMTHZrWlB0S2J5NGlxMitLZ1JtdVZxbXJNVTBYQzZMZDl3WmttL2huUjNtT3V6bko4MGZmV1JDQ0xGWDEwY2EzCnc5TGM1d1JLTFBZZXQvcEs5SitOYWN3TFJRYTczVFovMUpQNW9BU3czVjNoYkxlLy9UeWpnOURqUlZGY3FYWnEKWWs2Mm5qSkhZVzh3WmlhZzc0QXU4dHE5OG5KandBV1ROMFV5L2w1Q2VpWnV5bzZlU0RHVDNJNm1BdGU1VXBvWAppNXBkYlZ6VDdOZC9IOEwwZHZNdVZ2N0FmakZlcU91cUZNNkkzTnlvbStLWENxNmJQdGxBWEkzeVFZc0t4ZlRkCkw3SnRaTmx6MGJ6eHJhcHI4RmpYcjhML1ZkeHQza00xMnJwb2kzL3hsckR6Q2Q2b2YrQ1MxelBocUdpOUhvcUoKZEU5VGhYMklTdkd2akVSYzVVNFRsNjJBNHNyeGJQbUt0eWx3dGNGVEJacUJiRGY3ZjBBc2cveWhndXdTcktsQQpBNkRWVXVCRFErdGpwZ0N0b0ZlOEhLVDJ6UFVlaEQ2ZjVNQkhmU2ZUZ1crTlhFSXNvVDNsampjY1hsYXhPcFJWCkNQNWxCczNmekxyYnBxbUlLaWZhdWlTNWM4TzlSUjhjQTVzeWlBOTBmbmJIdDlmdGxpRG9jcFRzNUtrbjk2NkIKZUxMM1dXVldCYUtvanJzY1RkVXJoalNnVVBmam5FTXpnVzR2eEc3d3BVNHR2ME4yaEtHUWc0bVhhcDV0SU5Pcwp4WktnZXRHUldnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=\"\n }\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"vm-blank.ff.lan\"\n ],\n \"resolver\": \"192.168.1.13\",\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"server_cert\",\n \"key\": \"server_key\",\n \"trusted_ca_certificates\": \"cacert\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ],\n \"mtls\": {\n \"enabled\": \"on\",\n \"client_certificates\": \"cacert\"\n },\n \"ocsp\": {\n \"enabled\": \"on\",\n \"responder\": \"http://ocsp.k8s.ie.ff.lan\"\n },\n \"stapling\": {\n \"enabled\": true,\n \"verify\": true,\n \"responder\": \"http://ocsp.k8s.ie.ff.lan\"\n }\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/vm-blank.ff.lan_access_log\",\n \"error\": \"/var/log/nginx/vm-blank.ff.lan_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://origin_server\"\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"origin_server\",\n \"origin\": [\n {\n \"server\": \"192.168.1.200:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "NGINX App Protect WAF",
+ "item": [
+ {
+ "name": "Create initial NGINX configuration with NGINX App Protect WAF",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www.online-boutique.local.key\"\n }\n }\n ],\n \"policies\": [\n {\n \"type\": \"app_protect\",\n \"name\": \"production-policy\",\n \"active_tag\": \"xss-blocked\",\n \"versions\": [\n {\n \"tag\": \"xss-blocked\",\n \"displayName\": \"Production Policy - XSS blocked\",\n \"description\": \"This is a production-ready policy - XSS blocked\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-blocked.json\"\n }\n },\n {\n \"tag\": \"xss-allowed\",\n \"displayName\": \"Production Policy - XSS allowed\",\n \"description\": \"This is a production-ready policy - XSS allowed\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-allowed.json\"\n }\n }\n ]\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ],\n \"app_protect\": {\n \"enabled\": true,\n \"policy\": \"production-policy\",\n \"log\": {\n \"profile_name\": \"secops_dashboard\",\n \"enabled\": true,\n \"destination\": \"127.0.0.1:514\"\n }\n }\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.1.200:80\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Change active NGINX App Protect policy",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"policies\": [\n {\n \"type\": \"app_protect\",\n \"name\": \"production-policy\",\n \"active_tag\": \"xss-allowed\",\n \"versions\": [\n {\n \"tag\": \"xss-blocked\",\n \"displayName\": \"Production Policy - XSS blocked\",\n \"description\": \"Production-ready policy - XSS blocked\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-blocked.json\"\n }\n },\n {\n \"tag\": \"xss-allowed\",\n \"displayName\": \"Production Policy - XSS allowed\",\n \"description\": \"Production-ready policy - XSS allowed\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/nap-policy-xss-allowed.json\"\n }\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update TLS certificates",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www2.online-boutique.local.crt\"\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v4.1/www2.online-boutique.local.key\"\n }\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Disable NGINX App Protect WAF",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_app_protect_module\",\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\"\n }\n ]\n }\n ]\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration status",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}/status",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}",
+ "status"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "method": "GET",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete declaration",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config/{{configUid}}",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config",
+ "{{configUid}}"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Server-side and source of truth authentication",
+ "item": [
+ {
+ "name": "Server-side and source of truth authentication",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "var respData = JSON.parse(responseBody);",
+ "",
+ "tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
+ "",
+ "pm.collectionVariables.set('configUid',respData.configUid);"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ],\n \"certificates\": [\n {\n \"type\": \"certificate\",\n \"name\": \"test_cert\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.crt\",\n \"authentication\": [\n {\n \"profile\": \"Bearer token-based authentication profile\"\n }\n ]\n }\n },\n {\n \"type\": \"key\",\n \"name\": \"test_key\",\n \"contents\": {\n \"content\": \"{{github_gitops_root}}/v2/www.online-boutique.local.key\",\n \"authentication\": [\n {\n \"profile\": \"Bearer token-based authentication profile\"\n }\n ]\n }\n }\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Online boutique HTTPS\",\n \"names\": [\n \"www.online-boutique.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:443\",\n \"http2\": true,\n \"tls\": {\n \"certificate\": \"test_cert\",\n \"key\": \"test_key\",\n \"ciphers\": \"DEFAULT\",\n \"protocols\": [\n \"TLSv1.2\",\n \"TLSv1.3\"\n ]\n }\n },\n \"log\": {\n \"access\": \"/var/log/nginx/online_boutique_https_access_log\",\n \"error\": \"/var/log/nginx/online_boutique_https_error_log\"\n },\n \"locations\": [\n {\n \"uri\": \"/\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://upstream_boutique\",\n \"authentication\": {\n \"server\": [\n {\n \"profile\": \"Header-based authentication profile\"\n }\n ]\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"upstream_boutique\",\n \"origin\": [\n {\n \"server\": \"192.168.2.200:80\"\n }\n ]\n }\n ],\n \"authentication\": {\n \"server\": [\n {\n \"name\": \"Bearer token-based authentication profile\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"bearer\"\n }\n },\n {\n \"name\": \"Header-based authentication profile\",\n \"type\": \"token\",\n \"token\": {\n \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJpYXQiOjE3MDI0ODEzNjcsImV4cCI6MTcwMjQ4MTM2OH0.eyJuYW1lIjoiQm9iIERldk9wcyIsInN1YiI6IkpXVCBzdWIgY2xhaW0iLCJpc3MiOiJKV1QgaXNzIGNsYWltIiwicm9sZXMiOlsiZGV2b3BzIl19.SKA_7MszAypMEtX5NDQ0TcUbVYx_Wt0hrtmuyTmrVKU\",\n \"type\": \"header\",\n \"location\": \"X-AUTH-TOKEN\"\n }\n }\n ]\n }\n }\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
+ "protocol": "http",
+ "host": [
+ "{{ncg_host}}"
+ ],
+ "port": "{{ncg_port}}",
+ "path": [
+ "{{ngc_api_version}}",
+ "config"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
],
"event": [
@@ -3880,7 +5863,7 @@
},
{
"key": "ngc_api_version",
- "value": "v4.0",
+ "value": "v4.1",
"type": "string"
}
]
diff --git a/contrib/postman/README.md b/contrib/postman/README.md
index 867fcbb..7eaa52d 100644
--- a/contrib/postman/README.md
+++ b/contrib/postman/README.md
@@ -2,7 +2,7 @@
This collection contains:
-API v4.0 - Latest
+API v4.1 - Latest
- `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST
- `Declarative automation examples` - Several examples and use cases
- `API Gateway` - Sample API gateway requests for Swagger and OpenAPI schemas import
@@ -11,7 +11,19 @@ API v4.0 - Latest
- `Housekeeping - common endpoints` - Miscellaneous general purpose requests
- `JWT Client Authentication` - JWT-based client authentication for HTTP
- `mTLS Client Authentication` - mTLS client authentication for HTTP
- - `NGINX App Protect WAF` - Sample requests for declarative configuration lifecycle management
+ - `NGINX App Protect WAF` - Sample requests for declarative configuration lifecycle management
+ - `Server-side and source of truth authentication` - Requests for authentication towards upstreams and source of truth
+
+API v4.0
+- `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST
+- `Declarative automation examples` - Several examples and use cases
+ - `API Gateway` - Sample API gateway requests for Swagger and OpenAPI schemas import
+ - `CRUD automation` - Sample requests for CRUD-based automation
+ - `GitOps autosync` - GitOps automation demo
+ - `Housekeeping - common endpoints` - Miscellaneous general purpose requests
+ - `JWT Client Authentication` - JWT-based client authentication for HTTP
+ - `mTLS Client Authentication` - mTLS client authentication for HTTP
+ - `NGINX App Protect WAF` - Sample requests for declarative configuration lifecycle management
API v3.1 - Deprecated
- `Configuration generation` - Declaration examples with output to plaintext, JSON, Kubernetes ConfigMap, HTTP POST
diff --git a/etc/config.toml b/etc/config.toml
index 03b8318..71e0ef1 100644
--- a/etc/config.toml
+++ b/etc/config.toml
@@ -3,7 +3,7 @@
# Main variables
[main]
banner = "NGINX Declarative API"
-version = "4.0"
+version = "4.1"
url = "https://github.com/f5devcentral/NGINX-Declarative-API"
# Templates
diff --git a/src/V4_1_CreateConfig.py b/src/V4_1_CreateConfig.py
new file mode 100644
index 0000000..eb4856f
--- /dev/null
+++ b/src/V4_1_CreateConfig.py
@@ -0,0 +1,792 @@
+"""
+Configuration creation based on jinja2 templates
+"""
+
+import base64
+import json
+import pickle
+import time
+import uuid
+from datetime import datetime
+from urllib.parse import urlparse
+
+import requests
+import schedule
+from fastapi.responses import Response, JSONResponse
+from jinja2 import Environment, FileSystemLoader
+from pydantic import ValidationError
+from requests.packages.urllib3.exceptions import InsecureRequestWarning
+
+import v4_1.APIGateway
+import v4_1.DevPortal
+import v4_1.DeclarationPatcher
+import v4_1.GitOps
+import v4_1.MiscUtils
+
+# NGINX App Protect helper functions
+import v4_1.NAPUtils
+import v4_1.NIMUtils
+
+# NGINX Declarative API modules
+from NcgConfig import NcgConfig
+from NcgRedis import NcgRedis
+
+# pydantic models
+from V4_1_NginxConfigDeclaration import *
+
+# Tolerates self-signed TLS certificates
+requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
+
+
+def getuniqueid():
+ return uuid.uuid4()
+
+
+def configautosync(configUid):
+ print("Autosyncing configuid [" + configUid + "]")
+
+ declaration = ''
+ declFromRedis = NcgRedis.redis.get(f'ncg.declaration.{configUid}')
+
+ if declFromRedis is not None:
+ declaration = pickle.loads(declFromRedis)
+ apiversion = NcgRedis.redis.get(f'ncg.apiversion.{configUid}').decode()
+
+ createconfig(declaration=declaration, apiversion=apiversion, runfromautosync=True, configUid=configUid)
+
+
+# Create the given declarative configuration
+# Return a JSON string:
+# { "status_code": nnn, "headers": {}, "message": {} }
+def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosync: bool = False, configUid: str = ""):
+ # Building NGINX configuration for the given declaration
+
+ # NGINX configuration files for staged config
+ configFiles = {'files': [], 'rootDir': NcgConfig.config['nms']['config_dir']}
+
+ # NGINX auxiliary files for staged config
+ auxFiles = {'files': [], 'rootDir': NcgConfig.config['nms']['config_dir']}
+
+ try:
+ # Pydantic JSON validation
+ ConfigDeclaration(**declaration.model_dump())
+ except ValidationError as e:
+ print(f'Invalid declaration {e}')
+
+ d = declaration.model_dump()
+ decltype = d['output']['type']
+
+ j2_env = Environment(loader=FileSystemLoader(NcgConfig.config['templates']['root_dir'] + '/' + apiversion),
+ trim_blocks=True, extensions=["jinja2_base64_filters.Base64Filters"])
+ j2_env.filters['regex_replace'] = v4_1.MiscUtils.regex_replace
+
+ if 'http' in d['declaration']:
+ if 'snippet' in d['declaration']['http']:
+ status, snippet = v4_1.GitOps.getObjectFromRepo(object = d['declaration']['http']['snippet'], authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": snippet}}
+
+ d['declaration']['http']['snippet'] = snippet
+
+ # Check HTTP upstreams validity
+ all_upstreams = []
+ http = d['declaration']['http']
+
+ if 'upstreams' in http:
+ for i in range(len(http['upstreams'])):
+
+ upstream = http['upstreams'][i]
+
+ if upstream['snippet']:
+ status, snippet = v4_1.GitOps.getObjectFromRepo(object = upstream['snippet'], authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": snippet}}
+
+ d['declaration']['http']['upstreams'][i]['snippet'] = snippet
+
+ all_upstreams.append(http['upstreams'][i]['name'])
+
+ # Check HTTP rate_limit profiles validity
+ all_ratelimits = []
+ http = d['declaration']['http']
+
+ d_rate_limit = v4_1.MiscUtils.getDictKey(d, 'declaration.http.rate_limit')
+ if d_rate_limit is not None:
+ for i in range(len(d_rate_limit)):
+ all_ratelimits.append(d_rate_limit[i]['name'])
+
+ # Check authentication profiles validity and creates authentication config files
+
+ # List of all auth client & server profile names
+ all_auth_client_profiles = []
+ all_auth_server_profiles = []
+
+ d_auth_profiles = v4_1.MiscUtils.getDictKey(d, 'declaration.http.authentication')
+ if d_auth_profiles is not None:
+ if 'client' in d_auth_profiles:
+ # Render all client authentication profiles
+
+ auth_client_profiles = d_auth_profiles['client']
+ for i in range(len(auth_client_profiles)):
+ auth_profile = auth_client_profiles[i]
+
+ match auth_profile['type']:
+ case 'jwt':
+ # Add the rendered authentication configuration snippet as a config file in the staged configuration - jwt template
+ templateName = NcgConfig.config['templates']['auth_client_root']+"/jwt.tmpl"
+ renderedClientAuthProfile = j2_env.get_template(templateName).render(
+ authprofile=auth_profile, ncgconfig=NcgConfig.config)
+
+ b64renderedClientAuthProfile = base64.b64encode(bytes(renderedClientAuthProfile, 'utf-8')).decode('utf-8')
+ configFileName = NcgConfig.config['nms']['auth_client_dir'] + '/'+auth_profile['name'].replace(' ','_')+".conf"
+ authProfileConfigFile = {'contents': b64renderedClientAuthProfile,
+ 'name': configFileName }
+
+ all_auth_client_profiles.append(auth_profile['name'])
+ auxFiles['files'].append(authProfileConfigFile)
+
+ # Add the rendered authentication configuration snippet as a config file in the staged configuration - jwks template
+ templateName = NcgConfig.config['templates']['auth_client_root']+"/jwks.tmpl"
+ renderedClientAuthProfile = j2_env.get_template(templateName).render(
+ authprofile=auth_profile, ncgconfig=NcgConfig.config)
+
+ b64renderedClientAuthProfile = base64.b64encode(bytes(renderedClientAuthProfile, 'utf-8')).decode('utf-8')
+ configFileName = NcgConfig.config['nms']['auth_client_dir'] + '/jwks_'+auth_profile['name'].replace(' ','_')+".conf"
+ authProfileConfigFile = {'contents': b64renderedClientAuthProfile,
+ 'name': configFileName }
+
+ all_auth_client_profiles.append(auth_profile['name'])
+ auxFiles['files'].append(authProfileConfigFile)
+
+
+ if 'server' in d_auth_profiles:
+ # Render all server authentication profiles
+
+ auth_server_profiles = d_auth_profiles['server']
+ for i in range(len(auth_server_profiles)):
+ auth_profile = auth_server_profiles[i]
+
+ match auth_profile['type']:
+ case 'token':
+ # Add the rendered authentication configuration snippet as a config file in the staged configuration - jwt template
+ templateName = NcgConfig.config['templates']['auth_server_root']+"/token.tmpl"
+ renderedServerAuthProfile = j2_env.get_template(templateName).render(
+ authprofile=auth_profile, ncgconfig=NcgConfig.config)
+
+ b64renderedServerAuthProfile = base64.b64encode(bytes(renderedServerAuthProfile, 'utf-8')).decode('utf-8')
+ configFileName = NcgConfig.config['nms']['auth_server_dir'] + '/'+auth_profile['name'].replace(' ','_')+".conf"
+ authProfileConfigFile = {'contents': b64renderedServerAuthProfile,
+ 'name': configFileName }
+
+ all_auth_server_profiles.append(auth_profile['name'])
+ auxFiles['files'].append(authProfileConfigFile)
+
+ # Parse HTTP servers
+ d_servers = v4_1.MiscUtils.getDictKey(d, 'declaration.http.servers')
+ if d_servers is not None:
+ apiGatewaySnippet = ''
+
+ for server in d_servers:
+ serverSnippet = ''
+
+ if server['snippet']:
+ status, serverSnippet = v4_1.GitOps.getObjectFromRepo(object = server['snippet'], authProfiles = d['declaration']['http']['authentication'], base64Encode = False)
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": serverSnippet}}
+
+ serverSnippet = serverSnippet['content']
+
+ for loc in server['locations']:
+ if loc['snippet']:
+ status, snippet = v4_1.GitOps.getObjectFromRepo(object = loc['snippet'], authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": snippet}}
+
+ loc['snippet'] = snippet
+
+ # Location upstream name validity check
+ if 'upstream' in loc and loc['upstream'] and urlparse(loc['upstream']).netloc not in all_upstreams:
+ return {"status_code": 422,
+ "message": {"status_code": status, "message":
+ {"code": status, "content": f"invalid HTTP upstream [{loc['upstream']}]"}}}
+
+ # Location client authentication name validity check
+ if 'authentication' in loc and loc['authentication']:
+ locAuthClientProfiles = loc['authentication']['client']
+
+ for authClientProfile in locAuthClientProfiles:
+ if authClientProfile['profile'] not in all_auth_client_profiles:
+ return {"status_code": 422,
+ "message": {"status_code": status, "message":
+ {"code": status, "content": f"invalid client authentication profile [{authClientProfile['profile']}] in location [{loc['uri']}]"}}}
+
+ # Location server authentication name validity check
+ if 'authentication' in loc and loc['authentication']:
+ locAuthServerProfiles = loc['authentication']['server']
+
+ for authServerProfile in locAuthServerProfiles:
+ if authServerProfile['profile'] not in all_auth_server_profiles:
+ return {"status_code": 422,
+ "message": {"status_code": status, "message":
+ {"code": status, "content": f"invalid server authentication profile [{authServerProfile['profile']}] in location [{loc['uri']}]"}}}
+
+ # API Gateway provisioning
+ if loc['apigateway'] and loc['apigateway']['api_gateway'] and loc['apigateway']['api_gateway']['enabled'] and loc['apigateway']['api_gateway']['enabled'] == True:
+ openApiAuthProfile = loc['apigateway']['openapi_schema']['authentication']
+ if openApiAuthProfile and openApiAuthProfile[0]['profile'] not in all_auth_server_profiles:
+ return {"status_code": 422,
+ "message": {"status_code": status, "message":
+ {"code": status,
+ "content": f"invalid server authentication profile [{openApiAuthProfile[0]['profile']}] for OpenAPI schema [{loc['apigateway']['openapi_schema']['content']}]"}}}
+
+ status, apiGatewayConfigDeclaration = v4_1.APIGateway.createAPIGateway(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication'])
+ else:
+ apiGatewayConfigDeclaration = ''
+
+ # API Gateway Developer portal provisioning
+ if loc['apigateway'] and loc['apigateway']['developer_portal'] and 'enabled' in loc['apigateway']['developer_portal'] and loc['apigateway']['developer_portal']['enabled'] == True:
+
+ status, devPortalHTML = v4_1.DevPortal.createDevPortal(locationDeclaration = loc, authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 400,
+ "message": {"status_code": status, "message":
+ {"code": status, "content": f"Developer Portal creation failed for {loc['apigateway']['openapi_schema']['content']}"}}}
+
+ ### Add optional API Developer portal HTML files
+ # devPortalHTML
+ newAuxFile = {'contents': devPortalHTML, 'name': NcgConfig.config['nms']['devportal_dir'] +
+ loc['apigateway']['developer_portal']['uri']}
+ auxFiles['files'].append(newAuxFile)
+
+ ### / Add optional API Developer portal HTML files
+
+ if loc['rate_limit'] is not None:
+ if 'profile' in loc['rate_limit'] and loc['rate_limit']['profile'] and loc['rate_limit'][
+ 'profile'] not in all_ratelimits:
+ return {"status_code": 422,
+ "message": {
+ "status_code": status,
+ "message":
+ {"code": status,
+ "content":
+ f"invalid rate_limit profile [{loc['rate_limit']['profile']}]"}}}
+
+ # API Gateway configuration template rendering
+ apiGatewaySnippet += j2_env.get_template(NcgConfig.config['templates']['apigwconf']).render(
+ declaration=apiGatewayConfigDeclaration, ncgconfig=NcgConfig.config)\
+ if apiGatewayConfigDeclaration else ''
+
+ server['snippet']['content'] = base64.b64encode(bytes(serverSnippet + apiGatewaySnippet, 'utf-8')).decode('utf-8')
+
+ if 'layer4' in d['declaration']:
+ # Check Layer4/stream upstreams validity
+ all_upstreams = []
+
+ d_upstreams = v4_1.MiscUtils.getDictKey(d, 'declaration.layer4.upstreams')
+ if d_upstreams is not None:
+ for i in range(len(d_upstreams)):
+ all_upstreams.append(d_upstreams[i]['name'])
+
+ d_servers = v4_1.MiscUtils.getDictKey(d, 'declaration.layer4.servers')
+ if d_servers is not None:
+ for server in d_servers:
+
+ if server['snippet']:
+ status, snippet = v4_1.GitOps.getObjectFromRepo(object = server['snippet'], authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": snippet}}
+
+ server['snippet'] = snippet
+
+ if 'upstream' in server and server['upstream'] and server['upstream'] not in all_upstreams:
+ return {"status_code": 422,
+ "message": {
+ "status_code": status,
+ "message":
+ {"code": status, "content": f"invalid Layer4 upstream {server['upstream']}"}}}
+
+ # HTTP configuration template rendering
+ httpConf = j2_env.get_template(NcgConfig.config['templates']['httpconf']).render(
+ declaration=d['declaration']['http'], ncgconfig=NcgConfig.config) if 'http' in d['declaration'] else ''
+
+ # Stream configuration template rendering
+ streamConf = j2_env.get_template(NcgConfig.config['templates']['streamconf']).render(
+ declaration=d['declaration']['layer4'], ncgconfig=NcgConfig.config) if 'layer4' in d['declaration'] else ''
+
+ b64HttpConf = str(base64.b64encode(httpConf.encode("utf-8")), "utf-8")
+ b64StreamConf = str(base64.b64encode(streamConf.encode("utf-8")), "utf-8")
+
+ if decltype.lower() == "plaintext":
+ # Plaintext output
+ return httpConf + streamConf
+
+ elif decltype.lower() == "json" or decltype.lower() == 'http':
+ # JSON-wrapped b64-encoded output
+ payload = {"http_config": f"{b64HttpConf}", "stream_config": f"{b64StreamConf}"}
+
+ if decltype.lower() == "json":
+ # JSON output
+ return {"status_code": 200, "message": {"status_code": 200, "message": payload}}
+ else:
+ # HTTP POST output
+ try:
+ r = requests.post(d['output']['http']['url'], data=json.dumps(payload),
+ headers={'Content-Type': 'application/json'})
+ except:
+ headers = {'Content-Type': 'application/json'}
+ content = {'message': d['output']['http']['url'] + ' unreachable'}
+
+ return {"status_code": 502, "message": {"status_code": 502, "message": content}, "headers": headers}
+
+ r.headers.pop("Content-Length") if "Content-Length" in r.headers else ''
+ r.headers.pop("Server") if "Server" in r.headers else ''
+ r.headers.pop("Date") if "Date" in r.headers else ''
+ r.headers.pop("Content-Type") if "Content-Type" in r.headers else ''
+
+ r.headers['Content-Type'] = 'application/json'
+
+ return {"status_code": r.status_code, "message": {"code": r.status_code, "content": r.text},
+ "headers": r.headers}
+
+ elif decltype.lower() == 'configmap':
+ # Kubernetes ConfigMap output
+ cmHttp = j2_env.get_template(NcgConfig.config['templates']['configmap']).render(nginxconfig=httpConf,
+ name=d['output']['configmap'][
+ 'name'] + '.http',
+ filename=
+ d['output']['configmap'][
+ 'filename'] + '.http',
+ namespace=
+ d['output']['configmap'][
+ 'namespace'])
+ cmStream = j2_env.get_template(NcgConfig.config['templates']['configmap']).render(nginxconfig=streamConf,
+ name=d['output']['configmap'][
+ 'name'] + '.stream',
+ filename=
+ d['output']['configmap'][
+ 'filename'] + '.stream',
+ namespace=
+ d['output']['configmap'][
+ 'namespace'])
+
+ return Response(content=cmHttp + '\n---\n' + cmStream, headers={'Content-Type': 'application/x-yaml'})
+
+ elif decltype.lower() == 'nms':
+ # NGINX Instance Manager Staged Configuration publish
+
+ nmsUsername = v4_1.MiscUtils.getDictKey(d, 'output.nms.username')
+ nmsPassword = v4_1.MiscUtils.getDictKey(d, 'output.nms.password')
+ nmsInstanceGroup = v4_1.MiscUtils.getDictKey(d, 'output.nms.instancegroup')
+ nmsSynctime = v4_1.MiscUtils.getDictKey(d, 'output.nms.synctime')
+
+ nmsUrlFromJson = v4_1.MiscUtils.getDictKey(d, 'output.nms.url')
+ urlCheck = urlparse(nmsUrlFromJson)
+
+ if urlCheck.scheme not in ['http', 'https'] or urlCheck.scheme == "" or urlCheck.netloc == "":
+ return {"status_code": 400,
+ "message": {"status_code": 400, "message": {"code": 400,
+ "content": f"invalid NGINX Management Suite URL {nmsUrlFromJson}"}},
+ "headers": {'Content-Type': 'application/json'}}
+
+ nmsUrl = f"{urlCheck.scheme}://{urlCheck.netloc}"
+
+ if nmsSynctime < 0:
+ return {"status_code": 400,
+ "message": {"status_code": 400, "message": {"code": 400, "content": "synctime must be >= 0"}},
+ "headers": {'Content-Type': 'application/json'}}
+
+ # Fetch NGINX App Protect WAF policies from source of truth if needed
+ d_policies = v4_1.MiscUtils.getDictKey(d, 'output.nms.policies')
+ if d_policies is not None:
+ for policy in d_policies:
+ if 'versions' in policy:
+ for policyVersion in policy['versions']:
+ status, content = v4_1.GitOps.getObjectFromRepo(object = policyVersion['contents'], authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": content}}
+
+ policyVersion['contents'] = content
+
+ # Check TLS items validity
+ all_tls = {'certificate': {}, 'key': {}}
+
+ d_certs = v4_1.MiscUtils.getDictKey(d, 'output.nms.certificates')
+ if d_certs is not None:
+ for i in range(len(d_certs)):
+ if d_certs[i]['name']:
+ all_tls[d_certs[i]['type']][d_certs[i]['name']] = True
+
+ d_servers = v4_1.MiscUtils.getDictKey(d, 'declaration.http.servers')
+ if d_servers is not None:
+ for server in d_servers:
+ if server['listen'] is not None:
+ if 'tls' in server['listen']:
+ cert_name = v4_1.MiscUtils.getDictKey(server, 'listen.tls.certificate')
+ if cert_name and cert_name not in all_tls['certificate']:
+ return {"status_code": 422,
+ "message": {
+ "status_code": 422,
+ "message": {"code": 422,
+ "content": "invalid TLS certificate " +
+ cert_name + " for server" + str(
+ server['names'])}
+ }}
+
+ cert_key = v4_1.MiscUtils.getDictKey(server, 'listen.tls.key')
+ if cert_key and cert_key not in all_tls['key']:
+ return {"status_code": 422,
+ "message": {
+ "status_code": 422,
+ "message": {"code": 422,
+ "content": "invalid TLS key " + cert_key + " for server" + str(
+ server['names'])}
+ }}
+
+ trusted_cert_name = v4_1.MiscUtils.getDictKey(server, 'listen.tls.trusted_ca_certificates')
+ if trusted_cert_name and trusted_cert_name not in all_tls['certificate']:
+ return {"status_code": 422,
+ "message": {
+ "status_code": 422,
+ "message": {"code": 422,
+ "content": "invalid trusted CA certificate " +
+ trusted_cert_name + " for server" + str(server['names'])}
+ }}
+
+ if v4_1.MiscUtils.getDictKey(server, 'listen.tls.mtls.enabled') in ['optional_no_ca'] \
+ and 'ocsp' in server['listen']['tls']:
+ return {"status_code": 422,
+ "message": {
+ "status_code": 422,
+ "message": {"code": 422,
+ "content": "OCSP is incompatible with 'optional_no_ca' client "
+ "mTLS verification for server" + str(
+ server['names'])}
+ }}
+
+ client_cert_name = v4_1.MiscUtils.getDictKey(server, 'listen.tls.mtls.client_certificates')
+ if client_cert_name and client_cert_name not in all_tls['certificate']:
+ return {"status_code": 422,
+ "message": {
+ "status_code": 422,
+ "message": {"code": 422,
+ "content": f"invalid mTLS client certificates [{client_cert_name}] for server {str(server['names'])}"}
+ }}
+
+ # Add optional certificates specified under output.nms.certificates
+ extensions_map = {'certificate': '.crt', 'key': '.key'}
+
+ d_certificates = v4_1.MiscUtils.getDictKey(d, 'output.nms.certificates')
+ if d_certificates is not None:
+ for c in d_certificates:
+ status, certContent = v4_1.GitOps.getObjectFromRepo(object = c['contents'], authProfiles = d['declaration']['http']['authentication'])
+
+ if status != 200:
+ return {"status_code": 422,
+ "message": {"status_code": status, "message": {"code": status, "content": certContent}}}
+
+ newAuxFile = {'contents': certContent['content'], 'name': NcgConfig.config['nms']['certs_dir'] +
+ '/' + c['name'] + extensions_map[c['type']]}
+ auxFiles['files'].append(newAuxFile)
+
+ ### / Add optional certificates specified under output.nms.certificates
+
+ # NGINX main configuration file through template
+ j2_env = Environment(loader=FileSystemLoader(NcgConfig.config['templates']['root_dir'] + '/' + apiversion),
+ trim_blocks=True, extensions=["jinja2_base64_filters.Base64Filters"])
+
+ nginxMainConf = j2_env.get_template(NcgConfig.config['templates']['nginxmain']).render(
+ nginxconf={'modules': v4_1.MiscUtils.getDictKey(d, 'output.nms.modules')})
+
+ # Base64-encoded NGINX main configuration (/etc/nginx/nginx.conf)
+ b64NginxMain = str(base64.urlsafe_b64encode(nginxMainConf.encode("utf-8")), "utf-8")
+
+ # Base64-encoded NGINX mime.types (/etc/nginx/mime.types)
+ f = open(NcgConfig.config['templates']['root_dir'] + '/' + apiversion + '/' + NcgConfig.config['templates'][
+ 'mimetypes'], 'r')
+ nginxMimeTypes = f.read()
+ f.close()
+
+ b64NginxMimeTypes = str(base64.urlsafe_b64encode(nginxMimeTypes.encode("utf-8")), "utf-8")
+ filesMimeType = {'contents': b64NginxMimeTypes, 'name': NcgConfig.config['nms']['config_dir'] + '/mime.types'}
+ auxFiles['files'].append(filesMimeType)
+
+ # Base64-encoded NGINX HTTP service configuration
+ filesNginxMain = {'contents': b64NginxMain, 'name': NcgConfig.config['nms']['config_dir'] + '/nginx.conf'}
+ filesHttpConf = {'contents': b64HttpConf,
+ 'name': NcgConfig.config['nms']['config_dir'] + '/' + NcgConfig.config['nms'][
+ 'staged_config_http_filename']}
+ filesStreamConf = {'contents': b64StreamConf,
+ 'name': NcgConfig.config['nms']['config_dir'] + '/' + NcgConfig.config['nms'][
+ 'staged_config_stream_filename']}
+
+ # Append config files to staged configuration
+ configFiles['files'].append(filesNginxMain)
+ configFiles['files'].append(filesHttpConf)
+ configFiles['files'].append(filesStreamConf)
+
+ # Staged config
+ baseStagedConfig = {'auxFiles': auxFiles, 'configFiles': configFiles}
+ stagedConfig = {'auxFiles': auxFiles, 'configFiles': configFiles,
+ 'updateTime': datetime.utcnow().isoformat()[:-3] + 'Z',
+ 'ignoreConflict': True, 'validateConfig': False}
+
+ currentBaseStagedConfig = NcgRedis.redis.get(f'ncg.basestagedconfig.{configUid}').decode('utf-8') if NcgRedis.redis.get(f'ncg.basestagedconfig.{configUid}') else None
+ newBaseStagedConfig = json.dumps(baseStagedConfig)
+
+ if currentBaseStagedConfig is not None and newBaseStagedConfig == currentBaseStagedConfig:
+ print(f'Declaration [{configUid}] not changed')
+ return {"status_code": 200,
+ "message": {"status_code": 200, "message": {"code": 200, "content": "no changes"}}}
+ else:
+ # Configuration objects have changed, publish to NIM needed
+ print(f'Declaration [{configUid}] changed, publishing to NMS')
+
+ # Retrieve instance group uid
+ try:
+ ig = requests.get(url=f'{nmsUrl}/api/platform/v1/instance-groups', auth=(nmsUsername, nmsPassword),
+ verify=False)
+ except Exception as e:
+ return {"status_code": 400,
+ "message": {"status_code": 400,
+ "message": {"code": 400, "content": f"Can't connect to {nmsUrl}"}}}
+
+ if ig.status_code != 200:
+ try:
+ return {"status_code": ig.status_code,
+ "message": {"status_code": ig.status_code,
+ "message": {"code": ig.status_code, "content": json.loads(ig.text)}}}
+ except:
+ return {"status_code": ig.status_code,
+ "message": {"status_code": ig.status_code,
+ "message": {"code": ig.status_code, "content": ig.text}}}
+
+ # Get the instance group id
+ igUid = v4_1.NIMUtils.getNIMInstanceGroupUid(nmsUrl=nmsUrl, nmsUsername=nmsUsername,
+ nmsPassword=nmsPassword, instanceGroupName=nmsInstanceGroup)
+
+ # Invalid instance group
+ if igUid is None:
+ return {"status_code": 404,
+ "message": {"status_code": 404, "message": {"code": 404,
+ "content": f"instance group {nmsInstanceGroup} not found"}},
+ "headers": {'Content-Type': 'application/json'}}
+
+ ### NGINX App Protect policies support - commits policies to control plane
+
+ # Check NGINX App Protect WAF policies configuration sanity
+ status, description = v4_1.NAPUtils.checkDeclarationPolicies(d)
+
+ if status != 200:
+ return {"status_code": 422, "message": {"status_code": status, "message": description}}
+
+ # Provision NGINX App Protect WAF policies to NGINX Instance Manager
+ provisionedNapPolicies, activePolicyUids = v4_1.NAPUtils.provisionPolicies(
+ nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword, declaration=d)
+
+ ### / NGINX App Protect policies support
+
+ ### Publish staged config to instance group
+ r = requests.post(url=nmsUrl + f"/api/platform/v1/instance-groups/{igUid}/config",
+ data=json.dumps(stagedConfig),
+ headers={'Content-Type': 'application/json'},
+ auth=(nmsUsername, nmsPassword),
+ verify=False)
+
+ if r.status_code != 202:
+ # Configuration push failed
+ return {"status_code": r.status_code,
+ "message": {"status_code": r.status_code, "message": r.text},
+ "headers": {'Content-Type': 'application/json'}}
+
+ # Fetch the deployment status
+ publishResponse = json.loads(r.text)
+
+ # Wait for either NIM success or failure after pushing a staged config
+ isPending = True
+ while isPending:
+ time.sleep(NcgConfig.config['nms']['staged_config_publish_waittime'])
+ deploymentCheck = requests.get(url=nmsUrl + publishResponse['links']['rel'],
+ auth=(nmsUsername, nmsPassword),
+ verify=False)
+
+ checkJson = json.loads(deploymentCheck.text)
+
+ if not checkJson['details']['pending']:
+ isPending = False
+
+ if len(checkJson['details']['failure']) > 0:
+ # Staged config publish to NIM failed
+ jsonResponse = checkJson['details']['failure'][0]
+ deploymentCheck.status_code = 422
+ else:
+ # Staged config publish to NIM succeeded
+ jsonResponse = json.loads(deploymentCheck.text)
+
+ # if nmsSynctime > 0 and runfromautosync == False:
+ if runfromautosync == False:
+ # No configuration is found, generate one
+ configUid = str(getuniqueid())
+
+ # Stores the staged config to redis
+ # Redis keys:
+ # ncg.declaration.[configUid] = original config declaration
+ # ncg.declarationrendered.[configUid] = original config declaration - rendered
+ # ncg.basestagedconfig.[configUid] = base staged configuration
+ # ncg.apiversion.[configUid] = ncg API version
+ # ncg.status.[configUid] = latest status
+
+ NcgRedis.redis.set(f'ncg.declaration.{configUid}', pickle.dumps(declaration))
+ NcgRedis.redis.set(f'ncg.declarationrendered.{configUid}', json.dumps(d))
+ NcgRedis.redis.set(f'ncg.basestagedconfig.{configUid}', json.dumps(baseStagedConfig))
+ NcgRedis.redis.set(f'ncg.apiversion.{configUid}', apiversion)
+
+ # Makes NGINX App Protect policies active
+ doWeHavePolicies = v4_1.NAPUtils.makePolicyActive(nmsUrl=nmsUrl, nmsUsername=nmsUsername,
+ nmsPassword=nmsPassword,
+ activePolicyUids=activePolicyUids,
+ instanceGroupUid=igUid)
+
+ if doWeHavePolicies:
+ # Clean up NGINX App Protect WAF policies not used anymore
+ # and not defined in the declaration just pushed
+ time.sleep(NcgConfig.config['nms']['staged_config_publish_waittime'])
+ v4_1.NAPUtils.cleanPolicyLeftovers(nmsUrl=nmsUrl, nmsUsername=nmsUsername,
+ nmsPassword=nmsPassword,
+ currentPolicies=provisionedNapPolicies)
+
+ # If deploying a new configuration in GitOps mode start autosync
+ if nmsSynctime == 0:
+ NcgRedis.declarationsList[configUid] = "static"
+ elif not runfromautosync:
+ # GitOps autosync
+ print(f'Starting autosync for configUid {configUid} every {nmsSynctime} seconds')
+
+ job = schedule.every(nmsSynctime).seconds.do(lambda: configautosync(configUid))
+ # Keep track of GitOps configs, key is the threaded job
+ NcgRedis.declarationsList[configUid] = job
+
+ NcgRedis.redis.set(f'ncg.apiversion.{configUid}', apiversion)
+
+ responseContent = {'code': deploymentCheck.status_code, 'content': jsonResponse, 'configUid': configUid}
+
+ # Configuration push completed, update redis keys
+ if configUid != "":
+ NcgRedis.redis.set('ncg.status.' + configUid, json.dumps(responseContent))
+
+ # if nmsSynctime > 0:
+ # Updates status, declaration and basestagedconfig in redis
+ NcgRedis.redis.set('ncg.declaration.' + configUid, pickle.dumps(declaration))
+ NcgRedis.redis.set('ncg.declarationrendered.' + configUid, json.dumps(d))
+ NcgRedis.redis.set('ncg.basestagedconfig.' + configUid, json.dumps(baseStagedConfig))
+
+ return {"status_code": deploymentCheck.status_code,
+ "message": {"status_code": deploymentCheck.status_code,
+ "message": responseContent},
+ "headers": {'Content-Type': 'application/json'}
+ }
+
+ else:
+ return {"status_code": 422, "message": {"status_code": 422, "message": f"output type {decltype} unknown"}}
+
+
+def patch_config(declaration: ConfigDeclaration, configUid: str, apiversion: str):
+ # Patch a declaration
+ if configUid not in NcgRedis.declarationsList:
+ return JSONResponse(
+ status_code=404,
+ content={'code': 404, 'details': {'message': f'declaration {configUid} not found'}},
+ headers={'Content-Type': 'application/json'}
+ )
+
+ # The declaration sections to be patched
+ declarationToPatch = declaration.model_dump()
+
+ # The currently applied declaration
+ status_code, currentDeclaration = get_declaration(configUid=configUid)
+
+ # Handle policy updates
+ d_policies = v4_1.MiscUtils.getDictKey(declarationToPatch, 'output.nms.policies')
+ if d_policies is not None:
+ # NGINX App Protect WAF policy updates
+ for p in d_policies:
+ currentDeclaration = v4_1.DeclarationPatcher.patchNAPPolicies(
+ sourceDeclaration=currentDeclaration, patchedNAPPolicies=p)
+
+ # Handle certificate updates
+ d_certificates = v4_1.MiscUtils.getDictKey(declarationToPatch, 'output.nms.certificates')
+ if d_certificates is not None:
+ # TLS certificate/key updates
+ for p in d_certificates:
+ currentDeclaration = v4_1.DeclarationPatcher.patchCertificates(
+ sourceDeclaration=currentDeclaration, patchedCertificates=p)
+
+ # Handle declaration updates
+ if 'declaration' in declarationToPatch:
+ # HTTP
+ d_upstreams = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.http.upstreams')
+ if d_upstreams:
+ # HTTP upstream patch
+ for u in d_upstreams:
+ currentDeclaration = v4_1.DeclarationPatcher.patchHttpUpstream(
+ sourceDeclaration=currentDeclaration, patchedHttpUpstream=u)
+
+ d_servers = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.http.servers')
+ if d_servers:
+ # HTTP servers patch
+ for s in d_servers:
+ currentDeclaration = v4_1.DeclarationPatcher.patchHttpServer(
+ sourceDeclaration=currentDeclaration, patchedHttpServer=s)
+
+ # Stream / Layer4
+ d_upstreams = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.layer4.upstreams')
+ if d_upstreams:
+ # Stream upstream patch
+ for u in d_upstreams:
+ currentDeclaration = v4_1.DeclarationPatcher.patchStreamUpstream(
+ sourceDeclaration=currentDeclaration, patchedStreamUpstream=u)
+
+ d_servers = v4_1.MiscUtils.getDictKey(declarationToPatch, 'declaration.layer4.servers')
+ if d_servers:
+ # Stream servers patch
+ for s in d_servers:
+ currentDeclaration = v4_1.DeclarationPatcher.patchStreamServer(
+ sourceDeclaration=currentDeclaration, patchedStreamServer=s)
+
+ # Apply the updated declaration
+ configDeclaration = ConfigDeclaration.model_validate_json(json.dumps(currentDeclaration))
+
+ r = createconfig(declaration=configDeclaration, apiversion=apiversion,
+ runfromautosync=True, configUid=configUid)
+
+ # Return the updated declaration
+ message = r['message']
+
+ if r['status_code'] != 200:
+ currentDeclaration = {}
+ # message = f'declaration {configUid} update failed';
+
+ responseContent = {'code': r['status_code'], 'details': {'message': message},
+ 'declaration': currentDeclaration, 'configUid': configUid}
+
+ return JSONResponse(
+ status_code=r['status_code'],
+ content=responseContent,
+ headers={'Content-Type': 'application/json'}
+ )
+
+
+# Gets the given declaration. Returns status_code and body
+def get_declaration(configUid: str):
+ cfg = NcgRedis.redis.get('ncg.declaration.' + configUid)
+
+ if cfg is None:
+ return 404, ""
+
+ return 200, pickle.loads(cfg).dict()
diff --git a/src/V4_1_NginxConfigDeclaration.py b/src/V4_1_NginxConfigDeclaration.py
new file mode 100644
index 0000000..df73430
--- /dev/null
+++ b/src/V4_1_NginxConfigDeclaration.py
@@ -0,0 +1,530 @@
+"""
+JSON declaration format
+"""
+
+from __future__ import annotations
+
+from typing import List, Optional
+
+from pydantic import BaseModel, Extra, model_validator
+
+
+class OutputConfigMap(BaseModel, extra="forbid"):
+ name: str = "nginx-config"
+ namespace: Optional[str] = ""
+ filename: str = "nginx-config.conf"
+
+
+class OutputHttp(BaseModel, extra="forbid"):
+ url: str = ""
+
+
+class NmsCertificate(BaseModel, extra="forbid"):
+ type: str
+ name: str
+ contents: Optional[ObjectFromSourceOfTruth] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'NmsCertificate':
+ _type = self.type
+
+ valid = ['certificate', 'key']
+ if _type not in valid:
+ raise ValueError(f"Invalid certificate type [{_type}] must be one of {str(valid)}")
+
+ return self
+
+
+class NmsPolicyVersion(BaseModel, extra="forbid"):
+ tag: str = ""
+ displayName: Optional[str] = ""
+ description: Optional[str] = ""
+ contents: Optional[ObjectFromSourceOfTruth] = {}
+
+
+class NmsPolicy(BaseModel, extra="forbid"):
+ type: str = ""
+ name: str = ""
+ active_tag: str = ""
+ versions: Optional[List[NmsPolicyVersion]] = []
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'NmsPolicy':
+ _type = self.type
+
+ valid = ['app_protect']
+ if _type not in valid:
+ raise ValueError(f"Invalid policy type [{_type}] must be one of {str(valid)}")
+
+ return self
+
+
+class AppProtectLogProfile(BaseModel, extra="forbid"):
+ name: str
+ format: Optional[str] = "default"
+ format_string: Optional[str] = ""
+ type: Optional[str] = "blocked"
+ max_request_size: Optional[str] = "any"
+ max_message_size: Optional[str] = "5k"
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'AppProtectLogProfile':
+ _type, _format, _format_string = self.type, self.format, self.format_string
+
+ valid = ['all', 'illegal', 'blocked']
+ if _type not in valid:
+ raise ValueError(f"Invalid NGINX App Protect log type [{_type}] must be one of {str(valid)}")
+
+ valid = ['default', 'grpc', 'arcsight', 'splunk', 'user-defined']
+ if _format not in valid:
+ raise ValueError(f"Invalid NGINX App Protect log format [{_format}] must be one of {str(valid)}")
+
+ if _format == 'user-defined' and _format_string == "":
+ raise ValueError(f"NGINX App Protect log format {_format} requires format_string")
+
+ return self
+
+
+class LogProfile(BaseModel, extra="forbid"):
+ type: str
+ app_protect: Optional[AppProtectLogProfile] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'LogProfile':
+ _type, app_protect = self.type, self.app_protect
+
+ valid = ['app_protect']
+ if _type not in valid:
+ raise ValueError(f"Invalid log profile type [{_type}] must be one of {str(valid)}")
+
+ isError = False
+ if _type == 'app_protect':
+ if app_protect is None:
+ isError = True
+
+ if isError:
+ raise ValueError(f"Invalid log profile data for type [{_type}]")
+
+ return self
+
+
+class OutputNMS(BaseModel, extra="forbid"):
+ url: str = ""
+ username: str = ""
+ password: str = ""
+ instancegroup: str = ""
+ synctime: Optional[int] = 0
+ modules: Optional[List[str]] = []
+ certificates: Optional[List[NmsCertificate]] = []
+ policies: Optional[List[NmsPolicy]] = []
+ log_profiles: Optional[List[LogProfile]] = []
+
+
+class Output(BaseModel, extra="forbid"):
+ type: str
+ configmap: Optional[OutputConfigMap] = {}
+ http: Optional[OutputHttp] = {}
+ nms: Optional[OutputNMS] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'Output':
+ _type, configmap, http, nms = self.type, self.configmap, self.http, self.nms
+
+ valid = ['plaintext', 'json', 'configmap', 'http', 'nms']
+ if _type not in valid:
+ raise ValueError(f"Invalid output type [{_type}] must be one of {str(valid)}")
+
+ isError = False
+
+ if _type == 'configmap' and not configmap:
+ isError = True
+ elif _type == 'http' and not http:
+ isError = True
+ elif _type == 'nms' and not nms:
+ isError = True
+
+ if isError:
+ raise ValueError(f"Invalid output data for type [{_type}]")
+
+ return self
+
+
+
+class OcspStapling(BaseModel, extra="forbid"):
+ enabled: Optional[bool] = False
+ verify: Optional[bool] = False
+ responder: Optional[str] = ""
+
+
+class Ocsp(BaseModel, extra="forbid"):
+ enabled: Optional[str] = "off"
+ responder: Optional[str] = ""
+
+
+class Mtls(BaseModel, extra="forbid"):
+ enabled: Optional[str] = "off"
+ client_certificates: str = ""
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'Mtls':
+ _enabled = self.enabled
+
+ valid = ['on', 'off', 'optional', 'optional_no_ca']
+ if _enabled not in valid:
+ raise ValueError(f"Invalid mTLS type [{_enabled}] must be one of {str(valid)}")
+
+ return self
+
+
+class Tls(BaseModel, extra="forbid"):
+ certificate: str = ""
+ key: str = ""
+ trusted_ca_certificates: str = ""
+ ciphers: Optional[str] = ""
+ protocols: Optional[List[str]] = []
+ mtls: Optional[Mtls] = {}
+ ocsp: Optional[Ocsp] = {}
+ stapling: Optional[OcspStapling] = {}
+
+
+class Listen(BaseModel, extra="forbid"):
+ address: Optional[str] = ""
+ http2: Optional[bool] = False
+ tls: Optional[Tls] = {}
+
+
+class ListenL4(BaseModel, extra="forbid"):
+ address: Optional[str] = ""
+ protocol: Optional[str] = "tcp"
+ tls: Optional[Tls] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'ListenL4':
+ protocol, tls = self.protocol, self.tls
+
+ valid = ['tcp', 'udp']
+ if protocol not in valid:
+ raise ValueError(f"Invalid protocol [{protocol}] must be one of {str(valid)}")
+
+ if protocol != 'tcp' and tls and tls.certificate:
+ raise ValueError("TLS termination over UDP is not supported")
+
+ return self
+
+
+class Log(BaseModel, extra="forbid"):
+ access: Optional[str] = ""
+ error: Optional[str] = ""
+
+
+class RateLimit(BaseModel, extra="forbid"):
+ profile: Optional[str] = ""
+ httpcode: Optional[int] = 429
+ burst: Optional[int] = 0
+ delay: Optional[int] = 0
+
+
+class LocationAuthClient(BaseModel, extra="forbid"):
+ profile: Optional[str] = ""
+
+
+class LocationAuthServer(BaseModel, extra="forbid"):
+ profile: Optional[str] = ""
+
+
+class LocationAuth(BaseModel, extra="forbid"):
+ client: Optional[List[LocationAuthClient]] = []
+ server: Optional[List[LocationAuthServer]] = []
+
+
+class RateLimitApiGw(BaseModel, extra="forbid"):
+ profile: Optional[str] = ""
+ httpcode: Optional[int] = 429
+ burst: Optional[int] = 0
+ delay: Optional[int] = 0
+ enforceOnPaths: Optional[bool] = True
+ paths: Optional[List[str]] = []
+
+class APIGatewayAuthentication(BaseModel, extra="forbid"):
+ client: Optional[List[LocationAuthClient]] = []
+ enforceOnPaths: Optional[bool] = True
+ paths: Optional[List[str]] = []
+
+
+class AuthClientJWT(BaseModel, extra="forbid"):
+ realm: str = "JWT Authentication"
+ key: str = ""
+ cachetime: Optional[int] = 0
+ jwt_type: str = "signed"
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'AuthClientJWT':
+ jwt_type, key = self.jwt_type, self.key
+
+ if not key.strip() :
+ raise ValueError(f"Invalid: JWT key must not be empty")
+
+ valid = ['signed', 'encrypted', 'nested']
+ if jwt_type not in valid:
+ raise ValueError(f"Invalid JWT type [{jwt_type}] must be one of {str(valid)}")
+
+ return self
+
+class AuthServerToken(BaseModel, extra="forbid"):
+ token: str = ""
+ type: Optional[str] = "bearer"
+ location: Optional[str] = ""
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'AuthServerToken':
+ location, type = self.location, self.type.lower()
+
+ valid = ['bearer', 'header']
+ if type not in valid:
+ raise ValueError(f"Invalid token type [{type}] must be one of {str(valid)}")
+
+ if type in ['header'] and location == "":
+ raise ValueError(f"Empty location for [{type}] token")
+
+ return self
+
+
+class HealthCheck(BaseModel, extra="forbid"):
+ enabled: Optional[bool] = False
+ uri: Optional[str] = "/"
+ interval: Optional[int] = 5
+ fails: Optional[int] = 1
+ passes: Optional[int] = 1
+
+
+class AppProtectLog(BaseModel, extra="forbid"):
+ enabled: Optional[bool] = False
+ profile_name: Optional[str] = ""
+ destination: Optional[str] = ""
+
+
+class AppProtect(BaseModel, extra="forbid"):
+ enabled: Optional[bool] = False
+ policy: str = ""
+ log: AppProtectLog = {}
+
+
+class Location(BaseModel, extra="forbid"):
+ uri: str
+ urimatch: Optional[str] = "prefix"
+ upstream: Optional[str] = ""
+ log: Optional[Log] = {}
+ apigateway: Optional[APIGateway] = {}
+ caching: Optional[str] = ""
+ rate_limit: Optional[RateLimit] = {}
+ health_check: Optional[HealthCheck] = {}
+ app_protect: Optional[AppProtect] = {}
+ snippet: Optional[ObjectFromSourceOfTruth] = {}
+ authentication: Optional[LocationAuth] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'Location':
+ urimatch = self.urimatch
+ upstream = self.upstream
+
+ valid = ['prefix', 'exact', 'regex', 'iregex', 'best']
+ if urimatch not in valid:
+ raise ValueError(f"Invalid URI match type [{urimatch}] must be one of {str(valid)}")
+
+ prefixes = ["http://", "https://"]
+ if upstream != "" and not upstream.lower().startswith(tuple(prefixes)):
+ raise ValueError(f"Upstream must start with one of {str(prefixes)}")
+
+ return self
+
+
+class ObjectFromSourceOfTruth(BaseModel, extra="forbid"):
+ content: str = ""
+ authentication: Optional[List[LocationAuthServer]] = []
+
+class Server(BaseModel, extra="forbid"):
+ name: str
+ names: Optional[List[str]] = []
+ resolver: Optional[str] = ""
+ listen: Optional[Listen] = {}
+ log: Optional[Log] = {}
+ locations: Optional[List[Location]] = []
+ app_protect: Optional[AppProtect] = {}
+ snippet: Optional[ObjectFromSourceOfTruth] = {}
+
+
+class L4Server(BaseModel, extra="forbid"):
+ name: str
+ listen: Optional[ListenL4] = {}
+ upstream: Optional[str] = ""
+ snippet: Optional[ObjectFromSourceOfTruth] = {}
+
+
+class Sticky(BaseModel, extra="forbid"):
+ cookie: str = ""
+ expires: Optional[str] = ""
+ domain: Optional[str] = ""
+ path: Optional[str] = ""
+
+
+class Origin(BaseModel, extra="forbid"):
+ server: str
+ weight: Optional[int] = 1
+ max_fails: Optional[int] = 1
+ fail_timeout: Optional[str] = "10s"
+ max_conns: Optional[int] = 0
+ slow_start: Optional[str] = "0"
+ backup: Optional[bool] = False
+
+
+class L4Origin(BaseModel, extra="forbid"):
+ server: str
+ weight: Optional[int] = 1
+ max_fails: Optional[int] = 1
+ fail_timeout: Optional[str] = ""
+ max_conns: Optional[int] = 0
+ slow_start: Optional[str] = ""
+ backup: Optional[bool] = False
+
+
+class Upstream(BaseModel, extra="forbid"):
+ name: str
+ origin: Optional[List[Origin]] = []
+ sticky: Optional[Sticky] = {}
+ snippet: Optional[ObjectFromSourceOfTruth] = {}
+
+
+class L4Upstream(BaseModel, extra="forbid"):
+ name: str
+ origin: Optional[List[L4Origin]] = []
+ snippet: Optional[ObjectFromSourceOfTruth] = {}
+
+
+class ValidItem(BaseModel, extra="forbid"):
+ codes: Optional[List[int]] = [200]
+ ttl: Optional[str] = 60
+
+
+class CachingItem(BaseModel, extra="forbid"):
+ name: str
+ key: str
+ size: Optional[str] = "10m"
+ valid: Optional[List[ValidItem]] = []
+
+
+class RateLimitItem(BaseModel, extra="forbid"):
+ name: str
+ key: str
+ size: Optional[str] = ""
+ rate: Optional[str] = ""
+
+
+class NginxPlusApi(BaseModel, extra="forbid"):
+ write: Optional[bool] = False
+ listen: Optional[str] = ""
+ allow_acl: Optional[str] = ""
+
+
+class MapEntry(BaseModel, extra="forbid"):
+ key: str
+ keymatch: str
+ value: str
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'MapEntry':
+ keymatch = self.keymatch
+
+ valid = ['exact', 'regex', 'iregex']
+ if keymatch not in valid:
+ raise ValueError(f"Invalid key match type [{keymatch}] must be one of {str(valid)}")
+
+ return self
+
+
+class Map(BaseModel, extra="forbid"):
+ match: str
+ variable: str
+ entries: Optional[List[MapEntry]] = []
+
+
+class Layer4(BaseModel, extra="forbid"):
+ servers: Optional[List[L4Server]] = []
+ upstreams: Optional[List[L4Upstream]] = []
+
+
+class Authentication_Client(BaseModel, extra="forbid"):
+ name: str
+ type: str
+
+ jwt: Optional[AuthClientJWT] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'Authentication_Client':
+ _type, name = self.type, self.name
+
+ valid = ['jwt']
+ if _type not in valid:
+ raise ValueError(f"Invalid client authentication type [{_type}] for profile [{name}] must be one of {str(valid)}")
+
+ return self
+
+
+class Authentication_Server(BaseModel, extra="forbid"):
+ name: str
+ type: str
+
+ token: Optional[AuthServerToken] = {}
+
+ @model_validator(mode='after')
+ def check_type(self) -> 'Authentication_Server':
+ _type, name = self.type, self.name
+
+ valid = ['token']
+ if _type not in valid:
+ raise ValueError(f"Invalid server authentication type [{_type}] for profile [{name}] must be one of {str(valid)}")
+
+ return self
+
+
+class Authentication(BaseModel, extra="forbid"):
+ client: Optional[List[Authentication_Client]] = []
+ server: Optional[List[Authentication_Server]] = []
+
+
+class Http(BaseModel, extra="forbid"):
+ servers: Optional[List[Server]] = []
+ upstreams: Optional[List[Upstream]] = []
+ caching: Optional[List[CachingItem]] = []
+ rate_limit: Optional[List[RateLimitItem]] = []
+ nginx_plus_api: Optional[NginxPlusApi] = {}
+ maps: Optional[List[Map]] = []
+ snippet: Optional[ObjectFromSourceOfTruth] = {}
+ authentication: Optional[Authentication] = {}
+
+
+class Declaration(BaseModel, extra="forbid"):
+ layer4: Optional[Layer4] = {}
+ http: Optional[Http] = {}
+
+
+class API_Gateway(BaseModel, extra="forbid"):
+ enabled: Optional[bool] = False
+ strip_uri: Optional[bool] = False
+ server_url: Optional[str] = ""
+
+class DeveloperPortal(BaseModel, extra="forbid"):
+ enabled: Optional[bool] = False
+ uri: Optional[str] = "/devportal.html"
+
+class APIGateway(BaseModel, extra="forbid"):
+ openapi_schema: Optional[ObjectFromSourceOfTruth] = {}
+ api_gateway: Optional[API_Gateway] = {}
+ developer_portal: Optional[DeveloperPortal] = {}
+ rate_limit: Optional[List[RateLimitApiGw]] = []
+ authentication: Optional[APIGatewayAuthentication] = {}
+ log: Optional[Log] = {}
+
+
+class ConfigDeclaration(BaseModel, extra="forbid"):
+ output: Output
+ declaration: Optional[Declaration] = {}
diff --git a/src/main.py b/src/main.py
index 03037b1..3cc0002 100644
--- a/src/main.py
+++ b/src/main.py
@@ -22,6 +22,9 @@
import V4_0_CreateConfig
import V4_0_NginxConfigDeclaration
+import V4_1_CreateConfig
+import V4_1_NginxConfigDeclaration
+
cfg = NcgConfig.NcgConfig(configFile="../etc/config.toml")
redis = NcgRedis.NcgRedis(host=cfg.config['redis']['host'], port=cfg.config['redis']['port'])
@@ -83,6 +86,28 @@ def post_config_v4_0(d: V4_0_NginxConfigDeclaration.ConfigDeclaration, response:
return JSONResponse(content=response, status_code=output['status_code'], headers=headers)
+# Submit declaration using v4.1 API
+@app.post("/v4.1/config", status_code=200, response_class=PlainTextResponse)
+def post_config_v4_1(d: V4_1_NginxConfigDeclaration.ConfigDeclaration, response: Response):
+ output = V4_1_CreateConfig.createconfig(declaration=d, apiversion='v4.1')
+
+ if type(output) in [Response, str]:
+ # ConfigMap or plaintext response
+ return output
+
+ headers = output['message']['headers'] if 'headers' in output['message'] else {'Content-Type': 'application/json'}
+
+ if 'message' in output:
+ if 'message' in output['message']:
+ response = output['message']['message']
+ else:
+ response = output['message']
+ else:
+ response = output
+
+ return JSONResponse(content=response, status_code=output['status_code'], headers=headers)
+
+
# Modify declaration using v3.1 API
@app.patch("/v3.1/config/{configuid}", status_code=200, response_class=PlainTextResponse)
def patch_config_v3_1(d: V3_1_NginxConfigDeclaration.ConfigDeclaration, response: Response, configuid: str):
@@ -95,6 +120,12 @@ def patch_config_v4_0(d: V4_0_NginxConfigDeclaration.ConfigDeclaration, response
return V4_0_CreateConfig.patch_config(declaration=d, configUid=configuid, apiversion='v4.0')
+# Modify declaration using v4.1 API
+@app.patch("/v4.1/config/{configuid}", status_code=200, response_class=PlainTextResponse)
+def patch_config_v4_1(d: V4_1_NginxConfigDeclaration.ConfigDeclaration, response: Response, configuid: str):
+ return V4_1_CreateConfig.patch_config(declaration=d, configUid=configuid, apiversion='v4.1')
+
+
# Get declaration - v3.1 API
@app.get("/v3.1/config/{configuid}", status_code=200, response_class=PlainTextResponse)
def get_config_declaration_v3_1(configuid: str):
@@ -133,9 +164,29 @@ def get_config_declaration_v4_0(configuid: str):
)
+# Get declaration - v4.1 API
+@app.get("/v4.1/config/{configuid}", status_code=200, response_class=PlainTextResponse)
+def get_config_declaration_v4_1(configuid: str):
+ status_code, content = V4_1_CreateConfig.get_declaration(configUid=configuid)
+
+ if status_code == 404:
+ return JSONResponse(
+ status_code=404,
+ content={'code': 404, 'details': {'message': f'declaration {configuid} not found'}},
+ headers={'Content-Type': 'application/json'}
+ )
+
+ return JSONResponse(
+ status_code=200,
+ content=content,
+ headers={'Content-Type': 'application/json'}
+ )
+
+
# Get declaration status
@app.get("/v3.1/config/{configuid}/status", status_code=200, response_class=PlainTextResponse)
@app.get("/v4.0/config/{configuid}/status", status_code=200, response_class=PlainTextResponse)
+@app.get("/v4.1/config/{configuid}/status", status_code=200, response_class=PlainTextResponse)
def get_config_status(configuid: str):
status = redis.redis.get('ncg.status.' + configuid)
@@ -156,6 +207,7 @@ def get_config_status(configuid: str):
# Delete declaration
@app.delete("/v3.1/config/{configuid}", status_code=200, response_class=PlainTextResponse)
@app.delete("/v4.0/config/{configuid}", status_code=200, response_class=PlainTextResponse)
+@app.delete("/v4.1/config/{configuid}", status_code=200, response_class=PlainTextResponse)
def delete_config(configuid: str = ""):
if configuid not in redis.declarationsList:
return JSONResponse(
diff --git a/src/v4_1/APIGateway.py b/src/v4_1/APIGateway.py
new file mode 100644
index 0000000..0c87e87
--- /dev/null
+++ b/src/v4_1/APIGateway.py
@@ -0,0 +1,37 @@
+"""
+API Gateway support functions
+"""
+
+import json
+
+import v4_1.GitOps
+import v4_1.MiscUtils
+from v4_1.OpenAPIParser import OpenAPIParser
+
+# pydantic models
+from V4_1_NginxConfigDeclaration import *
+
+
+# Builds the declarative JSON for the API Gateway configuration
+# Return a tuple: status, description. If status = 200 things were successful
+def createAPIGateway(locationDeclaration: dict, authProfiles: Authentication={}):
+ apiGwDeclaration = {}
+
+ if locationDeclaration['apigateway']['openapi_schema']:
+ status, apiSchemaString = v4_1.GitOps.getObjectFromRepo(object=locationDeclaration['apigateway']['openapi_schema'],
+ authProfiles = authProfiles['server'] if 'server' in authProfiles else {}, base64Encode=False)
+
+ if v4_1.MiscUtils.yaml_or_json(apiSchemaString['content']) == 'yaml':
+ # YAML to JSON conversion
+ apiSchemaString['content'] = v4_1.MiscUtils.yaml_to_json(apiSchemaString['content'])
+
+ apiSchema = OpenAPIParser(json.loads(apiSchemaString['content']))
+
+ apiGwDeclaration = {}
+ apiGwDeclaration['location'] = locationDeclaration
+ apiGwDeclaration['info'] = apiSchema.info()
+ apiGwDeclaration['servers'] = apiSchema.servers()
+ apiGwDeclaration['paths'] = apiSchema.paths()
+ apiGwDeclaration['version'] = apiSchema.version()
+
+ return 200, apiGwDeclaration
\ No newline at end of file
diff --git a/src/v4_1/DeclarationPatcher.py b/src/v4_1/DeclarationPatcher.py
new file mode 100644
index 0000000..6e27285
--- /dev/null
+++ b/src/v4_1/DeclarationPatcher.py
@@ -0,0 +1,238 @@
+"""
+Declaration parsing functions
+"""
+
+
+# Returns the patched declaration based on the patchedHttpServer
+def patchHttpServer(sourceDeclaration: dict, patchedHttpServer: dict):
+ allTargetServers = []
+
+ haveWePatched = False
+
+ if 'declaration' not in sourceDeclaration:
+ sourceDeclaration['declaration'] = {}
+
+ if 'http' not in sourceDeclaration['declaration']:
+ sourceDeclaration['declaration']['http'] = {}
+
+ if 'servers' not in sourceDeclaration['declaration']['http']:
+ sourceDeclaration['declaration']['http']['servers'] = []
+
+ # HTTP server patch
+ for s in sourceDeclaration['declaration']['http']['servers']:
+ if s['name'] == patchedHttpServer['name']:
+ # Patching an existing HTTP server, 'name' is the key
+ if len(patchedHttpServer) > 1:
+ # Patching HTTP server specifying only 'name' (len == 1) means delete
+ # If further fields are specified HTTP server is patched
+ allTargetServers.append(patchedHttpServer)
+
+ haveWePatched = True
+ else:
+ # Unmodified HTTP server
+ allTargetServers.append(s)
+
+ if not haveWePatched:
+ # The HTTP server being patched is a new one, let's add it
+ allTargetServers.append(patchedHttpServer)
+
+ sourceDeclaration['declaration']['http']['servers'] = allTargetServers
+
+ return sourceDeclaration
+
+
+# Returns the patched declaration based on the patchedHttpUpstream
+def patchHttpUpstream(sourceDeclaration: dict, patchedHttpUpstream: dict):
+ allTargetUpstreams = []
+
+ haveWePatched = False
+
+ if 'declaration' not in sourceDeclaration:
+ sourceDeclaration['declaration'] = {}
+
+ if 'http' not in sourceDeclaration['declaration']:
+ sourceDeclaration['declaration']['http'] = {}
+
+ if 'upstreams' not in sourceDeclaration['declaration']['http']:
+ sourceDeclaration['declaration']['http']['upstreams'] = []
+
+ # HTTP upstreams patch
+ for s in sourceDeclaration['declaration']['http']['upstreams']:
+ if s['name'] == patchedHttpUpstream['name']:
+ # Patching an existing HTTP upstream, 'name' is the key
+ if len(patchedHttpUpstream) > 1:
+ # Patching HTTP upstream specifying only 'name' (len == 1) means delete
+ # If further fields are specified HTTP upstream is patched
+ allTargetUpstreams.append(patchedHttpUpstream)
+
+ haveWePatched = True
+ else:
+ # Unmodified HTTP upstream
+ allTargetUpstreams.append(s)
+
+ if not haveWePatched:
+ # The HTTP upstream being patched is a new one, let's add it
+ allTargetUpstreams.append(patchedHttpUpstream)
+
+ sourceDeclaration['declaration']['http']['upstreams'] = allTargetUpstreams
+
+ return sourceDeclaration
+
+
+# Returns the patched declaration based on the patchedStreamServer
+def patchStreamServer(sourceDeclaration: dict, patchedStreamServer: dict):
+ allTargetServers = []
+
+ haveWePatched = False
+
+ if 'declaration' not in sourceDeclaration:
+ sourceDeclaration['declaration'] = {}
+
+ if 'layer4' not in sourceDeclaration['declaration']:
+ sourceDeclaration['declaration']['layer4'] = {}
+
+ if 'servers' not in sourceDeclaration['declaration']['layer4']:
+ sourceDeclaration['declaration']['layer4']['servers'] = []
+
+ # HTTP server patch
+ for s in sourceDeclaration['declaration']['layer4']['servers']:
+ if s['name'] == patchedStreamServer['name']:
+ # Patching an existing Stream server, 'name' is the key
+ if len(patchedStreamServer) > 1:
+ # Patching Stream server specifying only 'name' (len == 1) means delete
+ # If further fields are specified HTTP server is patched
+ allTargetServers.append(patchedStreamServer)
+
+ haveWePatched = True
+ else:
+ # Unmodified HTTP server
+ allTargetServers.append(s)
+
+ if not haveWePatched:
+ # The Stream server being patched is a new one, let's add it
+ allTargetServers.append(patchedStreamServer)
+
+ sourceDeclaration['declaration']['layer4']['servers'] = allTargetServers
+
+ return sourceDeclaration
+
+
+# Returns the patched declaration based on the patchedStreamUpstream
+def patchStreamUpstream(sourceDeclaration: dict, patchedStreamUpstream: dict):
+ allTargetUpstreams = []
+
+ haveWePatched = False
+
+ if 'declaration' not in sourceDeclaration:
+ sourceDeclaration['declaration'] = {}
+
+ if 'layer4' not in sourceDeclaration['declaration']:
+ sourceDeclaration['declaration']['layer4'] = {}
+
+ if 'upstreams' not in sourceDeclaration['declaration']['layer4']:
+ sourceDeclaration['declaration']['layer4']['upstreams'] = []
+
+ # HTTP upstreams patch
+ for s in sourceDeclaration['declaration']['layer4']['upstreams']:
+ if s['name'] == patchedStreamUpstream['name']:
+ # Patching an existing Stream upstream, 'name' is the key
+ if len(patchedStreamUpstream) > 1:
+ # Patching Stream upstream specifying only 'name' (len == 1) means delete
+ # If further fields are specified HTTP upstream is patched
+ allTargetUpstreams.append(patchedStreamUpstream)
+
+ haveWePatched = True
+ else:
+ # Unmodified HTTP upstream
+ allTargetUpstreams.append(s)
+
+ if not haveWePatched:
+ # The Stream upstream being patched is a new one, let's add it
+ allTargetUpstreams.append(patchedStreamUpstream)
+
+ sourceDeclaration['declaration']['layer4']['upstreams'] = allTargetUpstreams
+
+ return sourceDeclaration
+
+
+# Returns the patched declaration based on the patchedNAPPolicies
+def patchNAPPolicies(sourceDeclaration: dict, patchedNAPPolicies: dict):
+ allTargetPolicies = []
+
+ haveWePatched = False
+
+ if 'output' not in sourceDeclaration:
+ return sourceDeclaration
+
+ if 'nms' not in sourceDeclaration['output']:
+ return sourceDeclaration
+
+ if 'policies' not in sourceDeclaration['output']['nms']:
+ return sourceDeclaration
+
+ # NGINX App Protect WAF policies patch
+ for p in sourceDeclaration['output']['nms']['policies']:
+ if 'type' in p and p['type'] == 'app_protect' \
+ and 'name' in p and p['name'] \
+ and p['type'] == patchedNAPPolicies['type'] \
+ and p['name'] == patchedNAPPolicies['name']:
+
+ # Patching an existing NGINX App Protect WAF policy, 'name' is the key
+ if patchedNAPPolicies['versions'] and patchedNAPPolicies['active_tag']:
+ # Patching NAP policy specifying 'versions' and 'active_tag' means updating
+ # If 'versions' and 'active_tag' are missing then it's a deletion
+ allTargetPolicies.append(patchedNAPPolicies)
+
+ haveWePatched = True
+ else:
+ # Unmodified HTTP upstream
+ allTargetPolicies.append(p)
+
+ if not haveWePatched:
+ # The NAP policy being patched is a new one, let's add it
+ allTargetPolicies.append(patchedNAPPolicies)
+
+ sourceDeclaration['output']['nms']['policies'] = allTargetPolicies
+
+ return sourceDeclaration
+
+
+# Returns the patched declaration based on patchedCertificates
+def patchCertificates(sourceDeclaration: dict, patchedCertificates: dict):
+ allTargetCertificates = []
+
+ haveWePatched = False
+
+ if 'output' not in sourceDeclaration:
+ return sourceDeclaration
+
+ if 'nms' not in sourceDeclaration['output']:
+ return sourceDeclaration
+
+ if 'certificates' not in sourceDeclaration['output']['nms']:
+ return sourceDeclaration
+
+ # TLS certificates patch
+ for c in sourceDeclaration['output']['nms']['certificates']:
+ if 'type' in c and c['type'] in ['certificate', 'key', 'chain'] \
+ and 'name' in c and c['name'] \
+ and c['type'] == patchedCertificates['type'] \
+ and c['name'] == patchedCertificates['name']:
+
+ if 'contents' in c and c['contents']:
+ # Patching an existing TLS certificate/key/chain, 'name' is the key.
+ # If content is empty the certificate is deleted
+ allTargetCertificates.append(patchedCertificates)
+
+ haveWePatched = True
+ else:
+ # Unmodified HTTP upstream
+ allTargetCertificates.append(c)
+
+ if not haveWePatched:
+ # The TLS certificate/key/chain being patched is a new one, let's add it
+ allTargetCertificates.append(patchedCertificates)
+
+ sourceDeclaration['output']['nms']['certificates'] = allTargetCertificates
+
+ return sourceDeclaration
diff --git a/src/v4_1/DevPortal.py b/src/v4_1/DevPortal.py
new file mode 100644
index 0000000..7f508cc
--- /dev/null
+++ b/src/v4_1/DevPortal.py
@@ -0,0 +1,46 @@
+"""
+API Gateway Developer Portal support functions
+"""
+
+import json
+import requests
+import base64
+
+# NGINX Declarative API modules
+from NcgConfig import NcgConfig
+import v4_1.GitOps
+import v4_1.MiscUtils
+
+# pydantic models
+from V4_1_NginxConfigDeclaration import *
+
+def buildDevPortal(openapischema):
+ try:
+ response = requests.post(f"http://{NcgConfig.config['devportal']['host']}:"
+ f"{NcgConfig.config['devportal']['port']}{NcgConfig.config['devportal']['uri']}",
+ headers={'Content-Type': 'application/json'}, data=openapischema)
+ except Exception as e:
+ return 400, ""
+
+ return response.status_code, json.loads(response.text)
+
+
+# Builds the declarative JSON for the API Gateway configuration
+# Return a tuple: status, description. If status = 200 things were successful
+def createDevPortal(locationDeclaration: dict, authProfiles: Authentication={}):
+ if locationDeclaration['apigateway']['openapi_schema']:
+ status, apiSchemaString = v4_1.GitOps.getObjectFromRepo(
+ object = locationDeclaration['apigateway']['openapi_schema'], authProfiles = authProfiles['server'] if 'server' in authProfiles else {}, base64Encode = False)
+
+ if v4_1.MiscUtils.yaml_or_json(apiSchemaString['content']) == 'yaml':
+ # YAML to JSON conversion
+ status, devportalJSON = buildDevPortal(openapischema = v4_1.MiscUtils.yaml_to_json(apiSchemaString['content']))
+ else:
+ status, devportalJSON = buildDevPortal(openapischema = apiSchemaString['content'])
+
+ if status == 200:
+ devportalHTML = base64.b64encode(bytes(devportalJSON['devportal'], 'utf-8')).decode('utf-8')
+ else:
+ devportalHTML = ""
+
+ return status, devportalHTML
\ No newline at end of file
diff --git a/src/v4_1/GitOps.py b/src/v4_1/GitOps.py
new file mode 100644
index 0000000..5a1be5b
--- /dev/null
+++ b/src/v4_1/GitOps.py
@@ -0,0 +1,64 @@
+"""
+GitOps support functions
+"""
+
+import base64
+import requests
+
+from requests import ReadTimeout, HTTPError, Timeout, ConnectionError, ConnectTimeout
+from typing import List
+
+# pydantic models
+from V4_1_NginxConfigDeclaration import *
+
+
+# Fetches a URL content
+def __fetchfromsourceoftruth__(url, headers = {} ):
+ # Object is fetched from external repository
+ try:
+ reply = requests.get(url = url, headers = headers, verify=False)
+ except (ConnectTimeout, HTTPError, ReadTimeout, Timeout, ConnectionError):
+ return 408, "URL " + url + " unreachable"
+
+ return reply.status_code, reply.text
+
+
+# If content starts with http(s):// fetches the object and return it b64-encoded by default.
+# base64Encode to be set to False to disable b64 encoding
+# Returns the status original content otherwise.
+# Return is a tuple: status_code, content
+def getObjectFromRepo(object: ObjectFromSourceOfTruth, authProfiles: Authentication={}, base64Encode: bool=True):
+ status_code = 200
+ response = object
+
+ if object:
+ if object['content'].lower().startswith(("http://","https://")):
+ # Object is fetched from external repository
+ headers = {}
+
+ # Set server authentication if needed
+ if authProfiles and 'server' in authProfiles:
+ for authP in authProfiles['server']:
+ if object['authentication'][0]['profile'] == authP['name']:
+ # Sets up authentication
+ if authP['type'].lower() == 'token':
+ authToken = authP['token']['token']
+ authTokenType = authP['token']['type']
+ authTokenLocation = authP['token']['location']
+
+ if authTokenType.lower() == 'bearer':
+ headers['Authorization'] = f"Bearer {authToken}"
+ elif authTokenType.lower() == 'header':
+ headers[authTokenLocation] = authToken
+
+ status_code, fetchedContent = __fetchfromsourceoftruth__(url = object['content'], headers = headers)
+
+ if status_code == 200:
+ if base64Encode == True:
+ fetchedContent = base64.b64encode(bytes(fetchedContent, 'utf-8')).decode('utf-8')
+ else:
+ fetchedContent = bytes(fetchedContent, 'utf-8').decode("utf-8")
+
+ response['content'] = fetchedContent
+
+ return status_code, response
\ No newline at end of file
diff --git a/src/v4_1/MiscUtils.py b/src/v4_1/MiscUtils.py
new file mode 100644
index 0000000..a5ea547
--- /dev/null
+++ b/src/v4_1/MiscUtils.py
@@ -0,0 +1,44 @@
+"""
+Support functions
+"""
+
+import re
+import json
+import yaml
+
+def getDictKey(_dict: dict, key_lookup: str, separator='.'):
+ """
+ Searches for a nested key in a dictionary and returns its value, or None if nothing was found.
+ key_lookup must be a string where each key is deparated by a given "separator" character, which by default is a dot
+ """
+ keys = key_lookup.split(separator)
+ subdict = _dict
+
+ for k in keys:
+ subdict = subdict[k] if k in subdict else None
+ if subdict is None:
+ return None
+
+ return subdict
+
+"""
+Jinja2 regexp filter
+"""
+def regex_replace(s, find, replace):
+ return re.sub(find, replace, s)
+
+"""
+JSON/YAML detection
+"""
+def yaml_or_json(document: str):
+ try:
+ json.load(document)
+ return 'json'
+ except Exception:
+ return 'yaml'
+
+"""
+YAML to JSON conversion
+"""
+def yaml_to_json(document: str):
+ return json.dumps(yaml.safe_load(document))
\ No newline at end of file
diff --git a/src/v4_1/NAPUtils.py b/src/v4_1/NAPUtils.py
new file mode 100644
index 0000000..bdfcea8
--- /dev/null
+++ b/src/v4_1/NAPUtils.py
@@ -0,0 +1,269 @@
+"""
+NGINX App Protect support functions
+"""
+
+import requests
+import json
+
+import v4_1.GitOps
+
+from fastapi.responses import Response, JSONResponse
+
+available_log_profiles = ['log_all', 'log_blocked', 'log_illegal', 'secops_dashboard']
+
+
+# Define (create/update) a NGINX App Protect policy on NMS.
+# If policyUid is not empty a the policy update is performed
+# Returns a tuple {status_code,text}. status_code is 201 if successful
+def __definePolicyOnNMS__(nmsUrl: str, nmsUsername: str, nmsPassword: str, policyName: str, policyDisplayName: str,
+ policyDescription: str, policyJson: str, policyUid: str = ""):
+ # policyBody holds the base64-encoded policy JSON definition
+ # Control plane-compiled policy bundles are supported. Create the NGINX App Protect policy on NMS
+ # POST https://{{nms_host}}/api/platform/v1/security/policies
+ # {
+ # "metadata": {
+ # "name": "prod-policy",
+ # "displayName": "Production Policy - blocking",
+ # "description": "Production-ready policy - blocking"
+ # },
+ # "content": ""
+ # }
+
+ policyCreationPayload = {'metadata': {}}
+ policyCreationPayload['metadata']['name'] = policyName
+ policyCreationPayload['metadata']['displayName'] = policyDisplayName
+ policyCreationPayload['metadata']['description'] = policyDescription
+ policyCreationPayload['content'] = policyJson
+
+ if policyUid != "":
+ # Existing policy update
+ r = requests.put(url=f"{nmsUrl}/api/platform/v1/security/policies/{policyUid}",
+ data=json.dumps(policyCreationPayload),
+ headers={'Content-Type': 'application/json'},
+ auth=(nmsUsername, nmsPassword),
+ verify=False)
+ else:
+ # New policy creation - first try to create it as a new revision for an existing policy
+ # The response code is 201 if successful and 404 if there is no policy with the given name
+ r = requests.post(url=f"{nmsUrl}/api/platform/v1/security/policies?isNewRevision=true",
+ data=json.dumps(policyCreationPayload),
+ headers={'Content-Type': 'application/json'},
+ auth=(nmsUsername, nmsPassword),
+ verify=False)
+
+ # Check if this is a new policy with no existing versions. If this is true create its initial version
+ if r.status_code == 404:
+ r = requests.post(url=f"{nmsUrl}/api/platform/v1/security/policies",
+ data=json.dumps(policyCreationPayload),
+ headers={'Content-Type': 'application/json'},
+ auth=(nmsUsername, nmsPassword),
+ verify=False)
+
+ return r
+
+
+# Retrieve security policies information
+def __getAllPolicies__(nmsUrl: str, nmsUsername: str, nmsPassword: str):
+ return requests.get(url=f'{nmsUrl}/api/platform/v1/security/policies',
+ auth=(nmsUsername, nmsPassword), verify=False)
+
+
+# Delete security policy from control plane
+def __deletePolicy__(nmsUrl: str, nmsUsername: str, nmsPassword: str, policyUid: str):
+ return requests.delete(url=f'{nmsUrl}/api/platform/v1/security/policies/{policyUid}',
+ auth=(nmsUsername, nmsPassword), verify=False)
+
+
+# Check NAP policies names validity for the given declaration
+# Return a tuple: status, description. If status = 200 checks were successful
+def checkDeclarationPolicies(declaration: dict):
+ # NGINX App Protect policies check - duplicated policy names
+
+ # all policy names as defined in the declaration
+ # { 'policyName': 'activeTag', ... }
+ allPolicyNames = {}
+
+ if 'policies' not in declaration['output']['nms']:
+ return 200, ""
+
+ for policy in declaration['output']['nms']['policies']:
+ # print(f"Found NAP Policy [{policy['name']}] active tag [{policy['active_tag']}]")
+
+ if policy['name'] and policy['name'] in allPolicyNames:
+ return 422, f"Duplicated NGINX App Protect WAF policy [{policy['name']}]"
+
+ allPolicyNames[policy['name']] = policy['active_tag']
+
+ # Check policy releases for non-univoque tags
+ allPolicyVersionTags = {}
+ for policyVersion in policy['versions']:
+ # print(f"--> Policy [{policy['name']}] tag [{policyVersion['tag']}]")
+ if policyVersion['tag'] and policyVersion['tag'] in allPolicyVersionTags:
+ return 422, f"Duplicated NGINX App Protect WAF policy tag [{policyVersion['tag']}] " \
+ f"for policy [{policy['name']}]"
+
+ allPolicyVersionTags[policyVersion['tag']] = "found"
+
+ if policy['active_tag'] and policy['active_tag'] not in allPolicyVersionTags:
+ return 422, f"Invalid active tag [{policy['active_tag']}] for policy [{policy['name']}]"
+
+ # Check policy names referenced by the declaration inside HTTP servers[]: they must be valid
+ if 'http' in declaration['declaration'] and 'servers' in declaration['declaration']['http']:
+ for httpServer in declaration['declaration']['http']['servers']:
+ if 'app_protect' in httpServer:
+ if 'policy' in httpServer['app_protect'] and httpServer['app_protect']['policy'] \
+ and httpServer['app_protect']['policy'] not in allPolicyNames:
+ return 422, f"Unknown NGINX App Protect WAF policy [{httpServer['app_protect']['policy']}] " \
+ f"referenced by HTTP server [{httpServer['name']}]"
+
+ if 'log' in httpServer['app_protect'] \
+ and 'profile_name' in httpServer['app_protect']['log'] \
+ and httpServer['app_protect']['log']['profile_name'] \
+ and httpServer['app_protect']['log']['profile_name'] \
+ not in available_log_profiles:
+ return 422, f"Invalid NGINX App Protect WAF log profile " \
+ f"[{httpServer['app_protect']['log']['profile_name']}] referenced by HTTP server " \
+ f"[{httpServer['name']}]"
+
+ # Check policy names referenced in HTTP servers[].locations[]
+ for location in httpServer['locations']:
+ if 'app_protect' in location:
+ if 'policy' in location['app_protect'] and location['app_protect']['policy'] \
+ and location['app_protect']['policy'] not in allPolicyNames:
+ return 422, f"Unknown NGINX App Protect WAF policy [{location['app_protect']['policy']}] " \
+ f"referenced by HTTP server [{httpServer['name']}] location [{location['uri']}]"
+
+ if 'log' in httpServer['app_protect'] and httpServer['app_protect']['log'] \
+ and httpServer['app_protect']['log']['profile_name'] \
+ and httpServer['app_protect']['log']['profile_name'] \
+ not in available_log_profiles:
+ return 422, f"Invalid NGINX App Protect WAF log profile " \
+ f"[{httpServer['app_protect']['log']['profile_name']}] referenced by HTTP server " \
+ f"[{httpServer['name']}] location [{location['uri']}]"
+
+ return 200, ""
+
+
+# For the given declaration creates/updates NGINX App Protect WAF policies on NGINX Management Suite
+# making sure that they are in sync with what is defined in the JSON declaration
+# Returns a tuple with two dictionaries: all_policy_names_and_versions, all_policy_active_names_and_uids
+def provisionPolicies(nmsUrl: str, nmsUsername: str, nmsPassword: str, declaration: dict):
+ # NGINX App Protect policies - each policy supports multiple tagged versions
+
+ # Policy names and all tag/uid pairs
+ # {'prod-policy': [{'tag': 'v1', 'uid': 'ebcf9c7e-0930-450d-8108-7cad30e59661'},
+ # {'tag': 'v2', 'uid': 'd18c2eb7-814e-4e4d-90fc-54014eef199e'}],
+ # 'staging-policy': [{'tag': 'block', 'uid': '9794faa7-5b6c-4ce5-9e68-946f04766bb4'},
+ # {'tag': 'xss-ok', 'uid': '7b4b850a-ff9e-42a0-85d0-850171474224'}]}
+ all_policy_names_and_versions = {}
+
+ # Policy names and active tag uids
+ # { 'prod-policy': 'ebcf9c7e-0930-450d-8108-7cad30e59661',
+ # 'staging-policy': '7b4b850a-ff9e-42a0-85d0-850171474224' }
+ all_policy_active_names_and_uids = {}
+
+ for p in declaration['output']['nms']['policies']:
+ policy_name = p['name']
+ if policy_name:
+ policy_active_tag = p['active_tag']
+
+ # Iterates over all NGINX App Protect policies
+ if p['type'] == 'app_protect':
+ # Iterates over all policy versions
+ for policyVersion in p['versions']:
+ status, policyBody = v4_1.GitOps.getObjectFromRepo(policyVersion['contents'])
+
+ if status != 200:
+ return JSONResponse(
+ status_code=422,
+ content={"code": status,
+ "details": policyBody['content']}
+ )
+
+ # Create the NGINX App Protect policy on NMS
+ r = __definePolicyOnNMS__(
+ nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword,
+ policyName=policy_name,
+ policyDisplayName=policyVersion['displayName'],
+ policyDescription=policyVersion['description'],
+ policyJson=policyBody['content']
+ )
+
+ # Check for errors creating NGINX App Protect policy
+ if r.status_code != 201:
+ return JSONResponse(
+ status_code=r.status_code,
+ content={"code": r.status_code, "details": json.loads(r.text)}
+ )
+ else:
+ # Policy was created, retrieve metadata.uid for each policy version
+ if policy_name not in all_policy_names_and_versions:
+ all_policy_names_and_versions[policy_name] = []
+
+ # Stores the policy version
+ uid = json.loads(r.text)['metadata']['uid']
+ tag = policyVersion['tag']
+
+ if policy_active_tag == tag:
+ all_policy_active_names_and_uids[policy_name] = uid
+
+ all_policy_names_and_versions[policy_name].append({'tag': tag, 'uid': uid})
+
+ return all_policy_names_and_versions, all_policy_active_names_and_uids
+
+
+# Publish a NGINX App Protect WAF policy making it active
+# activePolicyUids is a dict { "policy_name": "active_uid", [...] }
+# Return True if at least one policy was enabled, False otherwise
+def makePolicyActive(nmsUrl: str, nmsUsername: str, nmsPassword: str, activePolicyUids: dict, instanceGroupUid: str):
+ doWeHavePolicies = False
+
+ for policyName in activePolicyUids:
+ body = {
+ "publications": [
+ {
+ "policyContent": {
+ "name": f'{policyName}',
+ "uid": f'{activePolicyUids[policyName]}'
+ },
+ "instanceGroups": [
+ f'{instanceGroupUid}'
+ ]
+ }
+ ]
+ }
+
+ doWeHavePolicies = True
+ r = requests.post(url=f'{nmsUrl}/api/platform/v1/security/publish', auth=(nmsUsername, nmsPassword),
+ data=json.dumps(body), headers={'Content-Type': 'application/json'}, verify=False)
+
+ return doWeHavePolicies
+
+
+# For the given declaration creates/updates NGINX App Protect WAF policies on NGINX Management Suite
+# making sure that they are in sync with what is defined in the JSON declaration
+# Returns a tuple: status, response payload
+def cleanPolicyLeftovers(nmsUrl: str, nmsUsername: str, nmsPassword: str, currentPolicies: dict):
+ # Fetch all policies currently defined on the control plane
+ allNMSPolicies = __getAllPolicies__(nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword)
+ allNMSPoliciesJson = json.loads(allNMSPolicies.text)
+
+ # Build a list of all uids for policies currently available on the control plane whose names match
+ # currentPolicies (policies that have just been pushed to data plane)
+ allUidsOnNMS = []
+ for p in allNMSPoliciesJson['items']:
+ if p['metadata']['name'] in currentPolicies:
+ allUidsOnNMS.append(p['metadata']['uid'])
+
+ allCurrentPoliciesUIDs = []
+ for policyName in currentPolicies:
+ if policyName:
+ for tag in currentPolicies[policyName]:
+ allCurrentPoliciesUIDs.append(tag['uid'])
+
+ uidsToRemove = list(set(allUidsOnNMS) - set(allCurrentPoliciesUIDs))
+
+ for uid in uidsToRemove:
+ __deletePolicy__(nmsUrl=nmsUrl, nmsUsername=nmsUsername, nmsPassword=nmsPassword, policyUid=uid)
+
+ return
diff --git a/src/v4_1/NIMUtils.py b/src/v4_1/NIMUtils.py
new file mode 100644
index 0000000..8472ba7
--- /dev/null
+++ b/src/v4_1/NIMUtils.py
@@ -0,0 +1,30 @@
+"""
+NGINX Instance Manager support functions
+"""
+
+import requests
+import json
+
+import v4_1.GitOps
+
+from fastapi.responses import Response, JSONResponse
+
+
+# Fetch an instance group UID from NGINX Instance Manager
+# Return None if not found
+def getNIMInstanceGroupUid(nmsUrl: str, nmsUsername: str, nmsPassword: str, instanceGroupName: str):
+ # Retrieve instance group uid
+ ig = requests.get(url=f'{nmsUrl}/api/platform/v1/instance-groups', auth=(nmsUsername, nmsPassword),
+ verify=False)
+
+ if ig.status_code != 200:
+ return None
+
+ # Get the instance group id
+ igUid = None
+ igJson = json.loads(ig.text)
+ for i in igJson['items']:
+ if i['name'] == instanceGroupName:
+ igUid = i['uid']
+
+ return igUid
diff --git a/src/v4_1/OpenAPIParser.py b/src/v4_1/OpenAPIParser.py
new file mode 100644
index 0000000..0e4e355
--- /dev/null
+++ b/src/v4_1/OpenAPIParser.py
@@ -0,0 +1,71 @@
+"""
+OpenAPI schema parser support functions
+"""
+
+import json
+
+class OpenAPIParser:
+ httpMethods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']
+
+ def __init__(self, openAPISchema):
+ self.openAPISchema = openAPISchema
+
+ def version(self):
+ if 'openapi' in self.openAPISchema:
+ return self.openAPISchema['openapi']
+ elif 'swagger' in self.openAPISchema:
+ return self.openAPISchema['swagger']
+
+ return None
+
+ def info(self):
+ return self.openAPISchema['info']
+
+ def servers(self):
+ self.allServers = []
+
+ # Loop over OpenAPI schema servers
+ if 'servers' in self.openAPISchema:
+ for server in self.openAPISchema['servers']:
+ urlName = server['url']
+ self.s = {}
+ self.s['url'] = urlName
+
+ if 'description' in server:
+ self.s['description'] = server['description']
+
+ self.allServers.append(self.s)
+
+ return self.allServers
+
+ def paths(self):
+ self.allPaths = []
+
+ # Loop over OpenAPI schema paths
+ if 'paths' in self.openAPISchema:
+ for path in self.openAPISchema['paths'].keys():
+ #print(f"- {path}")
+ self.p = {}
+ self.p['path'] = path
+ self.p['methods'] = []
+
+ # Loop over path HTTP methods found in schema
+ for method in self.openAPISchema['paths'][path].keys():
+ methodInfo = self.openAPISchema['paths'][path][method]
+
+ if method.upper() in self.httpMethods:
+ #print(f" - {method} - {methodInfo['description'] if 'description' in methodInfo else ''}")
+ self.m = {}
+ self.m['method'] = method
+ self.m['details'] = {}
+
+ if 'description' in methodInfo and methodInfo['description']:
+ self.m['details']['description'] = methodInfo['description']
+ if 'operationId' in methodInfo and methodInfo['operationId']:
+ self.m['details']['operationId'] = methodInfo['operationId']
+
+ self.p['methods'].append(self.m)
+
+ self.allPaths.append(self.p)
+
+ return self.allPaths
\ No newline at end of file
diff --git a/templates/v4.1/apigateway.tmpl b/templates/v4.1/apigateway.tmpl
new file mode 100644
index 0000000..9a0bd2a
--- /dev/null
+++ b/templates/v4.1/apigateway.tmpl
@@ -0,0 +1,110 @@
+{% if declaration.servers %}
+ {# --- OpenAPI schema contains server details --- #}
+ {% if declaration.servers[0].url.lower().startswith('http://') or declaration.servers[0].url.lower().startswith('https://') %}
+ {# --- OpenAPI schema contains a full server URL --- #}
+ {% set destination_server = declaration.servers[0].url %}
+ {% else %}
+ {# --- OpenAPI schema contains a server URI --- #}
+ {% set destination_server = declaration.location.apigateway.api_gateway.server_url + declaration.servers[0].url %}
+ {% endif %}
+{% else %}
+ {# --- OpenAPI schema contains no server details --- #}
+ {% set destination_server = declaration.location.apigateway.api_gateway.server_url %}
+{% endif %}
+
+# API Gateway: {{ declaration.info.title }} {{ declaration.info.version }}
+# OpenAPI version: {{ declaration.version }}
+# Base URI: {{ declaration.location.uri }}
+# Strip base URI: {{ declaration.location.apigateway.api_gateway.strip_uri }}
+# Destination server: {{ destination_server }}
+
+{% if declaration.paths -%}
+{% for path in declaration.paths %}
+location {% if '{' not in path.path %}={% else %}~{% endif %} {{ declaration.location.uri }}{{ path.path | regex_replace('{(.*?)}','(.*)') }} {
+ {% for method in path.methods -%}
+ # {{ method.method|upper }} - operationId: {{ method.details.operationId }}
+ {% endfor -%}
+ {% set method_names = path.methods|map(attribute='method')|list %}
+
+ {% if declaration.location.apigateway.log.access %}access_log {{ declaration.location.apigateway.log.access }} main;{% endif %}
+
+ {% if declaration.location.apigateway.log.error %}error_log {{ declaration.location.apigateway.log.error }};{% endif %}
+
+
+ limit_except {{ method_names|join(' ')|upper }} { deny all; }
+
+ {# --- Rate limiting start --- #}
+ {%- for rl in declaration.location.apigateway.rate_limit -%}
+ {%- set enforceRL = namespace(toBeEnforced = False) -%}
+ {%- if rl.enforceOnPaths == False -%}
+ {%- set enforceRL.toBeEnforced = True -%}
+ {%- endif -%}
+ {%- for rlPath in rl.paths -%}
+ {%- if path.path == rlPath -%}
+ {%- if rl.enforceOnPaths == True -%}
+ {%- set enforceRL.toBeEnforced = True -%}
+ {%- else -%}
+ {%- set enforceRL.toBeEnforced = False -%}
+ {%- endif -%}
+ {%- endif -%}
+ {%- endfor -%}
+
+ {%- if enforceRL.toBeEnforced == True -%}
+ {%- if rl.profile %}limit_req zone={{ rl.profile }}{% if rl.burst %} burst={{ rl.burst }}{% endif %}{% if rl.delay == 0 %} nodelay;{% else %} delay={{ rl.delay }};{% endif %}{% endif %}
+
+ {% if rl.httpcode %}limit_req_status {{ rl.httpcode }};{% endif %}
+ {%- endif -%}
+ {%- endfor -%}
+
+ {# --- Rate limiting end --- #}
+
+
+ {# --- Authentication start --- #}
+ {%- if declaration.location.apigateway.authentication -%}
+ {%- set enforceAuth = namespace(toBeEnforced = False) -%}
+ {%- if declaration.location.apigateway.authentication.enforceOnPaths == False -%}
+ {%- set enforceAuth.toBeEnforced = True -%}
+ {%- endif -%}
+ {%- for authPath in declaration.location.apigateway.authentication.paths -%}
+ {%- if path.path == authPath -%}
+ {%- if declaration.location.apigateway.authentication.enforceOnPaths == True -%}
+ {%- set enforceAuth.toBeEnforced = True -%}
+ {%- else -%}
+ {%- set enforceRL.toBeEnforced = False -%}
+ {%- endif -%}
+ {%- endif -%}
+ {%- endfor -%}
+
+ {# --- Client authentication --- #}
+ {%- if enforceAuth.toBeEnforced == True -%}
+ {%- if declaration.location.apigateway.authentication and declaration.location.apigateway.authentication.client -%}
+ {%- for clientAuthProfile in declaration.location.apigateway.authentication.client -%}
+ include "{{ ncgconfig.nms.auth_client_dir }}/{{ clientAuthProfile.profile | replace(" ", "_") }}.conf";
+ {% endfor -%}
+ {%- endif -%}
+ {%- endif -%}
+
+ {%- endif %}
+
+ {# --- Authentication end --- #}
+
+ {% if declaration.location.apigateway.api_gateway.strip_uri -%}
+ rewrite ^{{ declaration.location.uri }}/(.*)$ /$1 break;
+ {% endif %}
+
+ {% if declaration.location.apigateway.api_gateway.server_url -%}
+ proxy_set_header Host {{ declaration.location.apigateway.api_gateway.server_url.split('://')[1].split('/')[0] }};
+ {% endif -%}
+
+ proxy_pass {{ destination_server }}$uri;
+}
+
+{% endfor %}
+
+{% if declaration.location.apigateway.developer_portal.enabled == True -%}
+location = {{ declaration.location.uri }}{{ declaration.location.apigateway.developer_portal.uri }} {
+ rewrite ^{{ declaration.location.uri }}/(.*)$ /$1 break;
+ root {{ ncgconfig.nms.devportal_dir }};
+}
+{% endif %}
+{% endif %}
diff --git a/templates/v4.1/auth/client/jwks.tmpl b/templates/v4.1/auth/client/jwks.tmpl
new file mode 100644
index 0000000..aea3a35
--- /dev/null
+++ b/templates/v4.1/auth/client/jwks.tmpl
@@ -0,0 +1,11 @@
+location = /_auth/jwt/{{ authprofile.name | replace(" ", "_") }}/_jwks_uri {
+ internal;
+
+ {% if authprofile.jwt.key.startswith('http://') or authprofile.jwt.key.startswith('https://') -%}
+ proxy_method GET;
+ proxy_pass {{ authprofile.jwt.key }};
+ {% else -%}
+ return 200 '{{ authprofile.jwt.key }}';
+ {%- endif %}
+
+}
diff --git a/templates/v4.1/auth/client/jwt.tmpl b/templates/v4.1/auth/client/jwt.tmpl
new file mode 100644
index 0000000..4aef347
--- /dev/null
+++ b/templates/v4.1/auth/client/jwt.tmpl
@@ -0,0 +1,4 @@
+auth_jwt "{{ authprofile.jwt.realm }}";
+auth_jwt_type {{ authprofile.jwt.jwt_type }};
+auth_jwt_key_request /_auth/jwt/{{ authprofile.name | replace(" ", "_") }}/_jwks_uri;
+auth_jwt_key_cache {{ authprofile.jwt.cachetime }};
\ No newline at end of file
diff --git a/templates/v4.1/auth/server/token.tmpl b/templates/v4.1/auth/server/token.tmpl
new file mode 100644
index 0000000..862630b
--- /dev/null
+++ b/templates/v4.1/auth/server/token.tmpl
@@ -0,0 +1,5 @@
+{% if authprofile.token.type == "bearer" %}
+proxy_set_header Authorization "Bearer {{ authprofile.token.token }}";
+{% elif authprofile.token.type == "header" %}
+proxy_set_header {{ authprofile.token.location }} "{{ authprofile.token.token }}";
+{% endif %}
\ No newline at end of file
diff --git a/templates/v4.1/configmap.tmpl b/templates/v4.1/configmap.tmpl
new file mode 100644
index 0000000..29c3973
--- /dev/null
+++ b/templates/v4.1/configmap.tmpl
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ name }}
+ {% if namespace -%}
+ namespace: {{ namespace }}
+ {% endif %}
+
+data:
+ {{ filename }}: |-
+{% filter indent(width = 4) %}
+ {{ nginxconfig }}
+{% endfilter %}
\ No newline at end of file
diff --git a/templates/v4.1/http.tmpl b/templates/v4.1/http.tmpl
new file mode 100644
index 0000000..4fb3331
--- /dev/null
+++ b/templates/v4.1/http.tmpl
@@ -0,0 +1,274 @@
+# NGINX configuration file - HTTP servers - generated by https://github.com/f5devcentral/NGINX-Declarative-API
+
+{# --- Maps section --- #}
+
+{% if declaration.maps %}
+{% for m in declaration.maps %}
+
+map {{ m.match }} {{ m.variable }} {
+ {% for e in m.entries %}
+ {%- if e.keymatch|lower == "exact" %}{% endif -%}
+ {%- if e.keymatch|lower == "regex" %} ~^ {%- endif -%}
+ {%- if e.keymatch|lower == "iregex" %} ~*^ {%- endif -%}
+ {{ e.key }} {{ e.value }};
+ {% endfor -%}
+
+}
+{% endfor %}
+{% endif %}
+
+{# --- Snippets section --- #}
+{% if declaration.snippet and declaration.snippet.content %}{{ declaration.snippet.content | b64decode }}{% endif %}
+
+
+{# --- Upstreams section --- #}
+{% if declaration.upstreams %}
+{% for u in declaration.upstreams %}
+{% if u.name %}
+{% if u.origin %}
+upstream {{ u.name }} {
+ zone {{ u.name }} 64k;
+ {% for o in u.origin -%}
+ server {{ o.server }}{% if o.weight %} weight={{ o.weight }}{% endif %}{% if o.max_fails %} max_fails={{ o.max_fails }}{% endif %}{% if o.fail_timeout %} fail_timeout={{ o.fail_timeout }}{% endif %}{% if o.max_conns %} max_conns={{ o.max_conns }}{% endif %}{% if o.slow_start %} slow_start={{ o.slow_start }}{% endif %}{% if o.backup and o.backup == True %} backup{% endif %};
+ {% endfor %}
+
+ {% if u.sticky and u.sticky.cookie and u.sticky.expires and u.sticky.domain and u.sticky.path -%}
+ sticky cookie {{ u.sticky.cookie }}{% if u.sticky.expires %} expires={{ u.sticky.expires }}{% endif %}{% if u.sticky.domain %} domain={{ u.sticky.domain }}{% endif %}{% if u.sticky.path %} path={{ u.sticky.path }}{% endif %};
+ {% endif -%}
+
+ {% if u.snippet and u.snippet.content %}{{ u.snippet.content | b64decode }}{% endif %}
+
+}
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endif %}
+
+{# --- Rate limit section --- #}
+
+{% if declaration.rate_limit %}
+{% for rl in declaration.rate_limit %}
+limit_req_zone {{ rl.key }} zone={{ rl.name }}:{{ rl.size }} rate={{ rl.rate }};
+{% endfor %}
+{% endif %}
+
+{# --- Server section for NGINX Plus API --- #}
+
+{% if declaration.nginx_plus_api %}
+{% if declaration.nginx_plus_api.listen %}
+server {
+ listen {{ declaration.nginx_plus_api.listen }};
+
+ location /api {
+ {% if declaration.nginx_plus_api.write == True %}api write=on;{% else %}api write=off;{% endif %}
+
+ {% if declaration.nginx_plus_api.allow_acl -%}
+ allow {{ declaration.nginx_plus_api.allow_acl }};
+ deny all;
+ {% else %}
+ allow all;
+ {% endif %}
+
+ }
+
+ location / {
+ root /usr/share/nginx/html;
+ index dashboard.html;
+ }
+}
+{% endif %}
+{% endif %}
+
+{# --- Server section --- #}
+
+{% for s in declaration.servers %}
+server {
+ # {{ s.name }}
+ {# --- Listen section start --- #}
+ {%- if s.listen -%}
+ {% if s.listen.address %}
+
+ listen {{ s.listen.address }}{% if s.listen.tls and s.listen.tls.certificate %} ssl{% endif %};
+ {% if s.listen.http2 and s.listen.http2 == True -%}http2 on;{% endif -%}
+ {%- endif %}
+
+ {# --- TLS section start --- #}
+ {%- if s.listen.tls -%}
+
+ {%- if s.listen.tls.certificate -%}
+ ssl_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.certificate }}.crt;
+ {% endif -%}
+ {%- if s.listen.tls.key -%}
+ ssl_certificate_key {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.key }}.key;
+ {% endif -%}
+ {% if s.listen.tls.ciphers -%}
+ ssl_ciphers {{ s.listen.tls.ciphers }};
+ {% endif -%}
+ {% if s.listen.tls.protocols -%}
+ ssl_protocols{% for p in s.listen.tls.protocols %} {{ p }}{% endfor %};
+ {% endif -%}
+ {% if s.listen.tls.trusted_ca_certificates -%}
+ ssl_trusted_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.trusted_ca_certificates }}.crt;
+ {% endif %}
+
+ {# --- mTLS section --- #}
+ {%- if s.listen.tls.mtls -%}
+ {%- if s.listen.tls.mtls.enabled|lower != "off" -%}
+ ssl_verify_client {{ s.listen.tls.mtls.enabled }};
+ {% endif %}
+ {% if s.listen.tls.mtls.client_certificates -%}
+ ssl_client_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.mtls.client_certificates }}.crt;
+ {% endif %}
+ {% endif %}
+
+ {# --- OCSP section start --- #}
+ {%- if s.listen.tls.ocsp and s.listen.tls.ocsp.enabled|lower != "off" -%}
+ ssl_ocsp {{ s.listen.tls.ocsp.enabled }};
+ {% if s.listen.tls.ocsp.responder -%}
+ ssl_ocsp_responder {{ s.listen.tls.ocsp.responder }};
+ {% endif %}
+ {% endif %}
+ {# --- OCSP section end --- #}
+
+ {# --- TLS stapling section start --- #}
+ {%- if s.listen.tls.stapling and s.listen.tls.stapling.enabled == True -%}
+ ssl_stapling on;
+ ssl_stapling_verify {% if s.listen.tls.stapling.verify == True %}on{% else %}off{% endif %};
+ {% if s.listen.tls.stapling.responder -%}
+ ssl_stapling_responder {{ s.listen.tls.stapling.responder }};
+
+ {% endif -%}
+ {%- endif %}
+ {# --- TLS stapling section end --- #}
+
+ {%- endif %}
+ {# --- TLS section end --- #}
+
+ {%- endif %}
+ {# --- Listen section end --- #}
+
+ {% if s.names -%}
+ server_name{% for svrname in s.names %} {{ svrname }}{% endfor -%};
+ status_zone {{ s.names[0] }};
+ proxy_set_header Host $host;
+ proxy_ssl_server_name on;
+ {% endif -%}
+
+ {% if s.resolver -%}
+ resolver {{ s.resolver }};
+ {% endif -%}
+
+ {# --- Server NGINX App Protect WAF section start --- #}
+
+ {% if s.app_protect -%}
+ {% if s.app_protect.enabled == True -%}
+ app_protect_enable on;
+ {% endif -%}
+ {% if s.app_protect.policy -%}
+ app_protect_policy_file {{ ncgconfig.nms.nap_policies_dir_pum }}/{{ s.app_protect.policy }}.tgz;
+ {% endif -%}
+ {% if s.app_protect.log -%}
+ {% if s.app_protect.log.enabled == True -%}
+ app_protect_security_log_enable on;
+ {% if s.app_protect.log.profile_name -%}
+ app_protect_security_log "{{ ncgconfig.nms.nap_logformats_dir_pum }}/{{ s.app_protect.log.profile_name }}.tgz" syslog:server={{ s.app_protect.log.destination }};
+ {% endif -%}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ {# --- Server NGINX App Protect WAF section end --- #}
+
+ {% if s.log.access %}access_log {{ s.log.access }} main;{% endif %}
+
+ {% if s.log.error %}error_log {{ s.log.error }};{% endif %}
+
+
+ {% filter indent(width=4) %}
+{% if s.snippet and s.snippet.content %}{{ s.snippet.content | b64decode }}{% endif %}
+ {% endfilter %}
+
+ {# --- Server location section start --- #}
+ {% for loc in s.locations %}
+
+ location
+ {%- if loc.urimatch -%}
+ {# location URI match types: prefix (default), exact (=), casesens_regex (~), caseinsens_regex (~*), best_nonregex (^~) #}
+ {%- if loc.urimatch|lower == "prefix" %} {% endif %}
+ {%- if loc.urimatch|lower == "exact" %} = {% endif %}
+ {%- if loc.urimatch|lower == "regex" %} ~ {% endif %}
+ {%- if loc.urimatch|lower == "iregex" %} ~* {% endif %}
+ {%- if loc.urimatch|lower == "best" %} ^~ {% endif %}
+ {%- endif -%}
+ {{ loc.uri }} {
+ {% if loc.authentication and loc.authentication.server and loc.authentication.server[0].profile -%}
+ include "{{ ncgconfig.nms.auth_server_dir }}/{{ loc.authentication.server[0].profile | replace(" ", "_") }}.conf";
+ {% endif %}
+
+ {% if loc.upstream %}proxy_pass {{ loc.upstream }};{% endif %}
+
+ {% if loc.log.access %}access_log {{ loc.log.access }} main;{% endif %}
+
+ {% if loc.log.error %}error_log {{ loc.log.error }};{% endif %}
+
+ {# --- Active healthchecks --- #}
+
+ {% if loc.health_check -%}
+ {% if loc.health_check.enabled == True -%}
+ health_check{% if loc.health_check.uri %} uri={{ loc.health_check.uri }}{% endif %}{% if loc.health_check.interval %} interval={{ loc.health_check.interval }}{% endif %}{% if loc.health_check.fails %} fails={{ loc.health_check.fails }}{% endif %}{% if loc.health_check.passes %} passes={{ loc.health_check.passes }}{% endif %};
+ {% endif %}
+ {% endif %}
+
+ {# --- Rate limiting --- #}
+
+ {% if loc.rate_limit -%}
+ {% if loc.rate_limit.profile %}limit_req zone={{ loc.rate_limit.profile }}{% if loc.rate_limit.burst %} burst={{ loc.rate_limit.burst }}{% endif %}{% if loc.rate_limit.delay == 0 %} nodelay;{% else %} delay={{ loc.rate_limit.delay }};{% endif %}
+
+ {% if loc.rate_limit.httpcode %}limit_req_status {{ loc.rate_limit.httpcode }};{% endif %}{% endif %}
+ {% endif %}
+
+ {# --- Client authentication --- #}
+ {%- if loc.authentication and loc.authentication.client -%}
+ {%- for clientAuthProfile in loc.authentication.client -%}
+ include "{{ ncgconfig.nms.auth_client_dir }}/{{ clientAuthProfile.profile | replace(" ", "_") }}.conf";
+ {% endfor -%}
+ {%- endif -%}
+
+ {# --- Location NGINX App Protect WAF --- #}
+
+ {% if loc.app_protect -%}
+ {% if loc.app_protect.enabled == True -%}
+ app_protect_enable on;
+ {% endif -%}
+ {% if loc.app_protect.policy -%}
+ app_protect_policy_file {{ ncgconfig.nms.nap_policies_dir_pum }}/{{ loc.app_protect.policy }}.tgz;
+ {% endif %}
+ {% if loc.app_protect.log -%}
+ {% if loc.app_protect.log.enabled == True -%}
+ app_protect_security_log_enable on;
+ {% if loc.app_protect.log.profile_name -%}
+ app_protect_security_log "{{ ncgconfig.nms.nap_logformats_dir_pum }}/{{ loc.app_protect.log.profile_name }}.tgz" syslog:server={{ loc.app_protect.log.destination }};
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+ {# --- Location snipptes --- #}
+
+ {% if loc.snippet and loc.snippet.content %}{{ loc.snippet.content | b64decode }}{% endif %}
+
+ }
+ {% endfor %}
+
+ {# --- JWT authentication JWKS endpoints --- #}
+ {%- if declaration.authentication and declaration.authentication.client -%}
+ {%- for clientAuthProfile in declaration.authentication.client -%}
+ {%- if clientAuthProfile.type == "jwt" -%}
+ include "{{ ncgconfig.nms.auth_client_dir }}/jwks_{{ clientAuthProfile.name | replace(" ", "_") }}.conf";
+ {% endif -%}
+ {%- endfor -%}
+ {%- endif -%}
+
+{%- if s.listen.address -%}
+{%- endif -%}
+}
+{% endfor -%}
\ No newline at end of file
diff --git a/templates/v4.1/logformat.tmpl b/templates/v4.1/logformat.tmpl
new file mode 100644
index 0000000..db9863e
--- /dev/null
+++ b/templates/v4.1/logformat.tmpl
@@ -0,0 +1,12 @@
+{
+ "filter": {
+ "request_type": "{{ log.type }}"
+ },
+
+ "content": {
+ "format": "{{ log.format }}",
+ "format_string": "{{ log.format_string }}",
+ "max_request_size": "{{ log.max_request_size }}",
+ "max_message_size": "{{ log.max_message_size }}"
+ }
+}
diff --git a/templates/v4.1/nginx-conf/mime.types b/templates/v4.1/nginx-conf/mime.types
new file mode 100644
index 0000000..d4e08df
--- /dev/null
+++ b/templates/v4.1/nginx-conf/mime.types
@@ -0,0 +1,97 @@
+types {
+ text/html html htm shtml;
+ text/css css;
+ text/xml xml;
+ image/gif gif;
+ image/jpeg jpeg jpg;
+ application/javascript js;
+ application/atom+xml atom;
+ application/rss+xml rss;
+
+ text/mathml mml;
+ text/plain txt;
+ text/vnd.sun.j2me.app-descriptor jad;
+ text/vnd.wap.wml wml;
+ text/x-component htc;
+
+ image/png png;
+ image/svg+xml svg svgz;
+ image/tiff tif tiff;
+ image/vnd.wap.wbmp wbmp;
+ image/webp webp;
+ image/x-icon ico;
+ image/x-jng jng;
+ image/x-ms-bmp bmp;
+
+ font/woff woff;
+ font/woff2 woff2;
+
+ application/java-archive jar war ear;
+ application/json json;
+ application/mac-binhex40 hqx;
+ application/msword doc;
+ application/pdf pdf;
+ application/postscript ps eps ai;
+ application/rtf rtf;
+ application/vnd.apple.mpegurl m3u8;
+ application/vnd.google-earth.kml+xml kml;
+ application/vnd.google-earth.kmz kmz;
+ application/vnd.ms-excel xls;
+ application/vnd.ms-fontobject eot;
+ application/vnd.ms-powerpoint ppt;
+ application/vnd.oasis.opendocument.graphics odg;
+ application/vnd.oasis.opendocument.presentation odp;
+ application/vnd.oasis.opendocument.spreadsheet ods;
+ application/vnd.oasis.opendocument.text odt;
+ application/vnd.openxmlformats-officedocument.presentationml.presentation
+ pptx;
+ application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+ xlsx;
+ application/vnd.openxmlformats-officedocument.wordprocessingml.document
+ docx;
+ application/vnd.wap.wmlc wmlc;
+ application/wasm wasm;
+ application/x-7z-compressed 7z;
+ application/x-cocoa cco;
+ application/x-java-archive-diff jardiff;
+ application/x-java-jnlp-file jnlp;
+ application/x-makeself run;
+ application/x-perl pl pm;
+ application/x-pilot prc pdb;
+ application/x-rar-compressed rar;
+ application/x-redhat-package-manager rpm;
+ application/x-sea sea;
+ application/x-shockwave-flash swf;
+ application/x-stuffit sit;
+ application/x-tcl tcl tk;
+ application/x-x509-ca-cert der pem crt;
+ application/x-xpinstall xpi;
+ application/xhtml+xml xhtml;
+ application/xspf+xml xspf;
+ application/zip zip;
+
+ application/octet-stream bin exe dll;
+ application/octet-stream deb;
+ application/octet-stream dmg;
+ application/octet-stream iso img;
+ application/octet-stream msi msp msm;
+
+ audio/midi mid midi kar;
+ audio/mpeg mp3;
+ audio/ogg ogg;
+ audio/x-m4a m4a;
+ audio/x-realaudio ra;
+
+ video/3gpp 3gpp 3gp;
+ video/mp2t ts;
+ video/mp4 mp4;
+ video/mpeg mpeg mpg;
+ video/quicktime mov;
+ video/webm webm;
+ video/x-flv flv;
+ video/x-m4v m4v;
+ video/x-mng mng;
+ video/x-ms-asf asx asf;
+ video/x-ms-wmv wmv;
+ video/x-msvideo avi;
+}
\ No newline at end of file
diff --git a/templates/v4.1/nginx-conf/nginx.conf b/templates/v4.1/nginx-conf/nginx.conf
new file mode 100644
index 0000000..6ee69c9
--- /dev/null
+++ b/templates/v4.1/nginx-conf/nginx.conf
@@ -0,0 +1,40 @@
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log notice;
+pid /var/run/nginx.pid;
+
+{% for m in nginxconf.modules %}
+load_module modules/{{m}}.so;
+{% endfor %}
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+ keepalive_timeout 65;
+ #gzip on;
+ include /etc/nginx/conf.d/*.conf;
+}
+
+
+# TCP/UDP proxy and load balancing block
+stream {
+ log_format stream-main '$remote_addr [$time_local] '
+ '$protocol $status $bytes_sent $bytes_received '
+ '$session_time "$ssl_preread_server_name"';
+ #access_log /dev/stdout stream-main;
+ include /etc/nginx/stream-conf.d/*.conf;
+}
\ No newline at end of file
diff --git a/templates/v4.1/stream.tmpl b/templates/v4.1/stream.tmpl
new file mode 100644
index 0000000..3ae64f0
--- /dev/null
+++ b/templates/v4.1/stream.tmpl
@@ -0,0 +1,66 @@
+# NGINX configuration file - Stream servers - generated by https://github.com/f5devcentral/NGINX-Declarative-API
+
+{# --- Upstreams section --- #}
+
+{% if declaration.upstreams %}
+{% for u in declaration.upstreams %}
+{% if u.name %}
+{% if u.origin %}
+upstream {{ u.name }} {
+ zone {{ u.name }} 64k;
+ {% for o in u.origin -%}
+ server {{ o.server }}{% if o.weight %} weight={{ o.weight }}{% endif %}{% if o.max_fails %} max_fails={{ o.max_fails }}{% endif %}{% if o.fail_timeout %} fail_timeout={{ o.fail_timeout }}{% endif %}{% if o.max_conns %} max_conns={{ o.max_conns }}{% endif %}{% if o.slow_start %} slow_start={{ o.slow_start }}{% endif %}{% if o.backup and o.backup == True %} backup{% endif %};
+ {% endfor %}
+
+ {% if u.snippet and u.snippet.content %}{{ u.snippet.content }}{% endif %}
+
+}
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endif %}
+
+
+{# --- Stream server section --- #}
+
+{% for s in declaration.servers %}
+ {%- if s.listen %}
+ {% if s.listen.address %}
+
+server {
+ listen {{ s.listen.address }}{% if s.listen.protocol == "udp" %} {{ s.listen.protocol }}{% endif %};
+ status_zone {{ s.name }};
+ {% endif -%}
+ {% endif -%}
+
+
+ {# --- TLS section --- #}
+ {%- if s.listen.tls -%}
+ {%- if s.listen.tls.certificate -%}
+ ssl_certificate {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.certificate }}.crt;
+ {% endif -%}
+ {%- if s.listen.tls.key -%}
+ ssl_certificate_key {{ ncgconfig.nms.certs_dir }}/{{ s.listen.tls.key }}.key;
+ {% endif -%}
+ {% if s.listen.tls.ciphers -%}
+ ssl_ciphers {{ s.listen.tls.ciphers }};
+ {% endif -%}
+ {% if s.listen.tls.protocols -%}
+ ssl_protocols{% for p in s.listen.tls.protocols %} {{ p }}{% endfor %};
+ {% endif %}
+ {% endif %}
+
+ {% if s.upstream -%}
+ proxy_pass {{ s.upstream }};
+ {% endif %}
+
+ {% if s.snippet and s.snippet.content %}{{ s.snippet.content | b64decode }}{% endif %}
+
+ {%- if s.listen %}
+ {%- if s.listen.address %}
+
+}
+ {% endif -%}
+ {% endif -%}
+
+{%- endfor %}