Consolidates the five cogarchhubgitops-* source skills into one skill with scope routing: ArgoCD Applications, Copier templates, Helm values, environment answer files, and the end-to-end resource-addition walkthrough. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.0 KiB
Copier Templating Layer
The copier.yaml variable schema, .jinja template authoring, conditional directory names,
and the render() macro. Read this when adding a variable, editing any .jinja file, or
debugging a rendering failure.
1. How Copier renders this repo
The repository is a Copier template. copier copy (or copier update) against an answer
file causes Copier to:
- Read every variable definition from
copier.yaml. - Substitute values from
copier-answers.yamlinto every*.jinjafile. - Evaluate Jinja2 expressions used as directory names — a falsy result omits the whole subtree from the output.
- Write rendered files without the
.jinjaextension to the destination.
The rendered output is what ArgoCD reads from Git. The template source is what you edit. These are different trees; never confuse a rendering bug with a cluster bug.
2. Conditional directory names
{{ '210-eso-operator-group' if operators.eso_is_enabled }}
- Flag
true→ directory renders as210-eso-operator-group/. - Flag
false→ the directory and everything inside it is omitted.
This is the feature-flag mechanism for the entire repository. Consequences:
- No
{% if %}blocks are needed inside manifests, and none should be added. - The expression must evaluate to exactly the intended name string when truthy, and to a
falsy value when not.
{{ '210-x' if flag else '' }}and{{ '210-x' if flag }}both work; keep the shorter form for consistency. - The variable named in the expression must be the one declared in
copier.yaml— a typo producesUndefinedErroror, worse, a silently falsy value that drops the resource. cluster/argocd/andcluster/resources/must use identical expressions. Renaming a flag means editing both.
3. The render() macro pattern
Every values.yaml.jinja wraps its content in a macro and strips blank lines:
{% macro render() %}
key: {{ variable }}
other_key: {{ other_variable }}
{% endmacro %}
{%- set result = render() -%}
{%- for line in result.split('\n') -%}
{% if line | trim | length != 0 -%}
{{ line }}
{% endif -%}
{%- endfor %}
Mandatory for all values.yaml.jinja files. Unexpanded Jinja2 constructs leave blank lines
in the output; in YAML those can break block structure and produce a parse error at sync
time — long after the commit that caused it.
Do not try to solve the same problem with Jinja2 whitespace control ({%-/-%}) inside the
macro body. The footer loop is the agreed mechanism; mixing approaches makes the output
depend on which one wins.
4. Variable schema (copier.yaml)
Each variable declares:
type—str,bool,dict,yamldefault— used when no answer file supplies a valuehelp— shown during interactive prompting
Naming conventions:
| Pattern | Purpose |
|---|---|
<domain>.is_enabled |
Top-level flag for a whole domain (operators.is_enabled) |
<domain>.<sub>_is_enabled |
Sub-feature flag (operators.eso_is_enabled) |
<domain>.<sub>.image.address |
Container image repository path |
<domain>.<sub>.image.tag |
Container image tag |
<domain>.<sub>.env.<var> |
Application environment variable |
gitops.repo_url |
Git URL used in ArgoCD sources |
gitops.branch |
Git branch used in ArgoCD sources |
Rules:
- Declare every variable before any template references it — an undeclared reference fails the render for every environment.
- Give every variable a sensible
defaultso--defaultsrendering works without an answer file. - Feature flags are
type: bool, neverstr. - New flags default to
false— resources are opt-in. - Group variables under a domain object (
operators:,applications:). Never add isolated top-level variables.
5. Accessing variables in templates
{# Direct — raises UndefinedError if the key is missing #}
{{ operators.eso_is_enabled }}
{# Safe with default — preferred for any optional nested key #}
{{ applications.get('integration', {}).get('replicas', 1) }}
Use the .get() chain for anything that might be absent in some environment. A direct
reference to an optional key is a latent failure: it renders fine in the environment you
tested and breaks the one you did not.
Defaults inside .get() should match the field's type — '' for strings, a number for
numeric fields.
6. Adding a new variable
-
Open
copier.yaml. -
Add it under the correct domain key:
<domain>: <descriptor>_is_enabled: type: bool default: false help: "Enable <NNN>-<name>" <descriptor>: image: address: type: str default: "" help: "Container image for <name>" tag: type: str default: "" help: "Container image tag for <name>" -
Reference it from the appropriate
.jinjatemplate. -
Add its environment-specific value to each relevant
copier-answers.yaml. -
Render and validate.
7. Authoring rules
- Every new
.jinjavalues file uses therender()macro pattern. - Keep Jinja2 logic minimal — directories handle feature flags, templates handle substitution.
- No credentials, tokens, or secrets inside any template.
- Answer files stay YAML; never
.jsonor.env. _src_pathand_commitin answer files are Copier metadata — never hand-edited.
8. Debugging a render failure
# Parse every template without rendering — fastest way to find a syntax error
python3 -c "
from jinja2 import Environment
import pathlib
env = Environment()
for f in pathlib.Path('cluster').rglob('*.jinja'):
env.parse(f.read_text()); print('OK:', f)
"
# Full render with defaults
copier copy . /tmp/gitops-render-defaults --overwrite --defaults
# Full render with a specific environment
copier copy . /tmp/gitops-render-env --overwrite --answers-file <env>/copier-answers.yaml
# No .jinja may survive into the output
find /tmp/gitops-render-env -name "*.jinja" && echo "ERROR: .jinja files in output"
| Error | Cause |
|---|---|
UndefinedError: '<x>' is undefined |
Variable not declared in copier.yaml, or a direct reference to an optional nested key — switch to .get() |
| Directory missing from output | Flag false, or a typo in the variable name inside the conditional expression |
.jinja file in the output |
File is inside a directory Copier excluded, or the extension is misspelled |
| Rendered YAML fails to parse | Missing render() stripping footer leaving blank lines |
copier update refuses to run |
_src_path or _commit was hand-edited |
9. Mistakes to avoid
- Skipping the
render()blank-line-stripping wrapper. - Using string
"true"/"false"forboolfeature flags. - Adding isolated top-level variables instead of grouping under a domain.
- Renaming a flag on only one of
cluster/argocd/andcluster/resources/. - Direct
{{ variable }}access for optional nested keys instead of.get(). - Committing an answer file containing real secrets.