Catalog module standard
The contract every module under oci/catalog/ meets; the why is in
The Flux catalog.
Models: hello for structure and tests, argocd for chart values.
What a module ships
Section titled “What a module ships”| Part | Path | Checked by |
|---|---|---|
| The template | oci/catalog/<kebab-name>/resourceset.yaml, one ResourceSet named after the folder, in flux-system | check-catalog-clouds.sh; pr-static.yaml (render, strict kubeconform) |
| The schema entry | local.catalog in opentofu/bootstrap/catalog.tf, key <snake_name>, every attribute with its default, enabled included | check-catalog-clouds.sh; the kube validations |
| Per-attribute rules | kube validations in opentofu/bootstrap/variables.tf, each failing case in tests/validations.tftest.hcl | tofu test |
| The overlays | ../../catalog/<kebab-name>/resourceset.yaml in each offering oci/clusters/<cloud>/kustomization.yaml | check-catalog-clouds.sh, both directions |
| What a cloud adds | oci/clusters/<cloud>/<kebab-name>/patch.yaml, applied by that cloud’s overlay to the module’s ResourceSet: a step cloud inserted before workload behind a test op, steps appended after it, a dependsOn; no file on a cloud with nothing of its own (SOCLE-32) | check-catalog-clouds.sh: applied, aimed at its module, nothing else in the folder, no inputs.cloud under oci/catalog/; pr-static.yaml renders each patched module |
| Cloud binding | <snake_name> = ["aws", …] in catalog_clouds, one per line, when not on every cloud | check-catalog-clouds.sh; refused at plan elsewhere |
| Monitoring, for every module with a workload | Its pods annotated prometheus.io/scrape on a declared port (vmagent’s scrape of annotated pods), or a VMServiceScrape when it serves HTTPS only; oci/catalog/<kebab-name>/monitoring/: awesome-prometheus-alerts’ file when it has one, the project’s dashboard, and the socle’s rules.yaml for what awesome-prometheus-alerts lacks; applied by a Kustomization <kebab-name>-monitoring with retryInterval: 1m, on only while the module and monitoring both are (MONITORING-12) | upstream-monitoring.sh check (copies unchanged), check-rules.sh (vmalert -dryRun), both in pr-static.yaml; the module’s e2e asserts the Kustomization Ready |
| The e2e proof | oci/catalog/<kebab-name>/tests/e2e/chainsaw-test.yaml, optional values.yaml | check-catalog-clouds.sh; e2e.yaml runs it |
| The page | docs/catalog/<kebab-name>.md, from _template.md | rule 6 of CONTRIBUTING; the site build |
| The decisions | docs/decisions/<kebab-name>.md, from _template.md; not published on the site | review |
Template rules
Section titled “Template rules”-
inputsFromtheResourceSetInputProvidersocle; no inline inputs. -
fluxcd.controlplane.io/reconcile: << if inputs.modules.<m>.enabled >>enabled<< else >>disabled<< end >>on each object’s own annotations, never incommonMetadata. - One namespace, rendered by the module, carrying the toggle.
- Templates test values, never presence.
- Nothing that is one cloud’s under
oci/catalog/, noinputs.cloudthere: it goes in the cloud’spatch.yaml; never a new OpenTofu input. - Steps
namespace, thenworkload(the chart, its values, its monitoring), so a cloud can insertcloudbetween them. - The chart pinned exactly (OCI tag, or
HelmRepositoryversion), listed in Compatibility. - No inline
spec.values;valuesFrom:<module>-socle-values,<module>-cloud-values(optional: true, when a cloud has values for the chart),<module>-client-values, then thevalues_secretSecret (optional: true); ConfigMaps labelledreconcile.fluxcd.io/watch: Enabled. -
valuesandvalues_secretin the schema; secret-bearing paths refused at plan, each with a failingtofu test. -
resourcesTemplatewhen a block appears under a condition. - Cloud access as Crossplane resources in the cloud’s
cloudstep, with adependsOnoncrossplaneand an empty<module>-accessResourceSetthatdependsOnthe role with areadyExpr, so theworkloadstep waits for it; nothing underopentofu/<cloud>/. - CRDs that hold your objects kept when off, or the page says not (CRDs when a module is off).
- New attributes in
oci/.ci/inputs-sample.yaml;flux-operator build rsetrenders, kubeconform-strictpasses.
The e2e proof
Section titled “The e2e proof”e2e.yaml runs one job per (module, cloud) whose overlay lists the module,
each on its own floci with the fixture root .github/e2e/<cloud>/; the real
root is applied once, in root (<cloud>).
chainsaw-test.yaml holds one Test per phase, selected by label:
| Label | Runs | Holds |
|---|---|---|
phase: health | every job, the real root’s included | the ResourceSet Ready, the workload Available, the socle’s values live |
phase: module | the module’s job, after health | mutations, each a patch of the input provider socle; the last restores it |
phase: destroyed | after the uninstall | what must survive it |
cloud: any / cloud: <cloud> | every cloud, or one | a per-cloud assertion is its own Test |
platform: any / platform: floci | everywhere, or floci only | what only floci can state |
platform: gke | the GKE sandbox, run by hand; the floci CI never selects it | what only a real GKE can state; the Test carries its own spec.timeouts, at least apply 30s, assert 180s, error 60s, delete 60s, cleanup 120s and exec 30s: Chainsaw’s defaults are too short for calls through the DNS endpoint and Autopilot’s restarts (measured 2026-10-16) |
The module phase proves at least:
- every named attribute reaching its live object;
- the
valuesFromorder on a key the socle sets:valuesover it, thevalues_secretSecret (labelledreconcile.fluxcd.io/watch: Enabled) over that, both cleared; - off: everything garbage-collected, and the effect on dependent modules; on again;
- what it does against the cloud, where floci can show it.
Every step has a catch (describe, events, podLogs), and the off step
waits for an idle release (Ready, Released, no Reconciling).
Under the hood
Bindings: tag, cloud, cluster, floci_ip (and resourceset for the
generic suite); tests/e2e/values.yaml adds others through --values. After
the module’s tests the job sets kube.<module>.enabled = false (the
disabled suite asserts nothing rendered), restores the defaults, re-runs
root and health, requires an empty tofu plan -detailed-exitcode, then
uninstalls and runs the destroyed suites. Chainsaw operations come first;
a script only for what the cluster cannot show (a record in floci’s Route
53) or a catch that prints the node’s state. What floci cannot serve (Pod
Identity, today) is a step of its own, named as such.
Reviewer checklist
Section titled “Reviewer checklist”- Folder
oci/catalog/<kebab-name>/, key<snake_name>,enabledin the entry. -
.github/scripts/check-catalog-clouds.shpasses. - Every object, the namespace included, carries the toggle.
- The chart pinned exactly, its version in
docs/reference/compatibility.md. -
valuesFromsocle, cloud, client, Secret; the watch label on every ConfigMap. - Every refusal and validation has a failing
tofu testcase. - No
inputs.cloudunderoci/catalog/; a cloud’s additions in itspatch.yaml, its access before the workload. - Kept CRDs, or the page says they go.
-
oci/.ci/inputs-sample.yamlupdated; the render kubeconforms strictly. -
healthandmoduletests, acatchon every step, an idle-release gate on off. - The
moduletest overrides a socle key throughvalues, thenvalues_secret. - Page and decisions file exist; the page has
title,description,category,requires. - The page has a
kube-startblock and akube-fullblock matchingcatalog.tf.