Skip to content

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
  1. Format check, vet, Go tests, build.
  2. Validates the fixtures. Policy Validate explains the skips.
  3. 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:

tools/validator/testdata/baseline.yaml
selector:
  matchExpressions:
    - key: name
      operator: Exists

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:

  1. {{hub ... hub}} as the policy propagator does. Lookups stay in the policy namespace unless the policy sets hubTemplateOptions.serviceAccountName.
  2. {{ ... }} as the config-policy-controller does: object-templates-raw whole, each object-templates entry separately with skipObject.

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:

stack/acs/converters/acs-console-link.yaml
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.

tools/validator/testdata/profiles/all-on.yaml
stack:
  acs:
    enabled: true
    toggles:
      console: true

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