15 KiB
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_acmestyle; backup'pendingfor mail-stack BackupGroupzot—tls_endpoint_with_acmestyle; backup'pendingfor registry-stack BackupGrouppostgresql—database; backup'pendingfor 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 runtimecrio—stateless; container runtimerunc—stateless; OCI runtimecrun—stateless; OCI runtimeyouki—stateless; OCI runtimeos—stateless; base OS configurationkubernetes—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 distributionk8s-nodejoin—stateless; one-shot join operationvol_prepare—stateless; block-device preplonghorn_node_prep—stateless; node-level Longhorn prep
Infrastructure glue (controllers / CSI / network):
cilium—infrastructure_glue; CNI controllerhccm—infrastructure_glue; Hetzner cloud controllerhetzner_csi—infrastructure_glue; CSI driverdemocratic_csi—infrastructure_glue; NFS dynamic provisionerexternal_nfs—infrastructure_glue; NFS serverlocal_path_provisioner—infrastructure_glue; local-path provisionerfip—infrastructure_glue; floating IP attachmentetcd—infrastructure_glue; data captured by SystemBackupDef.etcdprivate_gateway—infrastructure_glue; Cilium gatewaybuildkit_runner—infrastructure_glue; ephemeral by design (ADR-039)
DNS providers:
coredns—dns_provider; zone files via SystemBackupDef.external_corednsexternal_dns—dns_provider; controller, records live in upstream providerresolv—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'pendingfor 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— declareconcernsdirectly in the workspace.nclfileops_controller.ncl,audit_mirror.ncl,radicle_seed.ncl,coredns_vpn.ncl,coredns_vpn_wrk1.ncl— workspace-declared, mostly reusingext.make_coredns(which carries inheritedconcernsfrom 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:
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 {):
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:
# 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)
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)
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)
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
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
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:
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:
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
- Declare
concerns | default = (_presets.tls_endpoint_with_acme {...}) & { manifest_hooks = {...} }indefaults.ncl(copy from forgejo/defaults.ncl). - Keep
manifest_plan.nclclean — no TLS steps (create-cf-secret, clusterissuer, certificate, referencegrant, patch-gateway-https, remove-gateway-https). - Export
TLS_HOOK_*vars inenv-<name>.j2. - Ensure
vars.nuoutputscf_secret_name,gateway_ip,tls_secret(without component prefix). - 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