@celilo/cli 0.5.0-alpha.1 → 0.5.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/drizzle/0009_dns_registrations.sql +13 -0
- package/drizzle/0010_dns_internal_records.sql +12 -0
- package/drizzle/0011_backups_name.sql +1 -0
- package/drizzle/meta/_journal.json +22 -1
- package/package.json +3 -3
- package/src/ansible/inventory.test.ts +10 -10
- package/src/ansible/validation.test.ts +25 -15
- package/src/api-clients/proxmox.test.ts +211 -1
- package/src/api-clients/proxmox.ts +399 -8
- package/src/cli/command-registry.ts +83 -6
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/dns.ts +57 -0
- package/src/cli/commands/events.test.ts +4 -4
- package/src/cli/commands/events.ts +89 -24
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-publish.ts +24 -0
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.test.ts +37 -0
- package/src/cli/commands/module-upgrade.ts +26 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/proxmox-vm-template-build.ts +171 -0
- package/src/cli/commands/publish/alpha.test.ts +26 -0
- package/src/cli/commands/publish/alpha.ts +23 -0
- package/src/cli/commands/publish/types.ts +7 -2
- package/src/cli/commands/publish/workspace.ts +11 -1
- package/src/cli/commands/restore.ts +29 -0
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +283 -209
- package/src/cli/commands/service-reconfigure.test.ts +115 -0
- package/src/cli/commands/service-reconfigure.ts +252 -129
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-doctor.ts +135 -40
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/commands/system-migrate.test.ts +40 -0
- package/src/cli/commands/system-migrate.ts +65 -0
- package/src/cli/completion.ts +23 -0
- package/src/cli/index.ts +91 -7
- package/src/cli/service-credential.ts +54 -0
- package/src/config/paths.test.ts +61 -48
- package/src/db/client.ts +15 -146
- package/src/db/migrate.ts +14 -6
- package/src/db/schema-introspection.ts +88 -0
- package/src/db/schema.ts +74 -0
- package/src/hooks/capability-loader-firewall.test.ts +3 -3
- package/src/hooks/capability-loader.ts +43 -2
- package/src/hooks/run-named-hook.ts +28 -2
- package/src/hooks/types.ts +2 -1
- package/src/infrastructure/property-extractor.test.ts +15 -0
- package/src/infrastructure/property-extractor.ts +12 -0
- package/src/manifest/contracts/v1.ts +16 -0
- package/src/manifest/schema.ts +17 -0
- package/src/manifest/validate.test.ts +53 -0
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-interview.ts +232 -0
- package/src/services/bus-secret-flow.test.ts +2 -2
- package/src/services/celilo-mgmt-hooks.test.ts +3 -2
- package/src/services/deploy-preflight.ts +25 -0
- package/src/services/deploy-validation.test.ts +54 -4
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/dns-internal-records.test.ts +126 -0
- package/src/services/dns-internal-records.ts +119 -0
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-provider-backfill.ts +14 -2
- package/src/services/dns-registrations.test.ts +120 -0
- package/src/services/dns-registrations.ts +108 -0
- package/src/services/events-daemon.test.ts +59 -0
- package/src/services/events-daemon.ts +191 -57
- package/src/services/fleet-checks.test.ts +508 -0
- package/src/services/fleet-checks.ts +678 -0
- package/src/services/module-build.test.ts +43 -38
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/module-subscriptions.test.ts +88 -0
- package/src/services/module-subscriptions.ts +50 -1
- package/src/services/module-validator/bundled-deps.test.ts +55 -0
- package/src/services/module-validator/bundled-deps.ts +115 -0
- package/src/services/module-validator/capability-versions.test.ts +1 -1
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +92 -12
- package/src/templates/generator.ts +165 -80
- package/src/test-utils/fixtures.test.ts +1 -1
- package/src/test-utils/integration-guard.ts +33 -0
- package/src/types/infrastructure.ts +6 -0
- package/src/variables/computed/computed-integration.test.ts +3 -3
- package/src/variables/computed/computed.test.ts +5 -5
- package/src/variables/declarative-derivation.test.ts +6 -6
|
@@ -5,6 +5,7 @@ import { dirname, join, relative } from 'node:path';
|
|
|
5
5
|
import { and, eq } from 'drizzle-orm';
|
|
6
6
|
import { generateInventory } from '../ansible/inventory';
|
|
7
7
|
import { generateAnsibleSecrets } from '../ansible/secrets';
|
|
8
|
+
import { ProxmoxClient, type ProxmoxCredentials } from '../api-clients/proxmox';
|
|
8
9
|
import { log } from '../cli/prompts';
|
|
9
10
|
import { getModuleStoragePath } from '../config/paths';
|
|
10
11
|
import { type DbClient, getDb } from '../db/client';
|
|
@@ -19,8 +20,14 @@ import {
|
|
|
19
20
|
import { getSingularSystemSpec } from '../manifest/schema';
|
|
20
21
|
import type { AnsibleCollection, ModuleManifest } from '../manifest/schema';
|
|
21
22
|
import { validateZoneRequirements } from '../manifest/validate';
|
|
23
|
+
import { getServiceCredentials } from '../services/container-service';
|
|
24
|
+
import { getModuleSystems } from '../services/deployed-systems';
|
|
25
|
+
import {
|
|
26
|
+
describeCapabilityProblem,
|
|
27
|
+
findBrokenCapabilityDerivations,
|
|
28
|
+
} from '../services/fleet-checks';
|
|
22
29
|
import { selectInfrastructure } from '../services/infrastructure-selector';
|
|
23
|
-
import { upsertModuleConfig } from '../services/module-config';
|
|
30
|
+
import { deleteModuleConfig, upsertModuleConfig } from '../services/module-config';
|
|
24
31
|
import type { InfrastructureSelection } from '../types/infrastructure';
|
|
25
32
|
import { convertSecretsToJinja } from '../variables/ansible-resolver';
|
|
26
33
|
import { buildResolutionContext } from '../variables/context';
|
|
@@ -129,82 +136,80 @@ export function getOutputFilename(templateFilename: string): string {
|
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
/**
|
|
132
|
-
* Inject framework-owned DNS-at-birth into every
|
|
139
|
+
* Inject framework-owned DNS-at-birth into every Proxmox compute resource —
|
|
140
|
+
* `proxmox_lxc` and `proxmox_vm_qemu`.
|
|
133
141
|
*
|
|
134
|
-
*
|
|
142
|
+
* A node's nameserver is infrastructure celilo owns — like vmid, target_ip, and
|
|
135
143
|
* inventory — not something a module author hand-writes. Authors declare zero
|
|
136
|
-
* DNS terraform; this stamps it onto each
|
|
137
|
-
* For every block it emits:
|
|
144
|
+
* DNS terraform; this stamps it onto each Proxmox resource block at generate
|
|
145
|
+
* time. For every block it emits:
|
|
138
146
|
*
|
|
139
147
|
* - `nameserver = "$self:lxc_nameserver"` — only when a nameserver is
|
|
140
|
-
* computable (`hasNameserver`). The
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* post-deploy.
|
|
144
|
-
* - `lifecycle { ignore_changes = [
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* aspect = ongoing DNS). See v2/LXC_INTERNAL_DNS.md.
|
|
148
|
+
* computable (`hasNameserver`). The attribute is `nameserver` for both an
|
|
149
|
+
* LXC and a cloud-init VM. The first system, deployed before any
|
|
150
|
+
* `dns_internal` provider exists, has no value: it inherits the node default
|
|
151
|
+
* and the `dns-client-config` aspect repairs resolv.conf post-deploy.
|
|
152
|
+
* - `lifecycle { ignore_changes = [...] }` — always. The list is the
|
|
153
|
+
* resource's create-time / ForceNew attributes, so an unchanged redeploy is
|
|
154
|
+
* a no-op and a real change an in-place UPDATE — never the destructive
|
|
155
|
+
* REPLACE the terraform-safety guard (create-only, see terraform-safety.ts)
|
|
156
|
+
* rejects. terraform = birth DNS, the `dns-client-config` aspect = ongoing
|
|
157
|
+
* DNS. The per-type lists are in the callback. See v2/LXC_INTERNAL_DNS.md.
|
|
151
158
|
*
|
|
152
159
|
* Anchored on the resource's opening line — it never brace-matches the nested
|
|
153
|
-
*
|
|
160
|
+
* disk/network/features blocks, so it's robust to attribute order/formatting.
|
|
154
161
|
* Idempotent at file granularity: a `.tf` that already declares
|
|
155
|
-
* `ignore_changes = [nameserver
|
|
156
|
-
*
|
|
162
|
+
* `ignore_changes = [nameserver` (re-run, or an author who opted in) is returned
|
|
163
|
+
* untouched.
|
|
157
164
|
*
|
|
158
165
|
* Policy function (Rule 10.1) - pure string transformation, no I/O.
|
|
159
166
|
*
|
|
160
167
|
* @param content - Raw terraform template content (pre variable-resolution)
|
|
161
168
|
* @param hasNameserver - Whether `$self:lxc_nameserver` resolves to a value
|
|
162
|
-
* @returns Content with DNS injected into each
|
|
169
|
+
* @returns Content with DNS injected into each Proxmox compute resource
|
|
163
170
|
*/
|
|
164
|
-
export function
|
|
171
|
+
export function injectProxmoxDns(content: string, hasNameserver: boolean): string {
|
|
165
172
|
// Already injected (idempotent) or author opted into the lifecycle — done.
|
|
166
|
-
// Match the `[nameserver` prefix (no closing bracket) so this stays true
|
|
167
|
-
//
|
|
168
|
-
// `[nameserver, network[0].hwaddr, …]` — otherwise re-generate double-injects.
|
|
173
|
+
// Match the `[nameserver` prefix (no closing bracket) so this stays true for
|
|
174
|
+
// both the LXC and the VM variant — otherwise re-generate double-injects.
|
|
169
175
|
if (content.includes('ignore_changes = [nameserver')) {
|
|
170
176
|
return content;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
// A template that still carries a `nameserver = …` attribute — a stale copied
|
|
174
|
-
// module
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// exists and just add the lifecycle guard (the load-bearing part). Both cases
|
|
178
|
-
// converge on exactly one nameserver + ignore_changes.
|
|
180
|
+
// module, or an author who set it by hand — already supplies the value.
|
|
181
|
+
// Injecting a second `nameserver` is a terraform "Attribute redefined" error.
|
|
182
|
+
// So skip the value line when one exists and just add the lifecycle guard.
|
|
179
183
|
const alreadyHasNameserver = /^[ \t]*nameserver[ \t]*=/m.test(content);
|
|
180
184
|
|
|
181
|
-
const openLineRe = /^([ \t]*)resource\s+"proxmox_lxc"\s+"[^"]+"\s*\{[ \t]*$/gm;
|
|
182
|
-
return content.replace(openLineRe, (openLine, indent: string) => {
|
|
185
|
+
const openLineRe = /^([ \t]*)resource\s+"(proxmox_lxc|proxmox_vm_qemu)"\s+"[^"]+"\s*\{[ \t]*$/gm;
|
|
186
|
+
return content.replace(openLineRe, (openLine, indent: string, resourceType: string) => {
|
|
183
187
|
const inner = `${indent} `;
|
|
184
188
|
const injected = [openLine];
|
|
185
189
|
if (hasNameserver && !alreadyHasNameserver) {
|
|
186
190
|
injected.push(`${inner}nameserver = "$self:lxc_nameserver"`);
|
|
187
191
|
}
|
|
188
192
|
injected.push(`${inner}lifecycle {`);
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
193
|
+
// The create-time / ForceNew attributes Proxmox assigns, per resource type.
|
|
194
|
+
// Listing them makes an unchanged redeploy a no-op and a real change an
|
|
195
|
+
// in-place UPDATE, never a destructive REPLACE. A non-existent attribute
|
|
196
|
+
// here fails `terraform validate`, so each list is exactly the schema
|
|
197
|
+
// attributes the resource actually has.
|
|
198
|
+
// LXC (ISS-0055/0089): the create-time MAC + rootfs volume, plus
|
|
199
|
+
// `ssh_public_keys` (ForceNew — a fleet-key rotation must not REPLACE the
|
|
200
|
+
// container). `rootfs.size` is NOT ignored, so a disk grow still applies.
|
|
201
|
+
// We do NOT ignore `target_node` — a node change is real (migration,
|
|
202
|
+
// ISS-0090), never silently dropped.
|
|
203
|
+
// VM (telmate 3.x proxmox_vm_qemu): `clone` (ForceNew clone source),
|
|
204
|
+
// `sshkeys` + `nameserver` (ForceNew cloud-init), and the NIC `macaddr`.
|
|
205
|
+
// Initial set from the provider schema — extend if a first real VM deploy
|
|
206
|
+
// surfaces another spurious-REPLACE attribute, exactly as ISS-0055 did for
|
|
207
|
+
// the LXC list.
|
|
208
|
+
const ignore =
|
|
209
|
+
resourceType === 'proxmox_vm_qemu'
|
|
210
|
+
? 'nameserver, network[0].macaddr, sshkeys, clone'
|
|
211
|
+
: 'nameserver, network[0].hwaddr, rootfs[0].volume, ssh_public_keys';
|
|
212
|
+
injected.push(`${inner} ignore_changes = [${ignore}]`);
|
|
208
213
|
injected.push(`${inner}}`);
|
|
209
214
|
return injected.join('\n');
|
|
210
215
|
});
|
|
@@ -259,6 +264,49 @@ async function readDeployedTargetNode(moduleId: string): Promise<string | null>
|
|
|
259
264
|
}
|
|
260
265
|
}
|
|
261
266
|
|
|
267
|
+
/**
|
|
268
|
+
* The node a module's container ACTUALLY lives on, from Proxmox (ISS-0090).
|
|
269
|
+
* Proxmox is the ultimate source of truth for current location — it sees a
|
|
270
|
+
* hand-migration that celilo's terraform state wouldn't. Returns null when the
|
|
271
|
+
* module has no deployed vmid yet, the service is unreachable, or the vmid isn't
|
|
272
|
+
* in the cluster — the caller then falls back to terraform state / the default.
|
|
273
|
+
* Never throws: a Proxmox outage must not block a deploy.
|
|
274
|
+
*/
|
|
275
|
+
async function readProxmoxNodeForModule(
|
|
276
|
+
moduleId: string,
|
|
277
|
+
serviceId: string,
|
|
278
|
+
db: DbClient,
|
|
279
|
+
): Promise<string | null> {
|
|
280
|
+
const vmid = getModuleSystems(moduleId, db).find(
|
|
281
|
+
(s) => s.infrastructure.type === 'container_service' && s.infrastructure.vmid != null,
|
|
282
|
+
)?.infrastructure.vmid;
|
|
283
|
+
if (vmid == null) return null;
|
|
284
|
+
try {
|
|
285
|
+
const creds = (await getServiceCredentials(serviceId)) as ProxmoxCredentials;
|
|
286
|
+
const result = await new ProxmoxClient(creds).nodeForVmid(vmid);
|
|
287
|
+
return result.success ? result.data : null;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Decide which node to target for a deploy (ISS-0090). Pure (Rule 10):
|
|
295
|
+
* Proxmox reality > recorded terraform state > service default.
|
|
296
|
+
* `default_target_node` governs only a FIRST placement; a changed default must
|
|
297
|
+
* never relocate a running container. A hand-migration (seen by Proxmox but not
|
|
298
|
+
* tf-state) is adopted. Deliberate moves are an explicit migrate (ISS-0062).
|
|
299
|
+
*/
|
|
300
|
+
export function decideTargetNode(opts: {
|
|
301
|
+
proxmoxNode: string | null;
|
|
302
|
+
stateNode: string | null;
|
|
303
|
+
defaultNode: string;
|
|
304
|
+
}): { node: string; source: 'proxmox' | 'state' | 'default' } {
|
|
305
|
+
if (opts.proxmoxNode) return { node: opts.proxmoxNode, source: 'proxmox' };
|
|
306
|
+
if (opts.stateNode) return { node: opts.stateNode, source: 'state' };
|
|
307
|
+
return { node: opts.defaultNode, source: 'default' };
|
|
308
|
+
}
|
|
309
|
+
|
|
262
310
|
/**
|
|
263
311
|
* Discover template files in directory recursively
|
|
264
312
|
*
|
|
@@ -682,6 +730,9 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
682
730
|
// Infrastructure Properties Resolution (Proxmox provider config)
|
|
683
731
|
// For Proxmox services, extract provider config and store as temporary values
|
|
684
732
|
// This happens during generation so templates can access target_node, lxc_template, etc.
|
|
733
|
+
// Resolved live each generate (ISS-0090) and injected into the context below,
|
|
734
|
+
// never cached in the DB. undefined for non-Proxmox / machine deploys.
|
|
735
|
+
let resolvedTargetNode: string | undefined;
|
|
685
736
|
if (isContainerService && isProxmoxService && infrastructureSelection?.serviceId) {
|
|
686
737
|
const service = await db
|
|
687
738
|
.select()
|
|
@@ -694,39 +745,49 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
694
745
|
default_target_node: string;
|
|
695
746
|
lxc_template: string;
|
|
696
747
|
storage: string;
|
|
748
|
+
// Optional: only set once a cloud-init VM template has been built/selected
|
|
749
|
+
// for the service (see service reconfigure). Required by `type: vm` modules;
|
|
750
|
+
// absent for services that only deploy LXCs.
|
|
751
|
+
vm_template?: string;
|
|
697
752
|
};
|
|
698
753
|
|
|
699
|
-
// ISS-0090:
|
|
700
|
-
//
|
|
701
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
754
|
+
// ISS-0090: deploy follows REALITY. Resolve the node from Proxmox (it sees
|
|
755
|
+
// a hand-migration that tf-state wouldn't), else the recorded terraform
|
|
756
|
+
// state, else the service default (FIRST placement only). A changed default
|
|
757
|
+
// must never relocate a running container; deliberate moves are an explicit
|
|
758
|
+
// migrate (ISS-0062).
|
|
759
|
+
const decision = decideTargetNode({
|
|
760
|
+
proxmoxNode: await readProxmoxNodeForModule(
|
|
761
|
+
moduleId,
|
|
762
|
+
infrastructureSelection.serviceId,
|
|
763
|
+
db,
|
|
764
|
+
),
|
|
765
|
+
stateNode: await readDeployedTargetNode(moduleId),
|
|
766
|
+
defaultNode: providerConfig.default_target_node,
|
|
767
|
+
});
|
|
768
|
+
resolvedTargetNode = decision.node;
|
|
769
|
+
if (decision.source !== 'default' && decision.node !== providerConfig.default_target_node) {
|
|
770
|
+
const from = decision.source === 'proxmox' ? 'Proxmox' : 'terraform state';
|
|
771
|
+
log.info(
|
|
772
|
+
`${moduleId} → node '${decision.node}' (from ${from}; service default is '${providerConfig.default_target_node}'). Relocating requires a deliberate migration.`,
|
|
773
|
+
);
|
|
716
774
|
}
|
|
717
775
|
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
776
|
+
// Persist only the non-drift provider values. target_node is reality — it's
|
|
777
|
+
// injected into the resolution context below, never cached (ISS-0090); drop
|
|
778
|
+
// any stale __infra_target_node a prior generate left behind.
|
|
779
|
+
upsertModuleConfig(db, moduleId, '__infra_lxc_template', providerConfig.lxc_template);
|
|
780
|
+
upsertModuleConfig(db, moduleId, '__infra_storage', providerConfig.storage);
|
|
781
|
+
// vm_template is only present once a VM template exists for the service.
|
|
782
|
+
// Persist it when set so `type: vm` modules can resolve $self:vm_template;
|
|
783
|
+
// a `type: vm` deploy against a service without one then fails loudly at
|
|
784
|
+
// variable resolution (the intended "no VM template configured" error).
|
|
785
|
+
if (providerConfig.vm_template) {
|
|
786
|
+
upsertModuleConfig(db, moduleId, '__infra_vm_template', providerConfig.vm_template);
|
|
727
787
|
}
|
|
788
|
+
deleteModuleConfig(db, moduleId, '__infra_target_node');
|
|
728
789
|
|
|
729
|
-
log.success(`Infrastructure
|
|
790
|
+
log.success(`Infrastructure resolved: target_node=${decision.node} (${decision.source})`);
|
|
730
791
|
}
|
|
731
792
|
}
|
|
732
793
|
|
|
@@ -758,8 +819,15 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
758
819
|
context.selfConfig.target_ip = ipConfig.value!;
|
|
759
820
|
}
|
|
760
821
|
|
|
761
|
-
//
|
|
762
|
-
|
|
822
|
+
// target_node is the live-resolved reality (ISS-0090) — inject it directly,
|
|
823
|
+
// never from a cached __infra_target_node row (which drifts).
|
|
824
|
+
if (resolvedTargetNode) {
|
|
825
|
+
context.selfConfig.target_node = resolvedTargetNode;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// lxc_template / storage are provider config (intent, not drift-prone) — read
|
|
829
|
+
// them back from the __infra_* rows persisted above.
|
|
830
|
+
const infraKeys = ['lxc_template', 'storage', 'vm_template'];
|
|
763
831
|
for (const key of infraKeys) {
|
|
764
832
|
const infraConfig = db
|
|
765
833
|
.select()
|
|
@@ -780,6 +848,23 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
780
848
|
};
|
|
781
849
|
}
|
|
782
850
|
|
|
851
|
+
// Policy: fail loud at the source on a broken capability chain. A
|
|
852
|
+
// required `source: capability` var whose chain doesn't resolve is
|
|
853
|
+
// silently dropped during derivation, then surfaces downstream as a
|
|
854
|
+
// cryptic `$self:<x> not found` in some template. Assert it here against
|
|
855
|
+
// the resolved capabilities map so generate names the broken link and
|
|
856
|
+
// the provider to redeploy (ISS-0115; the data-plane sibling of ISS-0088).
|
|
857
|
+
const capProblems = findBrokenCapabilityDerivations(moduleId, manifest, context.capabilities);
|
|
858
|
+
if (capProblems.length > 0) {
|
|
859
|
+
return {
|
|
860
|
+
success: false,
|
|
861
|
+
error: `Cannot generate '${moduleId}': ${capProblems.length} capability-derived variable(s) won't resolve:\n${capProblems
|
|
862
|
+
.map((p) => ` - ${describeCapabilityProblem(p)}`)
|
|
863
|
+
.join('\n')}`,
|
|
864
|
+
details: capProblems,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
783
868
|
// Execution: Store derived variables in module_configs
|
|
784
869
|
// Variables with derive_from are resolved in the context but not stored in module_configs.
|
|
785
870
|
// Store them so hooks and host_vars can access them.
|
|
@@ -987,7 +1072,7 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
987
1072
|
// every proxmox_lxc resource before resolution (terraform files only).
|
|
988
1073
|
const content =
|
|
989
1074
|
!isAnsibleTemplate && template.targetPath.endsWith('.tf')
|
|
990
|
-
?
|
|
1075
|
+
? injectProxmoxDns(template.content, Boolean(context.selfConfig.lxc_nameserver))
|
|
991
1076
|
: template.content;
|
|
992
1077
|
const result = isAnsibleTemplate
|
|
993
1078
|
? await convertSecretsToJinja(content, context, db)
|
|
@@ -60,7 +60,7 @@ describe('Fixture Test Utilities', () => {
|
|
|
60
60
|
test('excludes secrets from config', async () => {
|
|
61
61
|
const config = await getModuleTestConfig('dns-external');
|
|
62
62
|
expect(config).toBeDefined();
|
|
63
|
-
expect(config.vps_ip).toBe('
|
|
63
|
+
expect(config.vps_ip).toBe('192.0.2.20');
|
|
64
64
|
expect(config.secrets).toBeUndefined();
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guard for tests that can't run in the `unit` target / on the minimal CI
|
|
5
|
+
* runner — they shell out to external tools (ansible, wg, terraform, docker…)
|
|
6
|
+
* or assert platform-specific behavior. Such tests are integration tests by
|
|
7
|
+
* Rule 7.1 and must NOT gate the unit CI.
|
|
8
|
+
*
|
|
9
|
+
* Returns `true` (→ skip) when:
|
|
10
|
+
* - CELILO_UNIT_ONLY=1 is set (the `unit` target forces them off), OR
|
|
11
|
+
* - a required tool is absent on this host, OR
|
|
12
|
+
* - we're on the wrong platform.
|
|
13
|
+
*
|
|
14
|
+
* Usage: describe.skipIf(skipIntegration({ tools: ['ansible'] }))(...)
|
|
15
|
+
* test.skipIf(skipIntegration({ platform: 'darwin' }))(...)
|
|
16
|
+
*/
|
|
17
|
+
function hasTool(tool: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`command -v ${tool}`, { stdio: 'ignore' });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function skipIntegration(
|
|
27
|
+
req: { tools?: string[]; platform?: NodeJS.Platform } = {},
|
|
28
|
+
): boolean {
|
|
29
|
+
if (process.env.CELILO_UNIT_ONLY === '1') return true;
|
|
30
|
+
if (req.tools?.some((t) => !hasTool(t))) return true;
|
|
31
|
+
if (req.platform && process.platform !== req.platform) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
@@ -31,6 +31,12 @@ export interface ProxmoxConfig {
|
|
|
31
31
|
default_target_node: string;
|
|
32
32
|
lxc_template: string;
|
|
33
33
|
storage: string;
|
|
34
|
+
/**
|
|
35
|
+
* Cloud-init VM template to clone for `requires.system.type: vm` modules — the
|
|
36
|
+
* VM analogue of `lxc_template`. Optional: only services hosting VM modules set
|
|
37
|
+
* it (the operator builds the template once, per VM_INFRA_TYPE.md).
|
|
38
|
+
*/
|
|
39
|
+
vm_template?: string;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -90,7 +90,7 @@ beforeEach(async () => {
|
|
|
90
90
|
// Encrypt with the SAME master key the resolver will decrypt with (the one
|
|
91
91
|
// beforeEach wrote into CELILO_DATA_DIR).
|
|
92
92
|
const enc = encryptSecret(
|
|
93
|
-
JSON.stringify({ '
|
|
93
|
+
JSON.stringify({ 'example.net': 'pw1', 'celilo.computer': 'pw2' }),
|
|
94
94
|
await loadTestMasterKey(),
|
|
95
95
|
);
|
|
96
96
|
db.$client
|
|
@@ -133,7 +133,7 @@ describe('computed capability fields — DB integration', () => {
|
|
|
133
133
|
expect(result.success).toBe(true);
|
|
134
134
|
if (result.success) {
|
|
135
135
|
// Non-scalar computed results serialize to JSON in the string path.
|
|
136
|
-
expect(JSON.parse(result.value)).toEqual(['
|
|
136
|
+
expect(JSON.parse(result.value)).toEqual(['example.net', 'celilo.computer']);
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
139
|
|
|
@@ -170,7 +170,7 @@ describe('computed capability fields — DB integration', () => {
|
|
|
170
170
|
const reg = ctx.capabilities.dns_registrar as Record<string, unknown>;
|
|
171
171
|
|
|
172
172
|
// The real array, not the raw marker object.
|
|
173
|
-
expect(reg.domain_list).toEqual(['
|
|
173
|
+
expect(reg.domain_list).toEqual(['example.net', 'celilo.computer']);
|
|
174
174
|
expect(reg.provider).toBe('namecheap');
|
|
175
175
|
});
|
|
176
176
|
|
|
@@ -23,7 +23,7 @@ function makeLookup(data: Record<string, unknown>): LookupFn {
|
|
|
23
23
|
|
|
24
24
|
const FIXTURE = {
|
|
25
25
|
secret: {
|
|
26
|
-
ddns_passwords: { '
|
|
26
|
+
ddns_passwords: { 'example.net': 'pw1', 'celilo.computer': 'pw2' },
|
|
27
27
|
},
|
|
28
28
|
self: {
|
|
29
29
|
zone_names: { dmz: 'DMZ', app: 'App' },
|
|
@@ -34,7 +34,7 @@ const FIXTURE = {
|
|
|
34
34
|
zones_with_dupes: ['x', 'y', 'x', 'z', 'y'],
|
|
35
35
|
},
|
|
36
36
|
system: {
|
|
37
|
-
primary_domain: '
|
|
37
|
+
primary_domain: 'example.net',
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
|
|
@@ -44,7 +44,7 @@ function evalOk(expr: string): unknown {
|
|
|
44
44
|
|
|
45
45
|
describe('computed DSL — keys', () => {
|
|
46
46
|
test('keys of the ddns_passwords secret map (the domain_list case)', () => {
|
|
47
|
-
expect(evalOk('keys(secret.ddns_passwords)')).toEqual(['
|
|
47
|
+
expect(evalOk('keys(secret.ddns_passwords)')).toEqual(['example.net', 'celilo.computer']);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('keys of a non-secret object', () => {
|
|
@@ -63,7 +63,7 @@ describe('computed DSL — values', () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
test('keys of a secret map is allowed (key names are non-sensitive)', () => {
|
|
66
|
-
expect(evalOk('keys(secret.ddns_passwords)')).toEqual(['
|
|
66
|
+
expect(evalOk('keys(secret.ddns_passwords)')).toEqual(['example.net', 'celilo.computer']);
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -107,7 +107,7 @@ describe('computed DSL — concat + unique (nesting/chaining)', () => {
|
|
|
107
107
|
describe('computed DSL — format', () => {
|
|
108
108
|
test('interpolates named parts', () => {
|
|
109
109
|
expect(evalOk("format('{host}.{zone}', host=self.hostname, zone=system.primary_domain)")).toBe(
|
|
110
|
-
'dns-int.
|
|
110
|
+
'dns-int.example.net',
|
|
111
111
|
);
|
|
112
112
|
});
|
|
113
113
|
|
|
@@ -164,14 +164,14 @@ describe('resolveDeclarativeDerivation', () => {
|
|
|
164
164
|
capabilities: {
|
|
165
165
|
dns_external: {
|
|
166
166
|
server: {
|
|
167
|
-
ip: '
|
|
167
|
+
ip: '192.0.2.20',
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
170
|
},
|
|
171
171
|
};
|
|
172
172
|
|
|
173
173
|
const result = resolveDeclarativeDerivation(variable, context);
|
|
174
|
-
expect(result).toBe('
|
|
174
|
+
expect(result).toBe('192.0.2.20');
|
|
175
175
|
});
|
|
176
176
|
|
|
177
177
|
test('resolves nested capability path', () => {
|
|
@@ -193,7 +193,7 @@ describe('resolveDeclarativeDerivation', () => {
|
|
|
193
193
|
dns_external: {
|
|
194
194
|
server: {
|
|
195
195
|
ip: {
|
|
196
|
-
primary: '
|
|
196
|
+
primary: '192.0.2.20',
|
|
197
197
|
secondary: '188.166.157.3',
|
|
198
198
|
},
|
|
199
199
|
},
|
|
@@ -202,7 +202,7 @@ describe('resolveDeclarativeDerivation', () => {
|
|
|
202
202
|
};
|
|
203
203
|
|
|
204
204
|
const result = resolveDeclarativeDerivation(variable, context);
|
|
205
|
-
expect(result).toBe('
|
|
205
|
+
expect(result).toBe('192.0.2.20');
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
test('throws on missing capability', () => {
|
|
@@ -350,13 +350,13 @@ describe('resolveDeclarativeDerivation', () => {
|
|
|
350
350
|
secrets: {},
|
|
351
351
|
capabilities: {
|
|
352
352
|
dns_external: {
|
|
353
|
-
server: { ip: '
|
|
353
|
+
server: { ip: '192.0.2.20' },
|
|
354
354
|
},
|
|
355
355
|
},
|
|
356
356
|
};
|
|
357
357
|
|
|
358
358
|
const result = resolveDeclarativeDerivation(variable, context);
|
|
359
|
-
expect(result).toBe('caddy.example.com@
|
|
359
|
+
expect(result).toBe('caddy.example.com@192.0.2.20');
|
|
360
360
|
});
|
|
361
361
|
});
|
|
362
362
|
|