# Adding a Cluster Resource End-to-End The integration walkthrough: one atomic change touching all three layers — Copier schema, ArgoCD Application, Helm values — plus the environment answer file. Read this when onboarding a brand-new operator, database, MCP component, or workload. For an isolated edit to one layer, read that layer's reference instead (`argocd-applications.md`, `copier-templates.md`, `helm-values.md`, `environments.md`). Starting templates: `assets/example-cluster-resource/`. --- ## Step 1 — Choose number, name, and flag ```text Band: from the table in SKILL.md §4 Number: next available in that band Name: - Flag: ._is_enabled (type: bool, default: false) ``` Confirm the number is free before writing anything: ```bash ls cluster/argocd/ cluster/resources/ | grep -o '[0-9]\{3\}' | sort -u ``` The directory names are Jinja2 expressions, so the numbers are still visible in a plain listing — no rendering needed for this check. --- ## Step 2 — Declare variables in `copier.yaml` ```yaml : _is_enabled: type: bool default: false help: "Enable -" : image: address: type: str default: "" help: "Container image for " tag: type: str default: "" help: "Container image tag for " # additional variables as the resource requires ``` The flag defaults to `false` so existing environments are unaffected until they opt in. --- ## Step 3 — Create the ArgoCD Application `cluster/argocd/{{ '-' if ._is_enabled }}/application.yaml.jinja` ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: - namespace: openshift-gitops finalizers: - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: {{ gitops.repo_url }} targetRevision: {{ gitops.branch }} path: cluster/resources/- helm: valueFiles: - values.yaml destination: server: https://kubernetes.default.svc namespace: syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true ``` --- ## Step 4 — Create the Helm values template `cluster/resources/{{ '-' if ._is_enabled }}/values.yaml.jinja` The conditional expression must be **character-identical** to step 3. ```jinja {% macro render() %} # values for - replicaCount: 1 image: repository: {{ .get('', {}).get('image', {}).get('address', '') }} tag: {{ .get('', {}).get('image', {}).get('tag', '') }} pullPolicy: Always # ... remaining values {% endmacro %} {%- set result = render() -%} {%- for line in result.split('\n') -%} {% if line | trim | length != 0 -%} {{ line }} {% endif -%} {%- endfor %} ``` --- ## Step 5 — Update environment answer files For each environment that should run this resource: ```yaml : _is_enabled: true : image: address: / tag: ``` Enable it in one environment first — typically the lowest — and promote after it syncs cleanly. Enabling everywhere in the same commit means a template bug reaches production in the same sync cycle. --- ## Step 6 — Validate both states ```bash # Flag off (defaults): the directories must NOT appear copier copy . /tmp/gitops-render-off --overwrite --defaults ls /tmp/gitops-render-off/cluster/argocd/ | grep "" # expect no output # Flag on: the directories must appear copier copy . /tmp/gitops-render-on --overwrite --defaults \ --data '._is_enabled=true' \ --data '..image.address=myregistry/myimage' \ --data '..image.tag=1.0.0' ls /tmp/gitops-render-on/cluster/argocd/ | grep "" # Both rendered files must parse python3 -c "import yaml; yaml.safe_load(open('/tmp/gitops-render-on/cluster/argocd/-/application.yaml'))" python3 -c "import yaml; yaml.safe_load(open('/tmp/gitops-render-on/cluster/resources/-/values.yaml'))" # Helm lint, when a chart path is available helm lint -f /tmp/gitops-render-on/cluster/resources/-/values.yaml ``` Testing the **off** state matters as much as the on state: a malformed conditional expression can render the directory unconditionally, silently deploying the new resource to every environment on the next sync. --- ## Step 7 — Consistency checklist - [ ] The three-digit number is unique and in the correct band. - [ ] The flag is declared in `copier.yaml` with `type: bool` and `default: false`. - [ ] The ArgoCD `metadata.name` matches the directory name exactly. - [ ] `spec.source.path` matches the `cluster/resources/` directory name exactly. - [ ] The conditional expressions in `cluster/argocd/` and `cluster/resources/` are identical. - [ ] `repoURL` and `targetRevision` use Jinja2 variables, not literals. - [ ] `resources-finalizer.argocd.argoproj.io` is present in `metadata.finalizers`. - [ ] `values.yaml.jinja` uses the `render()` macro and stripping footer. - [ ] All variable references use `.get()` with a type-appropriate default. - [ ] No secrets or credentials in any new file. - [ ] At least one environment's answer file enables and fully configures the resource. - [ ] Rendered and validated with the flag both off and on. --- ## Mistakes to avoid - Creating the ArgoCD Application without the matching `cluster/resources/` directory — ArgoCD fails with path-not-found. - Different conditional expressions on the two sides — one renders, the other is omitted. - Referencing a variable that was never declared in `copier.yaml` — the render fails for every environment, not just yours. - Hardcoding image address or tag in `values.yaml.jinja`. - Setting the new flag `default: true` — new resources are opt-in. - Committing without rendering; a broken template blocks every environment.