# Validation checks

Whenever you compile or deploy a cluster, Kix runs 24 cross-resource validation checks across all rendered manifests to catch wiring mistakes that would otherwise fail in Kubernetes. Because these checks guard against broken references (such as a Service pointing to a missing port, a workload mounting a nonexistent ConfigMap, or a custom resource deployed before its CRD), they are always active and cannot be turned off.

:::note[Helpful background]
- [05. Basic graph and deploy loop](/docs/tutorials/05-basic-graph-and-deploy-loop/): see how `kix check` validates a cluster before deployment.
- [Scorecard levels: manifest, package, namespace, cluster](/docs/explanation/scorecard-levels/): learn how always-on validation checks fit alongside constructor checks and configurable scorecard rules.
- [The dependency graph as the model](/docs/explanation/the-dependency-graph/): understand how Kix tracks resource dependencies so custom resources are applied after their CRDs.
- [Labels and Selectors (Kubernetes docs)](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/): review how Kubernetes connects Services, NetworkPolicies, and PodDisruptionBudgets to pods using label selectors.
:::

This page is maintained alongside `kix/lib/eval/validate.nix`. The test suites in `kix/lib/tests/validate.nix` (checks 1 through 18) and `kix/lib/tests/units/lib-reference-checks.nix` (checks 18 through 24) contain passing and failing cases for each check.

## Checks

| # | Check | Skips |
| --- | --- | --- |
| 1 | A workload's `serviceAccountName` names a `ServiceAccount` in its namespace. | `default`; unset |
| 2 | A `RoleBinding` or `ClusterRoleBinding` `roleRef` names a `Role` or `ClusterRole` in the build. | Names starting with `system:` or ending with `-psp`; `admin`, `edit`, `view`, `cluster-admin`, and `extension-apiserver-authentication-reader` |
| 3 | `volumes[].configMap.name` names a `ConfigMap` in the workload's namespace. | `Secret` volumes (often provisioned externally) |
| 4 | A `Service` selector matches at least one workload pod template in its namespace. | Services with no selector; control-plane scrape selectors (`kube-dns`, `kube-scheduler`, and similar); Services carrying `kix.run/operand-of` |
| 5 | A `HorizontalPodAutoscaler` `scaleTargetRef` names a workload in its namespace. | |
| 6 | A `StatefulSet` `serviceName` names a headless `Service` (`clusterIP: None`) in its namespace. | Unset `serviceName` |
| 7 | No two resources share the same `kind`, `namespace`, and `name`. | |
| 8 | Every resource carries `app.kubernetes.io/managed-by: kix` in `metadata.labels`. | |
| 9 | A workload's `selector.matchLabels` is a subset of its pod template labels (`Deployment`, `StatefulSet`, `DaemonSet`, `Job`). | |
| 10 | `Ingress` backends name a `Service` in the same namespace. | |
| 11 | Every `metadata.namespace` has a corresponding `Namespace` resource in the build. | `_`-prefixed sentinel namespaces used internally for cluster-scoped instances |
| 12 | A `NetworkPolicy` `podSelector` matches at least one pod template in its namespace. | Empty `podSelector` (`{ }`) |
| 13 | A named `Service` `targetPort` exists on a container of a selected pod template. | Numeric `targetPort` values; Services that match no pod template in the build |
| 14 | A `PodDisruptionBudget` `selector.matchLabels` matches at least one pod template in its namespace. | PDBs without `matchLabels`; PDBs carrying `kix.run/operand-of` |
| 15 | A `Job`'s pod template labels do not satisfy any `Service` selector in its namespace (which would route live service traffic to a batch Job pod). | Services without a selector |
| 16 | The `kix.run/rerun` annotation appears only on `Job` resources and only with the value `"on-change"`. | |
| 17 | A workload's `priorityClassName` names a `PriorityClass` in the build. | `system-cluster-critical` and `system-node-critical` |
| 18 | A resource of a namespaced kind sets `metadata.namespace`. | Custom kinds whose scope is not declared in the build (verified against cluster API discovery by `kix deploy` instead) |
| 19 | A workload's `volumes[].persistentVolumeClaim.claimName` names a `PersistentVolumeClaim` in its namespace. | |
| 20 | The `storageClassName` on a `PersistentVolumeClaim`, on a `StatefulSet`'s `volumeClaimTemplates`, or on an ephemeral volume's `volumeClaimTemplate` names a `StorageClass` in the build (or a platform import), or matches a `PersistentVolume` in the build. | Unset or `""`; storage class names embedded inside operator custom resource specs |
| 21 | Every custom resource outside the built-in Kubernetes API groups depends (directly or transitively via `kix.run/depends-on`) on a `CustomResourceDefinition`, a CRD ref marker, or an `APIService` for its group and kind that serves its version. | Built-in control-plane API groups (`""`, `apps`, `batch`, `autoscaling`, `policy`, `networking.k8s.io`, `rbac.authorization.k8s.io`, `storage.k8s.io`, `scheduling.k8s.io`, `admissionregistration.k8s.io`, `apiextensions.k8s.io`, `apiregistration.k8s.io`, `coordination.k8s.io`, `discovery.k8s.io`, `events.k8s.io`, `node.k8s.io`, `certificates.k8s.io`, `flowcontrol.apiserver.k8s.io`, `resource.k8s.io`, `authentication.k8s.io`, `authorization.k8s.io`, `internal.apiserver.k8s.io`, `storagemigration.k8s.io`, and `kix.run`); extension groups under `*.k8s.io` such as `gateway.networking.k8s.io`, `snapshot.storage.k8s.io`, and `metrics.k8s.io` are **not** skipped |
| 22 | Every key a container reads by name from a `Secret` or `ConfigMap` (`secretKeyRef`, `configMapKeyRef`, volume or projected `items[].key`, or the first path segment of a `subPath` mount without `items`) is declared on that `Secret` or present in that `ConfigMap`. | References with `optional = true`; whole-object mounts (`envFrom`, volumes without `items` or `subPath`); names wrapped in `kix.untrackedRef`; Secrets without `kix.run/secret-keys` (such as raw Helm chart Secrets); objects ceding `data`, `binaryData`, or `stringData` via `kix.run/cede-fields` |
| 23 | At most one `PriorityClass` sets `globalDefault: true`, at most one `StorageClass` carries `storageclass.kubernetes.io/is-default-class: "true"` (or the beta `storageclass.beta.kubernetes.io/is-default-class`), and at most one `IngressClass` carries `ingressclass.kubernetes.io/is-default-class: "true"`. | |
| 24 | A `Job` does not set `spec.ttlSecondsAfterFinished` (so Kubernetes does not delete finished Jobs behind Kix's back and cause later deploys to re-run them). | `CronJob` `spec.jobTemplate` specs (whose Jobs are created and cleaned up by Kubernetes) |

## Check 18 and deploy-time scope verification

Check 18 verifies all built-in namespaced kinds, custom kinds whose `CustomResourceDefinition` in the build sets `spec.scope: Namespaced`, and custom kinds declared via `scope.mkCRDRef` with `crdScope = "Namespaced"` (the default). For example, building a `ConfigMap` with `scope.mkClusterResource` fails check 18 at evaluation time.

If a custom resource's scope is not known at build time, `kix deploy` queries the target cluster's API discovery before applying any wave and aborts the plan if a namespaced resource omits `metadata.namespace`.

## Check 21: custom resources and their CRD declarations

The Kubernetes API server rejects custom resources whose kind is not yet established, so `kix deploy` must order every custom resource after its CRD. Check 21 verifies that every resource outside the built-in API groups has a dependency path in `kix.run/depends-on` to one of:
- a `CustomResourceDefinition` in the build (`scope.mkCRD`, vendored CRDs, or chart CRDs);
- a CRD reference marker (`scope.mkCRDRef` or `kix.mkImport { crds = [ ... ]; }`); or
- an `APIService` for its API group.

Check 21 also verifies that the custom resource's API version is served by the declaration (`served = true` in `spec.versions` on a CRD, or listed in `servedVersions` on a `mkCRDRef`). Building custom resources with `scope.mkCR dep.out.crds.<Kind>` wires this dependency automatically.

When a Helm chart renders a custom resource belonging to another package's CRD (such as a `ServiceMonitor` enabled via chart `values`), check 21 fails because the chart resource has no dependency link to the `prometheus-operator` CRD marker. Disable the chart's built-in toggle and use `kix.monitor` or `scope.mkCR` in the Kix package instead.

```text
- [validate] ServiceMonitor apps/web (instance apps/web) does not depend on the CRD ref crd-ref-prometheus-operator-servicemonitors-monitoring-coreos-com for servicemonitors.monitoring.coreos.com (instance monitoring/prometheus-operator), which declares its kind, so the deploy can apply it before the CRD is served. ...
```

## Check 22: named Secret and ConfigMap keys

Check 22 compares named key reads against:
- `kix.run/secret-keys` on Secrets built with `scope.mkSecret`;
- `kix.run/import-secret-keys` on external Secret markers built with `scope.mkSecretRef`; and
- `data` and `binaryData` keys on `ConfigMap` resources in the build.

If a `scope.mkSecretRef` sets `keys = [ ]`, any container that references a specific key by name fails check 22. List the expected keys in `keys` so both check 22 and `kix deploy`'s pre-wave readiness check can verify them.

## How names and selectors resolve

References are resolved in the referring resource's own namespace, except for built-in cluster-scoped kinds (such as a `RoleBinding` referencing a `ClusterRole` or a `PersistentVolumeClaim` referencing a `StorageClass`). Selectors on `Service`, `NetworkPolicy`, and `PodDisruptionBudget` resources match pod templates in the same namespace only.

The following markers satisfy existence and selector checks:
- **Untracked references**: A name wrapped in [`kix.untrackedRef`](/docs/reference/composition-helpers/#facts-and-untrackedref) satisfies name checks in the referring resource's namespace regardless of kind.
- **Service and Secret ref markers**: `scope.mkServiceRef` and `scope.mkSecretRef` satisfy `Service` and `Secret` checks in their namespace, and the `selector` declared on `scope.mkServiceRef` counts as a pod template for checks 4 and 12. (Check 6 still requires a rendered `Service` with `clusterIP: None`.)
- **Platform and service imports**: `kix.mkImport` markers with `kind` and `out.name` satisfy checks for that kind. Built-in cluster-scoped kinds (`StorageClass`, `IngressClass`) imported via `kix.mkImport` satisfy references across all namespaces.
- **Operator-managed pods**: A `Service` (check 4) or `PodDisruptionBudget` (check 14) that selects pods created at runtime by an operator can set `metadata.annotations."kix.run/operand-of" = "<group>/<Kind>/<name>"`. The check passes when that custom resource exists in the same namespace in the build.

## Failure output

When one or more checks fail, cluster evaluation stops and lists every violation:

```text
error: Cluster 'homelab-kind' has 1 failed assertion(s):
  - [validate] Service data/postgres-metrics selector {"app":"pg"} matches no workload pod template
```

:::note[Explanation]
See [Cross-resource validation at cluster evaluation time](/docs/explanation/scorecard-levels/#cross-resource-validation-at-cluster-evaluation-time) for how these checks complement constructor validation and scorecard rules.
:::