# 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 — 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//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, }, }