@celilo/cli 0.5.0-alpha.0 → 0.5.0-alpha.2
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/meta/_journal.json +8 -1
- package/package.json +3 -3
- package/src/api-clients/proxmox.test.ts +30 -0
- package/src/api-clients/proxmox.ts +57 -0
- package/src/cli/command-registry.ts +33 -1
- package/src/cli/commands/dns.ts +57 -0
- package/src/cli/commands/events.ts +51 -19
- package/src/cli/commands/module-upgrade.test.ts +37 -0
- package/src/cli/commands/module-upgrade.ts +16 -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/completion.ts +6 -0
- package/src/cli/index.ts +55 -5
- package/src/db/schema.ts +36 -0
- package/src/hooks/capability-loader.ts +30 -2
- package/src/hooks/run-named-hook.ts +28 -2
- package/src/hooks/types.ts +2 -1
- package/src/manifest/contracts/v1.ts +16 -0
- package/src/manifest/schema.ts +10 -0
- 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/module-validator/capability-versions.test.ts +1 -1
- package/src/templates/generator.test.ts +30 -3
- package/src/templates/generator.ts +80 -5
|
@@ -6,6 +6,7 @@ import { and, eq } from 'drizzle-orm';
|
|
|
6
6
|
import { generateInventory } from '../ansible/inventory';
|
|
7
7
|
import { generateAnsibleSecrets } from '../ansible/secrets';
|
|
8
8
|
import { log } from '../cli/prompts';
|
|
9
|
+
import { getModuleStoragePath } from '../config/paths';
|
|
9
10
|
import { type DbClient, getDb } from '../db/client';
|
|
10
11
|
import {
|
|
11
12
|
capabilities,
|
|
@@ -195,12 +196,69 @@ export function injectProxmoxLxcDns(content: string, hasNameserver: boolean): st
|
|
|
195
196
|
// schema attributes in telmate ~>2.9 and `terraform validate` rejects them.
|
|
196
197
|
// `nameserver` stays for the original DNS reason; `rootfs.size` is NOT ignored
|
|
197
198
|
// so a disk grow still applies in place.
|
|
198
|
-
|
|
199
|
+
// ISS-0089: also ignore `ssh_public_keys` — it's ForceNew, and on a module
|
|
200
|
+
// deployed before the current fleet key, the LXC's birth keys differ from the
|
|
201
|
+
// regenerated config, forcing a spurious REPLACE on redeploy. Rotating the
|
|
202
|
+
// authorized keys must never recreate the container. We do NOT ignore
|
|
203
|
+
// `target_node` — a node change is real and is handled by migration (ISS-0090),
|
|
204
|
+
// never silently dropped.
|
|
205
|
+
injected.push(
|
|
206
|
+
`${inner} ignore_changes = [nameserver, network[0].hwaddr, rootfs[0].volume, ssh_public_keys]`,
|
|
207
|
+
);
|
|
199
208
|
injected.push(`${inner}}`);
|
|
200
209
|
return injected.join('\n');
|
|
201
210
|
});
|
|
202
211
|
}
|
|
203
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Extract the node a proxmox_lxc resource is deployed on from a parsed terraform
|
|
215
|
+
* state object. Pure logic (Rule 10), split from the file read for testability.
|
|
216
|
+
* Prefers the explicit `target_node` attribute; falls back to parsing the
|
|
217
|
+
* resource id (`<node>/lxc/<vmid>`). Returns null when there's no proxmox_lxc
|
|
218
|
+
* resource (e.g. an empty/fresh state).
|
|
219
|
+
*/
|
|
220
|
+
export function targetNodeFromTfState(state: {
|
|
221
|
+
resources?: Array<{
|
|
222
|
+
type?: string;
|
|
223
|
+
instances?: Array<{ attributes?: { target_node?: string; id?: string } }>;
|
|
224
|
+
}>;
|
|
225
|
+
}): string | null {
|
|
226
|
+
const lxc = state.resources?.find((r) => r.type === 'proxmox_lxc');
|
|
227
|
+
const attrs = lxc?.instances?.[0]?.attributes;
|
|
228
|
+
if (!attrs) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (attrs.target_node) {
|
|
232
|
+
return attrs.target_node;
|
|
233
|
+
}
|
|
234
|
+
// id format: "<node>/lxc/<vmid>"
|
|
235
|
+
return attrs.id?.split('/')[0] || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read the node a module's container is currently deployed on, from its terraform
|
|
240
|
+
* state file. This is celilo's authoritative record of placement (ISS-0090).
|
|
241
|
+
* Returns null when no state exists yet (a first deploy) or it can't be read —
|
|
242
|
+
* the caller then falls back to the service `default_target_node`.
|
|
243
|
+
*/
|
|
244
|
+
async function readDeployedTargetNode(moduleId: string): Promise<string | null> {
|
|
245
|
+
const statePath = join(
|
|
246
|
+
getModuleStoragePath(),
|
|
247
|
+
moduleId,
|
|
248
|
+
'generated',
|
|
249
|
+
'terraform',
|
|
250
|
+
'terraform.tfstate',
|
|
251
|
+
);
|
|
252
|
+
if (!existsSync(statePath)) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return targetNodeFromTfState(JSON.parse(await readFile(statePath, 'utf-8')));
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
204
262
|
/**
|
|
205
263
|
* Discover template files in directory recursively
|
|
206
264
|
*
|
|
@@ -638,9 +696,28 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
638
696
|
storage: string;
|
|
639
697
|
};
|
|
640
698
|
|
|
699
|
+
// ISS-0090: target the node the container ACTUALLY lives on, not the
|
|
700
|
+
// service default. `default_target_node` governs only NEW placement; a
|
|
701
|
+
// changed default must NEVER relocate a running system. celilo's terraform
|
|
702
|
+
// state is its authoritative record of where it placed this container (kept
|
|
703
|
+
// in sync by deploy and by `module migrate`), so read the deployed node from
|
|
704
|
+
// there. Fall back to the default only when there's no state yet — a first
|
|
705
|
+
// deploy. Deliberate relocation is an explicit `module migrate`, not a
|
|
706
|
+
// side-effect of the default changing.
|
|
707
|
+
let targetNode = providerConfig.default_target_node;
|
|
708
|
+
const deployedNode = await readDeployedTargetNode(moduleId);
|
|
709
|
+
if (deployedNode) {
|
|
710
|
+
if (deployedNode !== targetNode) {
|
|
711
|
+
log.info(
|
|
712
|
+
`${moduleId} is already deployed on node '${deployedNode}' — targeting it (service default is '${providerConfig.default_target_node}'). Relocating requires a deliberate migration.`,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
targetNode = deployedNode;
|
|
716
|
+
}
|
|
717
|
+
|
|
641
718
|
// Store provider config values as temporary config (similar to IPAM allocation)
|
|
642
719
|
const infraProperties = [
|
|
643
|
-
{ key: 'target_node', value:
|
|
720
|
+
{ key: 'target_node', value: targetNode },
|
|
644
721
|
{ key: 'lxc_template', value: providerConfig.lxc_template },
|
|
645
722
|
{ key: 'storage', value: providerConfig.storage },
|
|
646
723
|
];
|
|
@@ -649,9 +726,7 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
649
726
|
upsertModuleConfig(db, moduleId, `__infra_${prop.key}`, prop.value);
|
|
650
727
|
}
|
|
651
728
|
|
|
652
|
-
log.success(
|
|
653
|
-
`Infrastructure properties resolved from service: target_node=${providerConfig.default_target_node}`,
|
|
654
|
-
);
|
|
729
|
+
log.success(`Infrastructure properties resolved from service: target_node=${targetNode}`);
|
|
655
730
|
}
|
|
656
731
|
}
|
|
657
732
|
|