@celilo/cli 0.5.0-alpha.8 → 0.5.0
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/package.json +1 -1
- package/src/api-clients/proxmox.test.ts +108 -20
- package/src/api-clients/proxmox.ts +130 -24
- package/src/cli/command-registry.ts +32 -3
- 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/events.ts +8 -3
- 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-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.ts +10 -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 +25 -18
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +275 -260
- package/src/cli/commands/service-reconfigure.ts +171 -153
- 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-init.ts +119 -128
- package/src/cli/completion.ts +15 -0
- package/src/cli/index.ts +25 -0
- package/src/cli/service-credential.ts +54 -0
- package/src/services/bus-interview.ts +232 -0
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +6 -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/proxmox-state-recovery.ts +26 -7
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +52 -1
- package/src/templates/generator.ts +106 -37
|
@@ -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,12 +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';
|
|
22
25
|
import {
|
|
23
26
|
describeCapabilityProblem,
|
|
24
27
|
findBrokenCapabilityDerivations,
|
|
25
28
|
} from '../services/fleet-checks';
|
|
26
29
|
import { selectInfrastructure } from '../services/infrastructure-selector';
|
|
27
|
-
import { upsertModuleConfig } from '../services/module-config';
|
|
30
|
+
import { deleteModuleConfig, upsertModuleConfig } from '../services/module-config';
|
|
28
31
|
import type { InfrastructureSelection } from '../types/infrastructure';
|
|
29
32
|
import { convertSecretsToJinja } from '../variables/ansible-resolver';
|
|
30
33
|
import { buildResolutionContext } from '../variables/context';
|
|
@@ -213,10 +216,11 @@ export function injectProxmoxDns(content: string, hasNameserver: boolean): strin
|
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/**
|
|
216
|
-
* Extract the node a
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* resource id (`<node>/lxc/<vmid>`
|
|
219
|
+
* Extract the node a Proxmox guest is deployed on from a parsed terraform state
|
|
220
|
+
* object — an LXC (proxmox_lxc) or a VM (proxmox_vm_qemu). Pure logic (Rule 10),
|
|
221
|
+
* split from the file read for testability. Prefers the explicit `target_node`
|
|
222
|
+
* attribute; falls back to parsing the resource id (`<node>/lxc/<vmid>` for an
|
|
223
|
+
* LXC, `<node>/qemu/<vmid>` for a VM). Returns null when there's no such
|
|
220
224
|
* resource (e.g. an empty/fresh state).
|
|
221
225
|
*/
|
|
222
226
|
export function targetNodeFromTfState(state: {
|
|
@@ -225,15 +229,17 @@ export function targetNodeFromTfState(state: {
|
|
|
225
229
|
instances?: Array<{ attributes?: { target_node?: string; id?: string } }>;
|
|
226
230
|
}>;
|
|
227
231
|
}): string | null {
|
|
228
|
-
const
|
|
229
|
-
|
|
232
|
+
const guest = state.resources?.find(
|
|
233
|
+
(r) => r.type === 'proxmox_lxc' || r.type === 'proxmox_vm_qemu',
|
|
234
|
+
);
|
|
235
|
+
const attrs = guest?.instances?.[0]?.attributes;
|
|
230
236
|
if (!attrs) {
|
|
231
237
|
return null;
|
|
232
238
|
}
|
|
233
239
|
if (attrs.target_node) {
|
|
234
240
|
return attrs.target_node;
|
|
235
241
|
}
|
|
236
|
-
// id format: "<node>/lxc/<vmid>"
|
|
242
|
+
// id format: "<node>/lxc/<vmid>" (LXC) or "<node>/qemu/<vmid>" (VM)
|
|
237
243
|
return attrs.id?.split('/')[0] || null;
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -261,6 +267,49 @@ async function readDeployedTargetNode(moduleId: string): Promise<string | null>
|
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The node a module's container ACTUALLY lives on, from Proxmox (ISS-0090).
|
|
272
|
+
* Proxmox is the ultimate source of truth for current location — it sees a
|
|
273
|
+
* hand-migration that celilo's terraform state wouldn't. Returns null when the
|
|
274
|
+
* module has no deployed vmid yet, the service is unreachable, or the vmid isn't
|
|
275
|
+
* in the cluster — the caller then falls back to terraform state / the default.
|
|
276
|
+
* Never throws: a Proxmox outage must not block a deploy.
|
|
277
|
+
*/
|
|
278
|
+
async function readProxmoxNodeForModule(
|
|
279
|
+
moduleId: string,
|
|
280
|
+
serviceId: string,
|
|
281
|
+
db: DbClient,
|
|
282
|
+
): Promise<string | null> {
|
|
283
|
+
const vmid = getModuleSystems(moduleId, db).find(
|
|
284
|
+
(s) => s.infrastructure.type === 'container_service' && s.infrastructure.vmid != null,
|
|
285
|
+
)?.infrastructure.vmid;
|
|
286
|
+
if (vmid == null) return null;
|
|
287
|
+
try {
|
|
288
|
+
const creds = (await getServiceCredentials(serviceId)) as ProxmoxCredentials;
|
|
289
|
+
const result = await new ProxmoxClient(creds).nodeForVmid(vmid);
|
|
290
|
+
return result.success ? result.data : null;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Decide which node to target for a deploy (ISS-0090). Pure (Rule 10):
|
|
298
|
+
* Proxmox reality > recorded terraform state > service default.
|
|
299
|
+
* `default_target_node` governs only a FIRST placement; a changed default must
|
|
300
|
+
* never relocate a running container. A hand-migration (seen by Proxmox but not
|
|
301
|
+
* tf-state) is adopted. Deliberate moves are an explicit migrate (ISS-0062).
|
|
302
|
+
*/
|
|
303
|
+
export function decideTargetNode(opts: {
|
|
304
|
+
proxmoxNode: string | null;
|
|
305
|
+
stateNode: string | null;
|
|
306
|
+
defaultNode: string;
|
|
307
|
+
}): { node: string; source: 'proxmox' | 'state' | 'default' } {
|
|
308
|
+
if (opts.proxmoxNode) return { node: opts.proxmoxNode, source: 'proxmox' };
|
|
309
|
+
if (opts.stateNode) return { node: opts.stateNode, source: 'state' };
|
|
310
|
+
return { node: opts.defaultNode, source: 'default' };
|
|
311
|
+
}
|
|
312
|
+
|
|
264
313
|
/**
|
|
265
314
|
* Discover template files in directory recursively
|
|
266
315
|
*
|
|
@@ -684,6 +733,9 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
684
733
|
// Infrastructure Properties Resolution (Proxmox provider config)
|
|
685
734
|
// For Proxmox services, extract provider config and store as temporary values
|
|
686
735
|
// This happens during generation so templates can access target_node, lxc_template, etc.
|
|
736
|
+
// Resolved live each generate (ISS-0090) and injected into the context below,
|
|
737
|
+
// never cached in the DB. undefined for non-Proxmox / machine deploys.
|
|
738
|
+
let resolvedTargetNode: string | undefined;
|
|
687
739
|
if (isContainerService && isProxmoxService && infrastructureSelection?.serviceId) {
|
|
688
740
|
const service = await db
|
|
689
741
|
.select()
|
|
@@ -696,39 +748,49 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
696
748
|
default_target_node: string;
|
|
697
749
|
lxc_template: string;
|
|
698
750
|
storage: string;
|
|
751
|
+
// Optional: only set once a cloud-init VM template has been built/selected
|
|
752
|
+
// for the service (see service reconfigure). Required by `type: vm` modules;
|
|
753
|
+
// absent for services that only deploy LXCs.
|
|
754
|
+
vm_template?: string;
|
|
699
755
|
};
|
|
700
756
|
|
|
701
|
-
// ISS-0090:
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
757
|
+
// ISS-0090: deploy follows REALITY. Resolve the node from Proxmox (it sees
|
|
758
|
+
// a hand-migration that tf-state wouldn't), else the recorded terraform
|
|
759
|
+
// state, else the service default (FIRST placement only). A changed default
|
|
760
|
+
// must never relocate a running container; deliberate moves are an explicit
|
|
761
|
+
// migrate (ISS-0062).
|
|
762
|
+
const decision = decideTargetNode({
|
|
763
|
+
proxmoxNode: await readProxmoxNodeForModule(
|
|
764
|
+
moduleId,
|
|
765
|
+
infrastructureSelection.serviceId,
|
|
766
|
+
db,
|
|
767
|
+
),
|
|
768
|
+
stateNode: await readDeployedTargetNode(moduleId),
|
|
769
|
+
defaultNode: providerConfig.default_target_node,
|
|
770
|
+
});
|
|
771
|
+
resolvedTargetNode = decision.node;
|
|
772
|
+
if (decision.source !== 'default' && decision.node !== providerConfig.default_target_node) {
|
|
773
|
+
const from = decision.source === 'proxmox' ? 'Proxmox' : 'terraform state';
|
|
774
|
+
log.info(
|
|
775
|
+
`${moduleId} → node '${decision.node}' (from ${from}; service default is '${providerConfig.default_target_node}'). Relocating requires a deliberate migration.`,
|
|
776
|
+
);
|
|
718
777
|
}
|
|
719
778
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
779
|
+
// Persist only the non-drift provider values. target_node is reality — it's
|
|
780
|
+
// injected into the resolution context below, never cached (ISS-0090); drop
|
|
781
|
+
// any stale __infra_target_node a prior generate left behind.
|
|
782
|
+
upsertModuleConfig(db, moduleId, '__infra_lxc_template', providerConfig.lxc_template);
|
|
783
|
+
upsertModuleConfig(db, moduleId, '__infra_storage', providerConfig.storage);
|
|
784
|
+
// vm_template is only present once a VM template exists for the service.
|
|
785
|
+
// Persist it when set so `type: vm` modules can resolve $self:vm_template;
|
|
786
|
+
// a `type: vm` deploy against a service without one then fails loudly at
|
|
787
|
+
// variable resolution (the intended "no VM template configured" error).
|
|
788
|
+
if (providerConfig.vm_template) {
|
|
789
|
+
upsertModuleConfig(db, moduleId, '__infra_vm_template', providerConfig.vm_template);
|
|
729
790
|
}
|
|
791
|
+
deleteModuleConfig(db, moduleId, '__infra_target_node');
|
|
730
792
|
|
|
731
|
-
log.success(`Infrastructure
|
|
793
|
+
log.success(`Infrastructure resolved: target_node=${decision.node} (${decision.source})`);
|
|
732
794
|
}
|
|
733
795
|
}
|
|
734
796
|
|
|
@@ -760,8 +822,15 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
760
822
|
context.selfConfig.target_ip = ipConfig.value!;
|
|
761
823
|
}
|
|
762
824
|
|
|
763
|
-
//
|
|
764
|
-
|
|
825
|
+
// target_node is the live-resolved reality (ISS-0090) — inject it directly,
|
|
826
|
+
// never from a cached __infra_target_node row (which drifts).
|
|
827
|
+
if (resolvedTargetNode) {
|
|
828
|
+
context.selfConfig.target_node = resolvedTargetNode;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// lxc_template / storage are provider config (intent, not drift-prone) — read
|
|
832
|
+
// them back from the __infra_* rows persisted above.
|
|
833
|
+
const infraKeys = ['lxc_template', 'storage', 'vm_template'];
|
|
765
834
|
for (const key of infraKeys) {
|
|
766
835
|
const infraConfig = db
|
|
767
836
|
.select()
|