# Helm Values Templates Everything under `cluster/resources/**/values.yaml.jinja`: image wiring, the `global` block, per-service sections, environment variables, probes, and replica counts. Read this when creating or editing a values template, or when rendered `values.yaml` produces invalid YAML or unexpected Helm behaviour. --- ## 1. Dual-layer rendering Every `values.yaml.jinja` is rendered **twice**: 1. **By Copier** — substitutes `{{ variable }}` from the answer file, producing `values.yaml`. 2. **By Helm** — ArgoCD passes that `values.yaml` to `helm template` at sync time. The file must therefore be valid Jinja2 *and* produce valid Helm values afterwards. A mistake in layer 1 fails the render; a mistake in layer 2 fails the sync, potentially hours later. Validate both (section 6). --- ## 2. Two categories of values file | Category | Shape | Used for | |---|---|---| | Operator / config resources | Simple YAML, minimal templating | CRD instances — OperatorGroup, Subscription, ClusterSecretStore | | Application workloads | Full Helm values with `global` + per-service sections | Platform services, MCP servers, notification, integration | Both use the same `render()` macro pattern. Do not mix the `global` shape and the standalone shape inside one file — pick the one matching the chart and follow it consistently. --- ## 3. Minimal structure (every file) ```jinja {% macro render() %} # --- values content here --- key: {{ domain.get('sub', {}).get('field', '') }} {% endmacro %} {%- set result = render() -%} {%- for line in result.split('\n') -%} {% if line | trim | length != 0 -%} {{ line }} {% endif -%} {%- endfor %} ``` --- ## 4. Shared-chart workload structure Services that share one Helm release read cluster-wide settings from a `global` block: ```yaml global: is_internal: "{{ applications.get('global', {}).get('is_internal', '') }}" ca_env: {{ applications.get('global', {}).get('ca_env', '') }} ca_secret_name: {{ applications.get('global', {}).get('ca_secret_name', '') }} host: {{ applications.get('global', {}).get('host', '') }} ca_host: {{ applications.get('global', {}).get('ca_host', '') }} suffix_url: {{ applications.get('global', {}).get('suffix_url', '') }} maintenance: "{{ applications.get('global', {}).get('maintenance', 'false') }}" node_selector_app: "{{ applications.get('global', {}).get('node_selector_app', '') }}" mongo: host: {{ applications.get('global', {}).get('mongo', {}).get('host', '') }} port: {{ applications.get('global', {}).get('mongo', {}).get('port', '') }} user: {{ applications.get('global', {}).get('mongo', {}).get('user', '') }} network: routes: [] : enabled: true image: name: {{ applications.get('', {}).get('image', {}).get('address', '') }} tag: {{ applications.get('', {}).get('image', {}).get('tag', '') }} replicas: {{ applications.get('', {}).get('replicas', 1) }} ``` `global` goes at the top of the file. All `global` sub-keys use the established names — do not introduce synonyms (`hostname` for `host`, `env` for `ca_env`); the chart templates read the exact keys. --- ## 5. Standalone workload structure Charts that do not share a `global` block use the standard Helm shape: ```yaml replicaCount: 1 image: repository: {{ domain.get('service', {}).get('image', {}).get('address', '') }} tag: {{ domain.get('service', {}).get('image', {}).get('tag', '') }} pullPolicy: Always imagePullSecrets: - name: "storage-images" service: type: ClusterIP port: 80 livenessProbe: httpGet: path: /health port: periodSeconds: 60 readinessProbe: httpGet: path: /health port: periodSeconds: 60 env: targetPort: : {{ domain.get('service', {}).get('env', {}).get('', '') }} ``` --- ## 6. Design rules ### Images - Always wire address and tag from Copier variables; never hardcode image coordinates. - Use `.get()` with an empty-string default for every image field. - `pullPolicy: Always` for mutable tags (`latest`, environment-specific). `IfNotPresent` only for immutable digest-pinned images. - Private registries need `imagePullSecrets` referencing the pre-existing secret — commonly `storage-images`. ### Booleans Booleans passed into `global` (`is_internal`, `maintenance`, `enable_instana`) are **quoted strings** because the chart templates treat them as strings. This is deliberate and differs from Copier feature flags, which are real YAML booleans. Keep the two straight: flags gate rendering, these gate chart behaviour. ### Environment variables - Map every env var from a Copier variable via `.get()`; never hardcode. - Group them under an `env:` key in the service's section. - Document *which* env vars are required, never their values — secrets come from an external secret store, not from a values file. ### Probes - Every application workload configures both `livenessProbe` and `readinessProbe`. - Prefer `httpGet` on `/health` where the app exposes it. - `periodSeconds: 60` is the default; change only when startup or health cadence demands it. ### Replicas - Default to `1` via the Copier variable: `replicas: {{ domain.get('svc', {}).get('replicas', 1) }}`. - Never hardcode — environments must scale independently. ### Blank-line stripping - Wrap the full body in `{% macro render() %}` … `{% endmacro %}`. - Always append the four-line stripping footer. - Do not rely on `-` whitespace control inside the macro body for the same purpose. --- ## 7. Adding a new values file 1. Create it at `cluster/resources/{{ '' if }}/values.yaml.jinja` — the expression identical to the `cluster/argocd/` side. 2. Open with `{% macro render() %}`. 3. Write the values body, sourcing every variable through a `.get()` chain. 4. Close with `{% endmacro %}` plus the stripping footer. 5. Confirm every referenced variable is declared in `copier.yaml`. 6. Render and validate. A starting template is in `assets/example-cluster-resource/resources/values.yaml.jinja`. --- ## 8. Validation ```bash # Render (Jinja2 layer) copier copy . /tmp/gitops-render --overwrite --defaults # Parse the rendered values (YAML layer) python3 -c "import yaml; yaml.safe_load(open('/tmp/gitops-render/cluster/resources//values.yaml'))" # Lint against the chart (Helm layer) helm lint -f /tmp/gitops-render/cluster/resources//values.yaml # See what Helm will actually produce helm template \ -f /tmp/gitops-render/cluster/resources//values.yaml ``` --- ## 9. Mistakes to avoid - Omitting the `render()` wrapper — blank lines from unexpanded blocks produce invalid YAML. - Hardcoding image tags, so every environment deploys the same version. - Putting secrets in a values file instead of an ExternalSecret / ClusterSecretStore. - Quoting numerics (replicas, ports) unless the chart explicitly expects a string. - Mixing the `global` pattern and the standalone pattern in one file. - Forgetting `imagePullSecrets` for a private-registry image — pods stall on `ImagePullBackOff`. - Renaming a `global` sub-key to a synonym the chart does not read — the value silently becomes empty rather than erroring.