provisioning-catalog/components/MIGRATION_CONCERNS.md

346 lines
15 KiB
Markdown

# ServiceConcerns Migration — Component Reference
This document describes the mechanical pattern for migrating each component
under `provisioning/extensions/components/<X>/nickel/` to declare a
`ServiceConcerns` umbrella (ADR-008, ADR-009).
## Status
Components migrated as of this writing — patrón aplicado y validado:
**TLS endpoints / databases:**
- `docker_mailserver``tls_endpoint_with_acme` style; backup `'pending` for mail-stack BackupGroup
- `zot``tls_endpoint_with_acme` style; backup `'pending` for registry-stack BackupGroup
- `postgresql``database`; backup `'pending` for workspace-level BackupPolicy with dump_strategy
**Storage backends / cert infrastructure:**
- `longhorn``infra_storage_managed`; backup `'disabled` (engine state via SystemBackupDef.longhorn_engine)
- `cert_manager``infra_storage_managed`; backup `'disabled` (cluster_resources via SystemBackupDef.tls_certmanager)
**Stateless runtimes / OS / kernel layer:**
- `containerd``stateless`; container runtime
- `crio``stateless`; container runtime
- `runc``stateless`; OCI runtime
- `crun``stateless`; OCI runtime
- `youki``stateless`; OCI runtime
- `os``stateless`; base OS configuration
- `kubernetes``stateless`; kubeadm-managed CP/kubelet (state in etcd via SystemBackupDef.etcd, PKI via SystemBackupDef.k8s_certs)
- `k0s``stateless`; data_dir via SystemBackupDef.host_configs when k0s is the active distribution
- `k8s-nodejoin``stateless`; one-shot join operation
- `vol_prepare``stateless`; block-device prep
- `longhorn_node_prep``stateless`; node-level Longhorn prep
**Infrastructure glue (controllers / CSI / network):**
- `cilium``infrastructure_glue`; CNI controller
- `hccm``infrastructure_glue`; Hetzner cloud controller
- `hetzner_csi``infrastructure_glue`; CSI driver
- `democratic_csi``infrastructure_glue`; NFS dynamic provisioner
- `external_nfs``infrastructure_glue`; NFS server
- `local_path_provisioner``infrastructure_glue`; local-path provisioner
- `fip``infrastructure_glue`; floating IP attachment
- `etcd``infrastructure_glue`; data captured by SystemBackupDef.etcd
- `private_gateway``infrastructure_glue`; Cilium gateway
- `buildkit_runner``infrastructure_glue`; ephemeral by design (ADR-039)
**DNS providers:**
- `coredns``dns_provider`; zone files via SystemBackupDef.external_coredns
- `external_dns``dns_provider`; controller, records live in upstream provider
- `resolv``stateless`; /etc/resolv.conf manager
**TLS endpoints with ACME:**
- `forgejo``tls_endpoint_with_acme`; backup `'pending` (Git+DB+actions)
- `woodpecker``tls_endpoint_with_acme`; backup `'pending` (DB+agent state)
- `stalwart``tls_endpoint_with_acme`; backup `'pending` (mail data+DB)
- `nats` — internal cluster service; backup `'pending` (JetStream state)
- `odoo` — bespoke; backup `'pending` for odoo-stack BackupGroup
**Databases:**
- `mariadb``database`; backup `'pending` (dump_strategy at workspace level)
- `surrealdb``database`; backup `'pending` (dump_strategy at workspace level)
**VPN gateway:**
- `wireguard` — stateless; peer keys + config in git/SOPS
**Total: 37 of 38 components migrated.** Only `backup_manager` is excluded —
it is the subsystem itself and does not consume its own ServiceConcerns schema.
**No components pending migration with `extensions/` directory.**
Components without `extensions/components/<name>/` directory (workspace-only):
- `prometheus.ncl`, `grafana.ncl`, `loki.ncl`, `vector.ncl` — declare `concerns`
directly in the workspace `.ncl` file
- `ops_controller.ncl`, `audit_mirror.ncl`, `radicle_seed.ncl`,
`coredns_vpn.ncl`, `coredns_vpn_wrk1.ncl` — workspace-declared, mostly
reusing `ext.make_coredns` (which carries inherited `concerns` from coredns).
## Reusable presets
`provisioning/schemas/lib/concerns_presets.ncl` exports 7 archetype presets that
defaults.ncl can import directly to avoid duplicating the same 9-line block
across components. Components that match an archetype exactly should use:
```nickel
let _presets = (import "schemas/lib/concerns_presets.ncl").presets in
{
<component> | default = {
# ... existing fields ...
concerns | default = _presets.<archetype>,
},
}
```
**Components using presets directly (8):** `containerd`, `crio`, `runc`, `crun`,
`youki`, `os` (`stateless`); `cilium`, `hccm` (`infrastructure_glue`).
**Components using preset + override** (`_presets.<name> & { … | force = … }`)
**(22):** `k8s-nodejoin`, `vol_prepare`, `resolv`, `wireguard`,
`longhorn_node_prep`, `kubernetes`, `k0s` (stateless variants);
`hetzner_csi`, `democratic_csi`, `external_nfs`, `local_path_provisioner`,
`fip`, `private_gateway`, `buildkit_runner`, `etcd` (infrastructure_glue);
`longhorn`, `cert_manager` (infra_storage_managed); `coredns`, `external_dns`
(dns_provider); `postgresql`, `mariadb`, `surrealdb` (database).
**Components using parametric preset** (`_presets.tls_endpoint_with_acme {…}`)
**(6):** `zot`, `docker_mailserver`, `forgejo`, `woodpecker`, `stalwart`, `odoo`.
**Of these, 4 also declare `manifest_hooks` in their `defaults.ncl`** (standard
gateway-based TLS lifecycle via `catalog/lib/tls-hook-methods.sh`):
`forgejo`, `wordpress_site`, `odoo`, `rustelo_website`.
`zot`, `docker_mailserver`, `stalwart` use custom TLS lifecycle (own lib methods)
and do NOT declare `manifest_hooks` — the preset does not carry hooks.
**Components with full inline blocks (1):** `nats` (internal cluster service,
distinct from public TLS endpoints).
**Total: 36 of 37 migrated components use presets** (97%).
## Mechanical pattern
For each `provisioning/extensions/components/<X>/nickel/`:
### Step 1 — `contracts.ncl`
Add the import at the top of the file (above the outer `{`):
```nickel
let _concerns_lib = import "schemas/lib/concerns.ncl" in
```
Inside the component schema record (before the trailing `..` if it has one,
or as the last field otherwise) add:
```nickel
# ServiceConcerns umbrella (ADR-008). See defaults.ncl for the populated value.
concerns | _concerns_lib.ServiceConcerns | optional,
```
If the contract is a closed record (no `..`), also add `..` so legacy
`mode`/`operations`/`live_check` fields are accepted by the contract.
### Step 2 — `defaults.ncl`
Add a `concerns | default = { ... }` block before the closing `}` of the
default record. Choose the variant matching the component's archetype.
#### Stateless preset (runtime/OS)
```nickel
concerns | default = {
tls = { kind = 'disabled, reason = "no TLS termination at this layer" },
dns = { kind = 'disabled, reason = "no DNS records owned by this component" },
certs = { kind = 'disabled, reason = "no ACME issuer required" },
backup = {
kind = 'disabled,
reason = "stateless: configuration in git, no runtime data to capture",
},
observability = { kind = 'pending, reason = "ObservabilityImpl iteration deferred", backlog_ref = "OBS-001" },
security = { kind = 'pending, reason = "SecurityImpl iteration deferred", backlog_ref = "SEC-001" },
},
```
#### Infrastructure glue (controllers/operators)
```nickel
concerns | default = {
tls = { kind = 'disabled, reason = "controller-level RBAC, not TLS endpoint" },
dns = { kind = 'disabled, reason = "no DNS records owned by this component" },
certs = { kind = 'disabled, reason = "no ACME issuer required" },
backup = {
kind = 'disabled,
reason = "state in K8s API captured by SystemBackupDef.cluster_resources",
},
observability = { kind = 'pending, reason = "ObservabilityImpl iteration deferred", backlog_ref = "OBS-001" },
security = { kind = 'pending, reason = "SecurityImpl iteration deferred", backlog_ref = "SEC-001" },
},
```
#### TLS endpoint with ACME (public service)
```nickel
concerns | default = {
tls = {
kind = 'enabled,
tls_impl = {
secret_name = "<COMPONENT>-tls",
issuer_ref = "letsencrypt-prod",
hostnames = [],
},
},
dns = {
kind = 'enabled,
dns_impl = {
internal = [],
zone = "",
},
},
certs = {
kind = 'enabled,
certs_impl = {
acme_server = "https://acme-v02.api.letsencrypt.org/directory",
email = "",
secret_ref = "",
provider = 'cloudflare,
},
},
backup = {
kind = 'pending,
reason = "BackupPolicy declared at workspace level",
backlog_ref = "BACKUP-<COMPONENT>-001",
},
observability = { kind = 'pending, reason = "ObservabilityImpl iteration deferred", backlog_ref = "OBS-001" },
security = { kind = 'pending, reason = "SecurityImpl iteration deferred", backlog_ref = "SEC-001" },
},
```
#### Database
```nickel
concerns | default = {
tls = { kind = 'disabled, reason = "internal cluster service, ingress-level TLS handled separately" },
dns = { kind = 'disabled, reason = "no public DNS records" },
certs = { kind = 'disabled, reason = "no ACME issuer required" },
backup = {
kind = 'pending,
reason = "BackupPolicy with database scope + dump_strategy declared at workspace level",
backlog_ref = "BACKUP-DB-<COMPONENT>-001",
},
observability = { kind = 'pending, reason = "ObservabilityImpl iteration deferred", backlog_ref = "OBS-001" },
security = { kind = 'pending, reason = "SecurityImpl iteration deferred", backlog_ref = "SEC-001" },
},
```
### Step 3 — Verify
```bash
cat > /tmp/check_<X>.ncl <<EOF
let mod = import "extensions/components/<X>/nickel/defaults.ncl" in
mod.<x>.concerns
EOF
nickel export --import-path provisioning /tmp/check_<X>.ncl
```
Expected output: a JSON object with `tls`, `dns`, `certs`, `backup`,
`observability`, `security` keys, each with `kind` and matching payload.
## Workspace-level overrides
After per-component defaults are populated, the workspace declarations in
`infra/<workspace>/components/<x>.ncl` can override individual concerns. The
most common override is to bring `backup` from `'pending` to `'enabled` with
a real `BackupPolicy` referencing `infra/<workspace>/lib/backup_policies.ncl`
presets.
Example — workspace activates backup for a component with full BackupPolicy:
```nickel
let bp_lib = import "../../lib/backup_policies.ncl" in
let ext = import "extensions/components/<X>/nickel/main.ncl" in
{
<x> = ext.make_<x> {
# ... existing fields ...
concerns = ext.defaults.<x>.concerns & {
backup = {
kind = 'enabled,
backup_impl = {
provider = { name = "restic" },
destinations = [bp_lib.destinations.hetzner_primary, bp_lib.destinations.b2_replica],
encryption = bp_lib.encryption.component_key "<x>",
schedule = { kind = 'cron, cron_expr = "0 3 * * *" },
retention = bp_lib.retention.gold,
scopes = [
{ kind = 'service_full, name = "main", paths = ["/var/lib/<x>"] },
],
tag_strategy = bp_lib.tag_strategy.component "<x>",
},
},
},
},
}
```
## Op-level manifest hooks (TLS gateway lifecycle)
Some cluster components require lifecycle steps that are structurally identical
across instances — Cloudflare secret, ClusterIssuer, Certificate, ReferenceGrant,
Gateway patch. Rather than repeating them in every `manifest_plan.ncl`, these
components declare `manifest_hooks` inside their `concerns` record in `defaults.ncl`.
The Nu bundle builder (`comp-build-cluster-bundle`) merges hooks from two sources:
```
effective_pre = concerns.manifest_hooks.op.pre ++ manifest_plan.hooks.op.pre
effective_post = manifest_plan.hooks.op.post ++ concerns.manifest_hooks.op.post
```
Generated `run-<op>.sh` executes: `effective_pre ++ plan_steps ++ effective_post`.
### Which components use manifest_hooks
Only components that deploy via the **standard gateway-based TLS lifecycle**
(cert-manager ClusterIssuer + Gateway API HTTPRoute + Cloudflare DNS-01):
- `forgejo`, `wordpress_site`, `odoo`, `rustelo_website`
Components with custom TLS lifecycle (`docker_mailserver`, `stalwart`, `zot`)
use the preset for concerns compliance only — no manifest_hooks.
### Required env vars for hook-based TLS components
Each component's `env-<name>.j2` must export these vars so that
`catalog/lib/tls-hook-methods.sh` can execute the hooks at deploy time:
```bash
TLS_HOOK_CF_SECRET_NAME=<cf-secret-name> # e.g. dns-jesusperez-pro
TLS_HOOK_GATEWAY_NAME=<gateway-name> # e.g. libre-wuji
TLS_HOOK_GATEWAY_NS=<gateway-namespace> # e.g. kube-system
TLS_HOOK_LISTENER_NAME=https-<service-name> # e.g. https-wordpress-diegodelgado
TLS_HOOK_DOMAIN=<primary-domain> # e.g. diegodelgado.es
TLS_HOOK_TLS_SECRET=<tls-secret-name> # e.g. wordpress-diegodelgado-tls
TLS_HOOK_NAMESPACE=<service-namespace> # e.g. diegodelgado
TLS_HOOK_CLUSTER_ISSUER=<issuer-name> # e.g. letsencrypt-prod-diegodelgado
```
These are populated from Tera variables that `vars.nu` must expose (no component
prefix): `cf_secret_name`, `gateway_ip`, `tls_secret`, `dns_zone`.
### Adding a new gateway-TLS component
1. Declare `concerns | default = (_presets.tls_endpoint_with_acme {...}) & { manifest_hooks = {...} }` in `defaults.ncl` (copy from forgejo/defaults.ncl).
2. Keep `manifest_plan.ncl` clean — no TLS steps (create-cf-secret, clusterissuer, certificate, referencegrant, patch-gateway-https, remove-gateway-https).
3. Export `TLS_HOOK_*` vars in `env-<name>.j2`.
4. Ensure `vars.nu` outputs `cf_secret_name`, `gateway_ip`, `tls_secret` (without component prefix).
5. No changes to lib.sh — TLS methods live in `catalog/lib/tls-hook-methods.sh`.
## Reference
- ADR-008: Service Concerns Umbrella
- ADR-009: Progressive Concerns Coverage
- ADR-010: Backup Architecture
- ADR-011: Multi-Destination Custody
- Schema: `provisioning/schemas/lib/concerns.ncl`
- Presets: `provisioning/schemas/lib/concerns_presets.ncl`
- Hook methods: `catalog/lib/tls-hook-methods.sh`
- Workspace defaults: `workspaces/libre-wuji/infra/lib/concerns_defaults.ncl`