Validation¶
tools/validator renders every element in stack/ for each fixture cluster, building the values
cascade like the ApplicationSet (Order), and checks sources and output. Needs Go
and the Helm version CI pins (Policy Validate). kubeconform is
optional; without it POLICY080 is skipped.
Running it¶
The CI commands, from the repo root:
make -C tools/validator lint test build # (1)!
tools/validator/bin/policystack-validator --repo-root . \
--extra-values tools/validator/testdata/baseline.yaml \
--include-sample-element --skip POLICY080,POLICY081 # (2)!
for p in tools/validator/testdata/profiles/*.yaml; do # (3)!
tools/validator/bin/policystack-validator --repo-root . --only RENDER000,POLICY081 \
--extra-values "tools/validator/testdata/baseline.yaml,$p"
done
- Format check, vet, Go tests, build.
- Validates the fixtures. Policy Validate explains the skips.
- Evaluates every converter's ACM templates, see ACM templates.
--extra-values files override the whole cascade, except in lifecycle/, which ships its own
selector. baseline.yaml supplies the selector the ApplicationSet normally injects
(Placement). Without it helm template fails on any enabled element:
Note
helm dependency update runs unless charts/ already holds <name>-<version>.tgz for every
dependency. Bumping policy-library to an unpublished version fails the fetch, and RENDER000
reports the element unrendered.
Fixtures¶
tools/validator/testdata/clusters holds example fleet files. The
file name is the cluster name: hubs/acm-dc1.yaml renders as acm-dc1 and reads
values/clusters/acm-dc1.yaml. A fixture that sets hub also renders lifecycle/ as
lifecycle-<cluster>, with the fixture itself appended to the cascade (Cluster lifecycle).
Fixture list
| Fixture | Covers |
|---|---|
prod-east-1, nonprod-west-1 |
Imported managed clusters, version and channel |
hubs/acm-dc1 |
The hub the lifecycle fixtures name |
hcp-kubevirt, hcp-agent |
HyperShift |
hive-aws |
Hive on AWS |
sno-edge |
SiteConfig single-node |
hcp-retired, hive-retired, sno-retired |
state: absent |
name-budget-30-characters-long |
The 30-character name every element must fit (Naming limits) |
Output¶
error POLICY020 [metallb] configPolicies[0] "metallb-instance" references templateNames[0] "metallb-crd" but converters/metallb-crd.yaml does not exist
at /path/to/policystack/stack/metallb/values.yaml:107:19
warning POLICY021 [metallb] converters/metallb-cr.yaml is not referenced by any templateNames[].name
at /path/to/policystack/stack/metallb/converters/metallb-cr.yaml:1
1 error(s), 1 warning(s)
A clean run prints ok: no findings. Findings on rendered output also name the fixture cluster.
| Exit | Meaning |
|---|---|
0 |
No errors. Warnings alone pass |
1 |
Errors, or warnings with --severity warning |
2 |
The validator failed, e.g. Helm not on PATH |
| Flag | Effect |
|---|---|
--only / --skip |
Comma-separated rule IDs to run / skip |
--severity warning |
Fail on warnings too |
--github |
GitHub Actions annotations |
--fixtures-dir |
Fleet files to render, hubs in its hubs/. Default tools/validator/testdata/clusters. CI passes fleet when it exists |
--lifecycle-dir |
Lifecycle chart, default <repo>/lifecycle |
--render-dir |
Writes <dir>/<fleet file path without .yaml>/<element>.yaml, a failed render as # RENDER000: lines. Render Diff diffs two of these |
--help lists the rest.
ACM templates¶
Helm treats escaped ACM templates ({{ "{{" }} ... {{ "}}" }}) as strings. POLICY081 resolves them
against an empty cluster with go-template-utils, the library ACM uses:
{{hub ... hub}}as the policy propagator does. Lookups stay in the policy namespace unless the policy setshubTemplateOptions.serviceAccountName.{{ ... }}as the config-policy-controller does:object-templates-rawwhole, eachobject-templatesentry separately withskipObject.
It fails on an undefined function (in any branch), a failed call such as index on nil, a hub
lookup outside the policy namespace, a kind the fake cluster does not serve (misspelled kind or
apiVersion), and output with <no value> or an object missing apiVersion/kind.
A missing object is not a failure: on a real cluster another policy or operator creates it.
Evaluation stops there, so branches that need existing objects are parsed but not run. Guard such
lookups with dig and skipObject:
href: '{{ "{{" }} $host := (lookup "route.openshift.io/v1" "Route" "{{ .Parameters.namespace }}" "central").spec.host {{ "}}" }}{{ "{{" }} skipObject (not $host) {{ "}}" }}https://{{ "{{" }} $host {{ "}}" }}'
Looking up a new kind
Add it to kinds in tools/validator/internal/acmtemplates/acmtemplates.go.
Fixtures leave most elements off, so CI evaluates against tools/validator/testdata/profiles, which turn every element and toggle on. Add a
new element or toggle to all-on.yaml, or to variants.yaml if it conflicts with one there.
TestProfileCoverage fails until every converter with ACM templates renders under some profile.
Profiles enable toggles without their input maps, so POLICY082 fires there; CI runs only RENDER000
and POLICY081 on them. A change to a lifecycle converter's ACM template needs a test case in
tools/validator/internal/acmtemplates, which resolves them against fake hub objects.
Rules¶
| ID | Severity | What it catches |
|---|---|---|
| POLICY001 | error | Policy/PolicySet ACM-replicated name <ns>.<name>-<release> > 63 chars; ConfigurationPolicy/OperatorPolicy/CertificatePolicy metadata.name (<parent>-<sub>) > 63 chars |
| POLICY002 | error | Duplicate rendered metadata.name WITHIN one element |
| POLICY003 | error | Two different elements render a policy-template with the same name. ACM requires template names to be unique across every Policy on a cluster, so policies[].name: install + configPolicies[].name: ns-monitoring in two elements is rejected on-cluster with "Template name must be unique". Covers toggled-off entries and the -ns/-status templates an operatorPolicy generates |
| POLICY010 | error | policyRef points to nonexistent or disabled parent policy |
| POLICY011 | error | dependency reference that can never be satisfied: unknown dependencies/extraDependencies target, unknown element:, element: on a template kind, waitForOperator naming no enabled operator, or a target policy policy-library never emits (no enabled sub-policy attached) |
| POLICY020 | error | templateNames[].name has no matching converters/<name>.yaml; <name> may contain /, as in hypershift/ready |
| POLICY021 | warning | Converter file not referenced by any templateNames[].name |
| POLICY022 | error | A converter reads a top-level config key missing from the element's values.yaml, which renders as null: .Values.stack.<el>.config.<key>, from $ too, or $v.<key> with $v bound to .Values.stack.<el>.config. Only Helm actions count, not escaped ACM templates. Nested keys are not checked |
| POLICY030 | error/warning | Invalid enum: severity / remediationAction / complianceType / upgradeApproval (None or Automatic). In operatorPolicies entries also: subscription.installPlanApproval set, a versions entry or subscription.startingCSV that is not a valid CSV name (templated entries skipped, comma-separated versions entries split), and a name: key, which is ignored (warning) |
| POLICY031 | error/warning | values keys policy-library never reads: enable: (element renders nothing) and defaultPolicy: (metadata silently dropped) are errors; default.severity/.remediationAction/.disabled are warnings. Also flags rawTemplate: true with more than one templateNames entry, and operatorPolicies as a list (policy-library 3.x needs a map) |
| POLICY032 | error/warning | stack.<el>.operatorPolicies in a values layer: a key the element's values.yaml does not define, a list, or any POLICY030 operator problem. Reported once per layer file |
| POLICY033 | error/warning | configPolicies[].pruneObjectBehavior other than None: an invalid value, an effective remediationAction other than enforce (a policy-level value overrides the template's), or any dependency (waitForOperator, extraDependencies, parent dependencies, orderManifests unless first template, orderPolicies unless first Policy), since ACM deletes the template whenever the dependency is unmet. DeleteAll is a warning |
| POLICY034 | error | Top-level version or channel in a values layer: only the fleet file sets them. Reported once per layer file |
| POLICY035 | error | A key in the root values.yaml or a file under values/ that nothing reads: an unknown top-level or global key, a stack key that names no element, a toggles key that names no entry of the element or of the layer's own lists, or a top-level config key missing from the element's values.yaml. Checks every file, used by a fleet file or not |
| POLICY036 | error | A configPolicies or operatorPolicies entry whose remediationAction differs (case-insensitive) from a set policies[].remediationAction of its parent. ACM applies the Policy-level action to every template, so an inform mustnothave gate under an enforce Policy deletes. An unset configPolicies action counts as the chart default, inform. Covers toggled-off entries |
| POLICY040 | error | policySets[].policies[] references a name not in policies[] |
| POLICY050 | error | Invalid fleet file: YAML error, unknown key, missing or empty revision, revision, hub, state, version, channel or valueFiles entry Argo CD reads as a number or boolean, valueFiles entry that is null, listed twice, or not a file under values/; version that is not x.y.z, install.version; empty hub, version or channel, hub with no hubs/<hub>.yaml, hub naming the cluster itself, hub in a hub file, state other than present/absent, install that is not a map, state or install without hub, or a cluster that sets hub whose name is not a DNS label |
| POLICY060 | warning | policy-library version drift across element Chart.yaml files |
| POLICY070 | error | helm lint non-zero |
| POLICY080 | error | kubeconform schema check on rendered manifests |
| POLICY081 | error | An escaped ACM template ACM would fail to resolve, resolved hub then managed on an empty cluster: an undefined function in any branch, a failed call such as index on nil, a hub lookup outside the policy namespace without hubTemplateOptions.serviceAccountName, a kind discovery does not serve, or output with <no value> or an object without apiVersion/kind. A missing object is not a failure. Findings name the sub-policy without its cluster suffix and carry no cluster, so a broken template reports once |
| POLICY082 | error | A rendered Policy whose every template is a ConfigurationPolicy with no objects (object-templates-raw empty or [], or no object-templates). ACM reports it Compliant without checking anything; policy-library renders [] when a raw converter's input map is empty. A Policy with only some empty templates passes |
| POLICY090 | error | Chart.yaml name → camelCase mismatch with single key under stack:. For a chart whose values.schema.json has an install.engine enum (lifecycle/), a stack: key that is not an engine or an engine with no stack: key. Every other check then runs on each engine's component, and each fixture renders the one its install.engine selects |
| POLICY091 | error | An element name equal to another's, or starting with another's followed by -. Release names join element and cluster with -, so a on cluster b-c and a-b on cluster c would share one Application and its Policies. lifecycle/ takes part, which reserves lifecycle |
| RENDER000 | error | helm template failed, or helm dependency update did. A chart whose dependencies were not fetched is not rendered, since Helm would use any charts/ archive of the dependency, whatever its version. Reported once per chart |