- name
- nic-add-policy
- description
- Step-by-step checklist for adding a new Policy CRD type to NIC. Use when implementing a new policy like AccessControl, RateLimit, JWTAuth, ExternalAuth, BasicAuth, IngressMTLS, EgressMTLS, OIDC, WAF, APIKey, Cache, or CORS, or extending the policy system with a new policy type.
# Adding a New Policy Type
Follow these steps IN ORDER. Each step depends on the previous.
## Step 1: Define the CRD type
File: `pkg/apis/configuration/v1/types.go`
- Add a new struct (e.g., `type MyPolicy struct { ... }`)
- Add a `*MyPolicy` pointer field to `PolicySpec`
- Use kubebuilder markers for validation
- JSON tags: **kebab-case** for NGINX-proxy fields, **camelCase** for K8s fields
- `*bool`/`*int` = optional/nullable. Plain `bool`/`int` = required or zero-default
- Booleans defaulting to `false` must be non-pointer value types
## Step 2: Regenerate deep copy
Run `make update-codegen` to update `zz_generated.deepcopy.go`.
## Step 3: Regenerate CRDs
Run `make update-crds` to regenerate `config/crd/bases/`, `deploy/crds.yaml`, and chart CRDs.
## Step 4: Add validation
File: `pkg/apis/configuration/validation/policy.go`
- Add `validate<MyPolicy>(spec *v1.MyPolicy, fieldPath *field.Path) field.ErrorList`
- Wire into `validatePolicySpec()` with field count increment and feature gate check
- Add tests in `policy_test.go` with valid and invalid cases
## Step 5: Add template structs
File: `internal/configs/version2/http.go`
- Add struct (e.g., `type MyPolicyConfig struct { ... }`)
- Add `*MyPolicyConfig` or fields to `Server`, `Location`, or both
- If the policy needs HTTP-level directives (zones, maps), add fields to `VirtualServerConfig`
## Step 6: Add config generation
File: `internal/configs/policy.go`
- Add field(s) to `policiesCfg`
- Add `add<MyPolicy>Config()` method following the pattern below
- Wire into the `switch` in `generatePolicies()`
- Add tests in `policy_test.go`
## Step 7: Wire into VirtualServer generation
File: `internal/configs/virtualserver.go`
- In `GenerateVirtualServerConfig()`, extract from `policiesCfg` and assign to `version2` fields
- Use `addPoliciesCfgToLocation()` for location-level assignment
## Step 8: Wire into Ingress generation (if applicable)
File: `internal/configs/ingress.go`
- In `generateNginxCfg()`, extract from `policiesCfg` and assign to `version1` fields
- Handle mergeable ingress in `generateNginxCfgForMergeableIngresses()`
## Step 9: Add NGINX template directives
- Version 2: `internal/configs/version2/nginx.virtualserver.tmpl` and `internal/configs/version2/nginx-plus.virtualserver.tmpl`
- Version 1: `internal/configs/version1/nginx.ingress.tmpl` and `internal/configs/version1/nginx-plus.ingress.tmpl`
- Use `{{- if }}` / `{{- with }}` guards around directive blocks
- Template helpers go in `internal/configs/version2/template_helper.go` and/or `internal/configs/version1/template_helper.go`, matching the template version you are updating
- HTTP-level directives (zones, maps) go BEFORE `server{}`
- Server-level inside `server{}`, location-level inside each `location{}`
## Step 10: Update snapshot tests
Files: `internal/configs/version2/templates_test.go` (VS/VSR/TS), `internal/configs/version1/template_test.go` (Ingress)
1. Add the new policy fields to the fixture structs used by the snapshot tests -- a regeneration with no fixture change produces no diff and leaves the policy untested.
2. Run `make test-update-snaps`.
3. `git diff -- '**/__snapshots__/**'` and confirm your directives render in the golden files for every edition the policy supports. Plus-only policies (OIDC, WAF) must appear in the Plus golden files **only**; policies available to both editions must appear in both.
4. Run `make test` to confirm green, and commit the regenerated golden files with the template change.
If you wired the policy into Ingress (Step 8), version1 snapshots must change too.
## Step 11: Update the Helm chart (if policy needs CLI flag or ConfigMap entry)
- `charts/nginx-ingress/values.yaml` -- add value with `##` doc
- `charts/nginx-ingress/values.schema.json` -- add schema entry
- `charts/nginx-ingress/templates/_helpers.tpl` -- add CLI arg or ConfigMap key
- `charts/tests/testdata/` -- add test values file
- `charts/tests/helmunit_test.go` -- add test case
## Step 12: Add controller support
File: `internal/k8s/`
- In `syncPolicy()`, ensure the new type is handled for VS/VSR/Ingress
- Check if it needs feature-gate guarding (isPlus, enableOIDC, etc.)
If the policy references secrets:
- Add every Secret field to `policySecretIndexFunc()` through `collectPolicySecretRefs()` or `collectWAFSecretRefs()`.
- Resolve each reference during extended-resource construction with `secretStore.GetSecret(namespacedKey, role)`.
- Store the result under `secrets.RefKey(namespacedKey, role)`.
- Select the role from the reference site's semantics; never infer it from `Secret.type` or Secret data.
- Ensure `syncPolicy()` fans out to every supported VS, VSR, and Ingress consumer.
- Add index tests covering add, update, delete, cross-namespace references, and duplicate references.
## Step 13: Write integration tests
Directory: `tests/suite/`
- Create test data YAMLs in `tests/data/<feature>/`
- Create `test_<feature>_policies_vs.py`, `_vsr.py`, `_ingress.py`
- Use `@pytest.mark.policies` and `@pytest.mark.policies_<feature>` markers
- Register the new marker in `pyproject.toml` -- pytest runs with `--strict-markers`
---
## Gotchas
- **Never** skip `make update-codegen` after changing `types.go` -- the build will fail with missing DeepCopy methods
- **Never** use raw user strings in NGINX config without `containsDangerousChars()` validation
- Both OSS and Plus templates must be updated for policies available to both editions -- they are separate files, each with its own snapshot entries. Plus-only policies (OIDC, WAF) belong in the Plus templates only
- A policy that reaches a template but has no snapshot fixture ships with zero rendered-output coverage
- `make update-crds` also refreshes `deploy/crds*.yaml` and `docs/crd/`; `charts/nginx-ingress/crds` is a symlink to `config/crd/bases/`
- If the policy adds telemetry counters, run `make telemetry-schema` -- CI fails on any diff in `internal/telemetry`
- `policiesCfg` duplicate check must warn and return, not error (exception: `addCORSConfig` has no duplicate check -- it overwrites, since CORS is additive via headers)
---
## Policy add*Config() Pattern
Every `add*Config()` method in `internal/configs/policy.go` follows this pattern:
```go
func (p *policiesCfg) addMyPolicyConfig(spec *conf_v1.MyPolicy, key, namespace string,
secretRefs map[secrets.SecretRefKey]*secrets.SecretReference) *validationResults {
res := newValidationResults()
// 1. Duplicate check
if p.MyPolicy != nil {
res.addWarningf("MyPolicy policy already configured, ignoring")
return res
}
// 2. Secret resolution (if applicable)
secretKey := namespace + "/" + spec.Secret
refKey := secrets.RefKey(secretKey, secrets.RoleExpected)
secretRef, ok := secretRefs[refKey]
if !ok || secretRef == nil {
res.isError = true
res.addWarningf("secret %s could not be resolved", secretKey)
return res
}
if secretRef.Error != nil {
res.isError = true
res.addWarningf("secret %s is invalid: %v", secretKey, secretRef.Error)
return res
}
// 3. Build template struct and assign
p.MyPolicy = &version2.MyPolicyConfig{
Field1: spec.Field1,
Field2: spec.Field2,
Secret: secretRef.Path,
}
return res
}
```
## NGINX Template Pattern
```nginx
{{- with $s.MyPolicy }}
my_directive {{ .Value }};
{{- if .OptionalField }}
my_optional_directive {{ .OptionalField }};
{{- end }}
{{- end }}
```
Ver no GitHub