Substitution Expansion in `workload validate`
ksail workload validate runs kubeconform against your Kubernetes manifests to catch schema errors before they reach the cluster. When your manifests use Flux postBuild substitutions (${VAR}, ${VAR:-default}, ${VAR:=default}), the validator needs concrete values to produce valid YAML for schema checking.
This guide explains how the expansion works, why environment variables are not read, and how to write substitutions that validate cleanly.
Why Local Environment Variables Are Not Used
Section titled “Why Local Environment Variables Are Not Used”workload validate is designed to be deterministic — the same manifests should produce the same validation result regardless of the machine, user, or environment they are run on. Reading from local environment variables would break this:
- A manifest that validates on a developer’s machine (because
$REPLICA_COUNTis set) might fail in CI (where it is not set). - Conversely, an incorrectly typed default (
replicas: "3"instead ofreplicas: 3) might slip through validation on a machine where the env var overrides the bad default.
Instead, KSail uses typed schema-aware placeholders derived from the Kubernetes JSON schema. Validation catches type mismatches at schema-check time, not at deploy time.
Expansion Rules
Section titled “Expansion Rules”${VAR} — Bare Variable Reference (No Default)
Section titled “${VAR} — Bare Variable Reference (No Default)”A bare ${VAR} is replaced with a typed placeholder based on the JSON schema type of the field it occupies.
| Schema type | Placeholder value |
|---|---|
string | "placeholder" |
integer | 0 |
number | 0.0 |
boolean | true |
| unknown / no schema | "placeholder" |
Example:
spec: replicas: ${REPLICA_COUNT} # integer field → 0 image: ${IMAGE_REF} # string field → "placeholder" enableFeature: ${FEATURE_ON} # boolean field → trueAfter expansion:
spec: replicas: 0 image: placeholder enableFeature: true${VAR:-default} and ${VAR:=default} — Variable With Default
Section titled “${VAR:-default} and ${VAR:=default} — Variable With Default”When a default is provided (either :- or := syntax), the default value is used as-is and coerced to the schema type.
spec: replicas: ${REPLICA_COUNT:-3} # integer field → 3 (not "3") name: ${APP_NAME:-my-app} # string field → "my-app" debug: ${DEBUG_MODE:-false} # boolean field → falseMixed-Text Substitution
Section titled “Mixed-Text Substitution”When a variable reference is embedded inside a larger string, the result is always a string:
spec: hostname: ${ENV}-app.example.com # → "placeholder-app.example.com" image: myrepo/${IMAGE_NAME:-nginx}:latest # → "myrepo/nginx:latest"Schema Inference
Section titled “Schema Inference”KSail loads JSON schemas from the kubeconform disk cache (populated on previous workload validate runs). Schema files are looked up from the OS user cache directory (for example ~/.cache/ksail/kubeconform/), or from ${TMPDIR}/ksail/kubeconform when the user cache directory is unavailable (such as in some CI environments).
- Core Kubernetes resources (Deployment, Service, ConfigMap, …): typed via kubernetes-json-schema when the relevant schema is present in the kubeconform disk cache
- CRDs (FluxCD, cert-manager, …): typed via the CRDs-catalog when their schemas are present in the kubeconform disk cache
- Resources without a cached schema (including core kinds or CRDs whose schemas have not yet been cached): all bare
${VAR}expand to"placeholder"(string)
Practical Guidance
Section titled “Practical Guidance”Avoid False Validation Failures
Section titled “Avoid False Validation Failures”If workload validate reports a type error for a field that uses ${VAR} without a default, add a meaningful default of the correct type:
# Before — bare ${VAR} expands to "placeholder" (string), fails integer validationspec: replicas: ${REPLICA_COUNT}
# After — default 2 is correctly typed as an integerspec: replicas: ${REPLICA_COUNT:-2}Verify Your Defaults Are Well-Typed
Section titled “Verify Your Defaults Are Well-Typed”When using ${VAR:-default}, ensure the default value matches the field’s expected type. Incorrect defaults will cause validation failures even if the env var is always set at runtime:
# This will fail validation: "2" is a string, but replicas expects an integerspec: replicas: ${REPLICA_COUNT:-"2"}
# Correct: unquoted numeric defaultspec: replicas: ${REPLICA_COUNT:-2}Use workload validate Before Pushing
Section titled “Use workload validate Before Pushing”Combine workload validate with workload push in your workflow to catch schema errors before they enter the GitOps pipeline:
# Validate first, then push if cleanksail workload validate && ksail workload pushThis is the same pattern the ksail-cluster composite GitHub Action uses when validate: "true" is set. See PR Preview Clusters for the full CI/CD workflow.
Multi-Environment Validation
Section titled “Multi-Environment Validation”Use --config to validate against the settings of a specific environment:
# Validate against staging schema/constraintsksail --config ksail.staging.yaml workload validate
# Validate against productionksail --config ksail.prod.yaml workload validateSee Multi-Environment Workflows for how to structure per-environment config files.
Supported Substitution Syntax
Section titled “Supported Substitution Syntax”KSail supports the standard Flux postBuild variable syntax:
| Syntax | Behavior during validation |
|---|---|
${VAR} | Typed placeholder (schema-derived) |
${VAR:-default} | default value, coerced to schema type |
${VAR:=default} | default value, coerced to schema type |
${VAR} in mixed text | Replaced with "placeholder" (string) |
Other Flux substitution forms (e.g., shell-style $VAR without braces, $(VAR) command substitution) are not expanded by KSail and are left as-is.