/** * Who owns a module-config value: the operator, or celilo. * * A module's manifest declares a `source` for every variable it owns. `user` * means the operator supplies it. Every other source — `capability`, `system`, * `infrastructure`, `terraform` — means celilo computes it from somewhere else: * another module's published capability data, system config, the selected * infrastructure, a Terraform output. * * That distinction has to be shared rather than re-derived per command, because * it was previously spelled differently in each place and the disagreements were * silent. `module config set` refused only `source: infrastructure`, so setting * a `capability`- or `system`-sourced value REPORTED SUCCESS, wrote the row, and * was then discarded on the next deploy — * `celilo module config set authentik auth_url …` being the live example. And * `module config get` printed every row flat, so a value celilo derived was * indistinguishable from one the operator had chosen. * * ## `derive_from` does not mean derived * * The tempting shortcut is "it has a `derive_from` template, so celilo computes * it". That is wrong, and expensively so. `iptables` declares: * * - name: firewall_ip * source: user * derive_from: "$machine:ipAddress" * * `$machine:` derivations are answered by the config interview — they seed a * default the operator confirms — not by template resolution. The row is * operator config. Treating it as derived would refuse an operator's attempt to * correct their own firewall address, and a migration that deleted rows on the * same test would blind the trusted-sources audit that reads it. * * `source` is the authority. Nothing else is. */ import type { ModuleManifest, VariableDeclare } from '../manifest/schema'; /** * Does celilo compute this variable, rather than the operator supply it? * * An ABSENT source reads as the operator's. The manifest schema requires * `source`, so absent means a malformed or pre-schema manifest sitting in * `modules.manifest_data` — and for those the question is which way to be * wrong. Guessing "derived" refuses an operator's attempt to set their own * config with a message insisting celilo owns a value nothing computes, which * is unanswerable. Guessing "user" preserves what celilo did before this * predicate existed, when only `infrastructure` was refused. */ export function isDerivedVariable(variable: Pick): boolean { return variable.source !== undefined && variable.source !== 'user'; } /** Every variable a module's manifest declares, indexed by name. */ export function declaredVariables(manifest: ModuleManifest): Map { return new Map((manifest.variables?.owns ?? []).map((variable) => [variable.name, variable])); } /** * A one-line explanation of where a derived value comes from, for output an * operator reads. Says which upstream to go fix, since fixing the source is the * only way to change a derived value. */ export function describeDerivedSource(variable: VariableDeclare): string { switch (variable.source) { case 'capability': return variable.derive_from ? `from another module's capability data (${variable.derive_from})` : "from another module's capability data"; case 'system': return variable.derive_from ? `from system config (${variable.derive_from})` : 'from system config'; case 'infrastructure': return 'from the infrastructure celilo selected for this module'; case 'terraform': return 'from a Terraform output, at deploy time'; case 'hook': return `by the module's own hooks at runtime (via context.config.set), and never by an operator`; default: return `computed by celilo (source: ${variable.source})`; } } /** * Why `module config set` refuses this variable, and what to do instead. * Actionable per source: a derived value is only wrong because its upstream is * wrong, and fixing the upstream fixes every consumer at once, where pinning one * module hides the divergence. */ export function explainNotSettable(moduleId: string, variable: VariableDeclare): string { const header = `'${variable.name}' is derived by celilo (source: ${variable.source}) — not operator-settable.`; const origin = `It is computed ${describeDerivedSource(variable)}, so a value set here would be overwritten the next time ${moduleId} is generated.`; switch (variable.source) { case 'capability': return `${header}\n${origin}\n • Fix it at the provider: change the config of the module that publishes this capability, then redeploy it.\n • 'celilo module config get ${moduleId}' shows the value celilo currently computes.`; case 'system': return `${header}\n${origin}\n • Fix it at the source: 'celilo system config set ${variable.derive_from?.replace(/^\$\{?system:/, '').replace(/\}$/, '') ?? ''} '.\n • That corrects every module deriving from it at once, rather than pinning this one.`; case 'infrastructure': return `${header}\n${origin}\n • node placement: set the service default for NEW deploys (celilo service reconfigure); move an existing container with 'celilo proxmox migrate'.\n • vmid / IP: auto-allocated by IPAM.`; case 'terraform': return `${header}\n${origin}\n • It is read back from Terraform outputs after the deploy creates the resource.`; case 'hook': return `${header}\n${origin}\n • It is written by the module's own hook via context.config.set, not by any operator path.`; default: return `${header}\n${origin}`; } }