provisioning/schemas/lib/concerns.ncl

171 lines
6.5 KiB
Text

# Service Concerns Umbrella — mandatory declarative surface in ComponentDef.
# Every component must declare what it does (or doesn't do) for each concern:
# tls, dns, certs, backup, observability, security. Each is one of:
# 'enabled <impl> — concern is implemented; impl carries the configuration
# 'disabled — explicitly opt-out, with a stated reason
# 'pending — implementation deferred, with a backlog reference
# 'inherited — copied from a parent component (e.g. odoo profile)
#
# The umbrella absorbs the loose fields that components carry today
# (tls_secret, cluster_issuer, cert{}, dns_internal, dns_records, …) into
# typed variants. Existing 'extensions/components/<x>/nickel/main.ncl helpers
# may continue to read the loose fields for backwards compatibility while
# also emitting `concerns` for new consumers.
let bp = import "backup_policy.ncl" in
{
# === Concern state ========================================================
# Discriminated union of concern states. Encoded as a record with a `kind`
# tag so multiple concerns can coexist in a single ServiceConcerns record
# (Nickel does not support algebraic data types directly).
ConcernState = {
kind | [| 'enabled, 'disabled, 'pending, 'inherited |],
# 'enabled — payload depends on the concern (tls.impl, dns.impl, …);
# callers thread the right impl type via the wrapper records below.
# 'disabled
reason | String | optional,
since | String | optional | doc "ISO date when concern was explicitly disabled",
# 'pending
backlog_ref | String | optional | doc "Identifier of the backlog/issue tracking the implementation",
target_iteration | String | optional,
# 'inherited
from | String | optional | doc "Name of the parent ComponentDef the concern is inherited from",
# 'enabled payload — exactly one of these is populated based on the concern
tls_impl | { .. } | optional,
dns_impl | { .. } | optional,
certs_impl | { .. } | optional,
backup_impl | { .. } | optional,
observability_impl | { .. } | optional,
security_impl | { .. } | optional,
},
# === Concern impl types ===================================================
# TLS implementation. Absorbs `tls_secret`, `cluster_issuer`, `tls_hostnames`.
TlsImpl = {
secret_name | String | doc "K8s Secret name where cert-manager stores the cert (was tls_secret)",
issuer_ref | String | doc "ClusterIssuer name (was cluster_issuer)",
hostnames | Array String | doc "Additional SANs (was tls_hostnames)" | default = [],
},
# DNS implementation. Absorbs `dns_internal` (private routes via gateway),
# `dns_records` (public records: domain/mx/spf/dmarc/dkim_selector/autoconfig),
# `dns_zone`, `acme_email`.
DnsRoute = {
name | String,
zone | String,
gateway | String | optional,
target | String | optional,
},
DnsRecordSpec = {
domain | String | optional,
hostname | String | optional,
mx | Array { priority | Number, value | String } | default = [],
spf | String | optional,
dmarc | { policy | [| 'none, 'quarantine, 'reject |], rua | String | optional, ruf | String | optional } | optional,
autoconfig | String | optional,
dkim_selector | String | optional,
extra | { .. } | doc "Free-form provider-specific records" | default = {},
},
DnsImpl = {
internal | Array DnsRoute | doc "Was dns_internal (dns_private.via_gateway/make_route)" | default = [],
public | DnsRecordSpec | optional | doc "Was dns_records",
zone | String | optional | doc "Was dns_zone",
acme_email | String | optional | doc "Was acme_email (only used when certs concern derives from this email)",
},
# Certificates implementation. Absorbs `cert = { acme_server, email, secret_ref, provider }`.
# Distinct from TLS: TLS = pedido al issuer; Certs = config del ACME issuer.
CertsImpl = {
acme_server | String,
email | String,
secret_ref | String | doc "DNS provider credentials secret reference",
provider | [| 'cloudflare, 'hetzner, 'aws, 'route53, 'digitalocean, 'gcp, 'azure |],
},
# Observability implementation. Surface only — deeper schemas land in a
# later iteration. Components most commonly declare 'pending here.
ObservabilityImpl = {
metrics | { enabled | Bool, port | Number | optional, path | String | default = "/metrics" } | default = { enabled = false },
logs | { enabled | Bool, sink | [| 'stdout, 'loki, 'journald |] | default = 'stdout } | default = { enabled = false },
traces | { enabled | Bool, otlp_endpoint | String | optional } | default = { enabled = false },
alerts | Array { name | String, expr | String, severity | [| 'info, 'warning, 'critical |] | default = 'warning } | default = [],
},
# Security implementation. Surface only.
SecurityImpl = {
network_policy | String | optional | doc "Reference to a NetworkPolicy resource",
pod_security | [| 'restricted, 'baseline, 'privileged |] | optional,
rbac | String | optional | doc "Reference to RBAC bundle",
},
# === Builders =============================================================
# Helper functions for components and migrations to construct ConcernState
# values without repeating the discriminated-union plumbing.
enabled_tls = fun impl => {
kind = 'enabled,
tls_impl = impl,
},
enabled_dns = fun impl => {
kind = 'enabled,
dns_impl = impl,
},
enabled_certs = fun impl => {
kind = 'enabled,
certs_impl = impl,
},
enabled_backup = fun impl => {
kind = 'enabled,
backup_impl = impl,
},
enabled_observability = fun impl => {
kind = 'enabled,
observability_impl = impl,
},
enabled_security = fun impl => {
kind = 'enabled,
security_impl = impl,
},
disabled = fun reason_text => {
kind = 'disabled,
reason = reason_text,
},
pending = fun reason_text backlog => {
kind = 'pending,
reason = reason_text,
backlog_ref = backlog,
},
inherited = fun parent_name => {
kind = 'inherited,
from = parent_name,
},
# === Top-level umbrella ===================================================
ServiceConcerns = {
tls | ConcernState,
dns | ConcernState,
certs | ConcernState,
backup | ConcernState,
observability | ConcernState,
security | ConcernState,
},
}