The Flux catalog
The catalog is the part of the socle that runs in the cluster: one Flux
Operator ResourceSet per module, shipped in the signed artifact, rendered
from the inputs OpenTofu validated. What you write is in
Configure a cluster; the rules a module follows are
in the Catalog module standard.
Three roles
Section titled “Three roles”| Role | Owner | Holds |
|---|---|---|
| Inputs | OpenTofu, in your root | cloud, cluster identity, socle version, kube |
| Templates | the artifact, ghcr.io/do-now-io/socle/flux-modules | one ResourceSet per module, one overlay per cloud |
| Rendering | the Flux Operator, in the cluster | reads the inputs, renders, reconciles, garbage-collects |
OpenTofu knows the catalog only as a schema, the artifact knows no client, and the operator joins them: no client repository, no templating in OpenTofu.
What OpenTofu deposits
Section titled “What OpenTofu deposits”opentofu/bootstrap applies, in order:
| # | Release or resource | Where | Why it is here |
|---|---|---|---|
| 1 | cilium | aws, azure | the cluster has no CNI (Cilium before Flux) |
| 2 | coredns | aws | removing the VPC CNI removed CoreDNS too |
| 3 | EKS add-ons: Pod Identity Agent, snapshot controller, EBS CSI, EFS CSI (off by default) | aws | the agent must run before any module asks for an identity |
| 4 | flux-operator | every cloud | pinned exactly (operator_version, 0.60.0) |
| 5 | flux-instance | every cloud | the controllers, no sync block, built from the operator’s embedded distribution (SOCLE-33) |
| 6 | socle | every cloud | the envelope, two objects below |
ResourceSetInputProviderflux-system/socle: your configuration, as OpenTofu validated and normalised it. Every module reads this one provider.ResourceSetflux-system/socle-root: renders theOCIRepositorysocle(URL, tag, optional pull secret, cosignverify) and theKustomizationsocleon./clusters/<cloud>. It is Ready only when every module is: that, not a green apply, is convergence.
Under the hood
The envelope is a local chart, manifests/,
whose only Helm expression is toYaml .Values.inputs; the << >> delimiters
are the operator’s. socle-root uses resourcesTemplate, a raw string,
because the optional secretRef adds or drops a whole key. The socle
Kustomization has prune: true, wait: true and deletionPolicy: WaitForTermination. The envelope’s chart version equals VERSION: the helm
provider re-applies a local chart only when its version or values change.
What the inputs hold (defaultValues, the bootstrap’s inputs output):
| Path | What it is |
|---|---|
cloud | aws, gcp, azure or scaleway; picks ./clusters/<cloud> |
cluster.name, .environment, .owner, .region, .accountId | the cluster’s identity; accountId on aws only |
cluster.projectId, .projectNumber | the GCP project the cluster runs in, on gcp only: a module’s Workload Identity principal names its number and the pool named after its id, and a bucket’s name carries the number |
socle.url, .version, .pullSecret | where the artifact is pulled from |
cosign.issuer, .subject | the identity the signature must match |
modules.<module>.<attribute> | every module, every attribute, your values over the defaults |
cilium.installed, .gatewayApi, .hubble | what the bootstrap decided about the network |
gateway.className, .shared, .namespace, .certificateArn | the GatewayClass, whether the shared Gateways exist, their certificate on aws |
gateway.certificateMap, .regionalCertificate | on gcp only: the Certificate Manager map of the public Gateway, and the certificate of the private one |
storage.snapshots | the CSI snapshot controller and its CRDs are there: on aws from the add-on the bootstrap installed, on gcp always (GKE manages the PD CSI driver and the snapshot CRDs) |
The schema is catalog.tf: kube is
any, validated against it at plan,
then merged over the defaults, so every attribute exists and templates test
values, never presence. What Flux deploys per cloud is declared again in
oci/clusters/<cloud>/kustomization.yaml;
check-catalog-clouds.sh
fails CI when the two disagree.
How a module template is written
Section titled “How a module template is written”Each rule was measured on a live cluster; the checklist is the Catalog module standard.
- One provider:
inputsFromtheResourceSetInputProvidersocle, never inline inputs. - The toggle is on each resource:
fluxcd.controlplane.io/reconcile: << if inputs.modules.<m>.enabled >>enabled<< else >>disabled<< end >>on every object’s own annotations. IncommonMetadatait is not templated. - What is a cloud’s, that cloud adds: the module in
oci/catalog/never readsinputs.cloudand is written as steps,namespacethenworkload; a DNS provider, a credential, a role, a Gateway’s shape isoci/clusters/<cloud>/<module>/patch.yaml, which inserts a stepcloudbetween them or appends one (SOCLE-32). Never from OpenTofu (helloshows it). - A module carries its own cloud access as Crossplane resources (Module IAM).
- Deleting a
ResourceSetuninstalls everything it rendered.
How values merge
Section titled “How values merge”The HelmRelease has no inline values:; its valuesFrom reads, in order:
<module>-socle-values, a ConfigMap with the socle’s defaults;<module>-cloud-values, a ConfigMap with the cloud’s, from the cloud’s step, when it has values for the chart (optional: true);<module>-client-values, a ConfigMap fromkube.<module>.values;- the Secret named in
values_secret,optional: true.
Later entries win, so yours beat the socle’s, and values beats a named
attribute on the same key. Secrets are refused in values, which reaches the
state and a ConfigMap.
Under the hood
helm-controller merges spec.values over valuesFrom, so a socle default
written inline would beat you; moved into the first reference, it loses.
Every ConfigMap carries the module’s toggle and reconcile.fluxcd.io/watch: Enabled, so a change reconciles at once. An overlay cannot JSON-patch one
value inside a values document, so the cloud’s values are a ConfigMap of
their own, rendered by the step the cloud adds before the chart’s.
CRDs when a module is off
Section titled “CRDs when a module is off”Turning a module off garbage-collects what it rendered. Its CRDs:
| Module | CRDs when off | Why |
|---|---|---|
gateway_api | kept | deletionPolicy: Orphan: deleting a CRD deletes every Gateway and route |
external_secrets | kept | helm.sh/resource-policy: keep: your ExternalSecrets and stores outlive it |
keda | kept | helm.sh/resource-policy: keep: your ScaledObjects outlive it |
velero | kept | cleanUpCRDs: false: a re-enable re-syncs Backups from the bucket |
argocd_image_updater | kept | helm.sh/resource-policy: keep: your ImageUpdater objects outlive it, inert; delete them first, their finalizer needs the controller |
crossplane | kept | the core applies them outside Helm; module roles still declared are orphaned in the cloud |
kyverno | deleted | the chart’s default: every Kyverno policy goes with them |
The same holds for a full Uninstall.
Uninstall order
Section titled “Uninstall order”tofu destroy removes the envelope, then the instance, then the operator.
deletionPolicy: WaitForTermination on the socle Kustomization holds the
envelope until every catalog ResourceSet is gone; without it, five survived
with their finalizer on CI’s first destroy. The procedure is in
Uninstall.
Measured
Section titled “Measured”- 2026-09-21, floci 1.5.34, flux-operator 0.60.0, Flux 2.9.5: apply 62 s; a value change rendered in 5 s; a module off garbage-collected in 10 s; second plan empty.
- 2026-09-30,
e2e.yaml, floci 2.1.0:socle-rootReady 46 to 56 s after Chainsaw starts;tofu destroy -target=module.socleleaves nothing, in 28 s.