@celilo/cli 0.11.0 → 0.12.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/AGENTS.md +7 -4
- package/CELILO_CORE_MODULES.md +7 -6
- package/CELILO_SUBSYSTEMS.md +52 -6
- package/MODULE_PRIMITIVES.md +164 -0
- package/README.md +5 -5
- package/drizzle/0015_port_forwards.sql +12 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +14 -6
- package/schemas/system_config.json +1 -1
- package/src/ansible/inventory.test.ts +1 -1
- package/src/ansible/inventory.ts +3 -3
- package/src/api/protocol.test.ts +1 -1
- package/src/api/remote-client.test.ts +1 -1
- package/src/api/serve.ts +5 -5
- package/src/capabilities/public-web-publish.test.ts +209 -0
- package/src/capabilities/validation.test.ts +1 -1
- package/src/capabilities/well-known.test.ts +1 -1
- package/src/capabilities/well-known.ts +2 -2
- package/src/cli/command-tree-parser.ts +3 -1
- package/src/cli/commands/api.ts +1 -1
- package/src/cli/commands/apt-upgrade.test.ts +33 -0
- package/src/cli/commands/apt-upgrade.ts +63 -0
- package/src/cli/commands/backup-pull.ts +1 -1
- package/src/cli/commands/commands-json.ts +29 -0
- package/src/cli/commands/completion.ts +1 -1
- package/src/cli/commands/events.ts +1 -1
- package/src/cli/commands/module-changeset.ts +1 -1
- package/src/cli/commands/module-import.ts +1 -1
- package/src/cli/commands/module-list.ts +16 -2
- package/src/cli/commands/module-publish.ts +1 -1
- package/src/cli/commands/module-remove.ts +26 -1
- package/src/cli/commands/module-types.ts +1 -1
- package/src/cli/commands/module-upgrade.ts +1 -1
- package/src/cli/commands/module-version.ts +1 -1
- package/src/cli/commands/module-where.test.ts +26 -0
- package/src/cli/commands/module-where.ts +130 -0
- package/src/cli/commands/proxmox-node-list.ts +1 -1
- package/src/cli/commands/publish/alpha.test.ts +1 -1
- package/src/cli/commands/publish/alpha.ts +3 -3
- package/src/cli/commands/publish/changesets.ts +1 -1
- package/src/cli/commands/publish/global-install.ts +6 -2
- package/src/cli/commands/publish/helpers.ts +21 -14
- package/src/cli/commands/publish/index.ts +4 -4
- package/src/cli/commands/publish/plan.ts +3 -3
- package/src/cli/commands/publish/preflight.ts +2 -2
- package/src/cli/commands/publish/types.ts +2 -2
- package/src/cli/commands/publish/workspace.test.ts +1 -1
- package/src/cli/commands/publish/workspace.ts +2 -2
- package/src/cli/commands/registry-owner.test.ts +166 -0
- package/src/cli/commands/registry-owner.ts +124 -0
- package/src/cli/commands/registry-token.test.ts +109 -0
- package/src/cli/commands/registry-token.ts +194 -0
- package/src/cli/commands/restore.ts +1 -1
- package/src/cli/commands/service-add-proxmox.ts +1 -1
- package/src/cli/commands/service-list.ts +15 -2
- package/src/cli/commands/subscribers-list.ts +1 -1
- package/src/cli/commands/system-apply-config-equivalence.test.ts +1 -1
- package/src/cli/commands/system-apply-config.ts +1 -1
- package/src/cli/commands/system-init-deprecation.test.ts +1 -1
- package/src/cli/commands/system-init.ts +2 -2
- package/src/cli/commands/token.test.ts +26 -0
- package/src/cli/commands/token.ts +160 -0
- package/src/cli/completion.ts +21 -0
- package/src/cli/generate-zsh-completion.test.ts +22 -4
- package/src/cli/generate-zsh-completion.ts +7 -3
- package/src/cli/index.ts +170 -7
- package/src/cli/parser.ts +1 -1
- package/src/cli/restore-command.test.ts +1 -1
- package/src/db/client.ts +1 -1
- package/src/db/schema.ts +50 -7
- package/src/hooks/capability-loader-firewall.test.ts +4 -3
- package/src/hooks/capability-loader.ts +100 -10
- package/src/hooks/define-hook.test.ts +24 -0
- package/src/hooks/executor.test.ts +1 -1
- package/src/hooks/executor.ts +2 -2
- package/src/hooks/types.ts +1 -1
- package/src/manifest/contracts/v1.ts +2 -2
- package/src/manifest/schema.ts +13 -13
- package/src/manifest/template-validator.ts +1 -1
- package/src/module/packaging/build.ts +19 -0
- package/src/module/packaging/workspace-deps.test.ts +94 -0
- package/src/module/packaging/workspace-deps.ts +185 -0
- package/src/module/versioning/changeset-version.ts +1 -1
- package/src/policy/no-hand-built-ssh.test.ts +90 -0
- package/src/registry/client.test.ts +86 -0
- package/src/registry/client.ts +67 -1
- package/src/services/api-access.ts +1 -1
- package/src/services/aspect-approvals.ts +1 -1
- package/src/services/aspect-runner.ts +3 -3
- package/src/services/aspect-template-resolver.test.ts +1 -1
- package/src/services/aspect-template-resolver.ts +1 -1
- package/src/services/build-bus/delivery-events.ts +1 -1
- package/src/services/build-bus/fan-out.ts +1 -1
- package/src/services/build-bus/hook-dispatch.ts +1 -1
- package/src/services/build-bus/receiver-server.ts +1 -1
- package/src/services/build-bus/status.test.ts +1 -1
- package/src/services/build-bus/status.ts +1 -1
- package/src/services/build-bus/subscriber-store.ts +1 -1
- package/src/services/bus-interview.ts +1 -1
- package/src/services/celilo-events.ts +1 -1
- package/src/services/celilo-mgmt-hooks.test.ts +1 -1
- package/src/services/cross-module-read.ts +1 -1
- package/src/services/deploy-posture.ts +1 -1
- package/src/services/deployed-systems.test.ts +1 -1
- package/src/services/deployed-systems.ts +4 -4
- package/src/services/dns-provider-backfill.ts +2 -2
- package/src/services/events-daemon.ts +1 -1
- package/src/services/machine-pool.ts +3 -3
- package/src/services/module-deploy.ts +93 -8
- package/src/services/module-subscriptions.ts +1 -1
- package/src/services/module-types-generator.ts +1 -1
- package/src/services/module-validator/git-hygiene.ts +1 -1
- package/src/services/port-forwards.test.ts +92 -0
- package/src/services/port-forwards.ts +86 -0
- package/src/services/proxmox-reconcile.ts +2 -2
- package/src/services/public-web-republish.test.ts +189 -0
- package/src/services/public-web-republish.ts +84 -0
- package/src/services/restore-from-file.ts +1 -1
- package/src/services/restore-preflight.ts +1 -1
- package/src/services/system-identity.ts +1 -1
- package/src/services/system-init.test.ts +2 -2
- package/src/services/system-init.ts +2 -2
- package/src/services/web-route-cleanup.test.ts +250 -0
- package/src/services/web-route-cleanup.ts +144 -0
- package/src/templates/generator.ts +2 -2
- package/src/types/infrastructure.ts +1 -1
- package/src/variables/computed/computed-integration.test.ts +1 -1
- package/src/variables/computed/evaluate.ts +1 -1
- package/src/variables/computed/parse.ts +1 -1
- package/src/variables/context.test.ts +1 -1
- package/src/variables/context.ts +5 -5
- package/src/variables/lxc-nameserver.test.ts +1 -1
- package/src/variables/resolver.ts +2 -2
- package/src/variables/types.ts +1 -1
- package/src/api/protocol.ts +0 -159
- package/src/api/remote-client.ts +0 -218
- package/src/cli/command-registry.ts +0 -1488
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static-config subscriber store for the build bus
|
|
3
|
-
* ([[
|
|
3
|
+
* ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 2-lite).
|
|
4
4
|
*
|
|
5
5
|
* The first cut of cross-machine event distribution skips the
|
|
6
6
|
* registry-server switchboard described in §6 of the spec — instead
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* nothing answers, the operator sees the unanswered query via
|
|
10
10
|
* `celilo events list-pending` and fixes the responder setup.
|
|
11
11
|
*
|
|
12
|
-
* See `infra/
|
|
12
|
+
* See `infra/openspec/changes/interactive-deploys-via-event-bus/proposal.md`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { type Bus, defineEvents, openBus } from '@celilo/event-bus';
|
|
@@ -64,7 +64,7 @@ export interface UninstallFailedPayload {
|
|
|
64
64
|
* module) has come up / been torn down. Carries the system's identity so a
|
|
65
65
|
* subscriber (e.g. a dns_internal provider) can register/deregister internal
|
|
66
66
|
* DNS records without re-querying the deploying module's config.
|
|
67
|
-
*
|
|
67
|
+
* openspec/specs/internal-dns-split-horizon/spec.md D5.
|
|
68
68
|
*/
|
|
69
69
|
export interface SystemCreatedPayload {
|
|
70
70
|
/** The module whose deploy produced this system (lets handlers resolve zones, etc.). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Behavior tests for the celilo-mgmt module's on_backup / on_restore
|
|
3
|
-
* hooks (Phase 3 of
|
|
3
|
+
* hooks (Phase 3 of openspec/specs/management-server-backup/spec.md).
|
|
4
4
|
*
|
|
5
5
|
* We import the hook scripts directly and call their handlers with a
|
|
6
6
|
* synthesized HookContext, then assert on the side-effects (files
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Materialization helpers for the `cross_module_read` privilege.
|
|
3
3
|
*
|
|
4
|
-
* Phase 2 of
|
|
4
|
+
* Phase 2 of openspec/specs/management-server-backup/spec.md. When an allow-listed
|
|
5
5
|
* module (today: only celilo-mgmt) declares `cross_module_read` in its
|
|
6
6
|
* `requires.capabilities`, its on_backup hook receives a
|
|
7
7
|
* `cross_module_root` input pointing at a directory mirroring OTHER
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy posture — fast vs. safe — for `celilo module upgrade` (ISS-0138,
|
|
3
|
-
*
|
|
3
|
+
* openspec/changes/build-bus-poll-cd/proposal.md).
|
|
4
4
|
*
|
|
5
5
|
* CD is CI-driven, so no operator is present to pass `--no-backup`. The posture
|
|
6
6
|
* (fast = skip backup + extended verify; safe = backup + full verify) is
|
|
@@ -17,7 +17,7 @@ const TEST_DB_PATH = './test-deployed-systems.db';
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Coverage for the one-time `module_systems` upgrade backfill
|
|
20
|
-
* (
|
|
20
|
+
* (openspec/specs/module-systems-addressing/spec.md): a deployment created before 0007 has its
|
|
21
21
|
* host data in module_configs / ip_allocations / module_infrastructure but an
|
|
22
22
|
* empty module_systems, and the refactored hooks resolve to no system. The
|
|
23
23
|
* backfill reconstructs it. Mirrors the real turnip prod state (caddy etc.:
|
|
@@ -15,7 +15,7 @@ import type { InfraSystemFields } from '../variables/types';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The deployment-STATE layer: a module's 0..N deployed systems
|
|
18
|
-
* (
|
|
18
|
+
* (openspec/specs/module-systems-addressing/spec.md). Replaces the scalar `target_ip`/`vmid`
|
|
19
19
|
* rows that used to live in module_configs and the single-result
|
|
20
20
|
* `getModuleHostAndIp`. There is deliberately no "get THE system" helper —
|
|
21
21
|
* callers work with the array so the 0/1/N reality stays visible.
|
|
@@ -216,7 +216,7 @@ function asZone(value: string | undefined): NetworkZone | null {
|
|
|
216
216
|
* `requires.system`, normalized to name `main`, or one `requires.systems`
|
|
217
217
|
* entry), so this records that single host from `hostname` config + the
|
|
218
218
|
* resolved IP. Returns the systems it recorded ([] for an API-only module with
|
|
219
|
-
* no host — e.g. namecheap). See
|
|
219
|
+
* no host — e.g. namecheap). See openspec/specs/module-systems-addressing/spec.md.
|
|
220
220
|
*/
|
|
221
221
|
export async function recordDeployedSystemForModule(
|
|
222
222
|
moduleId: string,
|
|
@@ -282,7 +282,7 @@ export async function recordDeployedSystemForModule(
|
|
|
282
282
|
|
|
283
283
|
/**
|
|
284
284
|
* One-time upgrade backfill: populate `module_systems` for deployments that
|
|
285
|
-
* predate `0007_module_systems` (
|
|
285
|
+
* predate `0007_module_systems` (openspec/specs/module-systems-addressing/spec.md). A DB created
|
|
286
286
|
* before the target_ip → systems refactor has its host data in
|
|
287
287
|
* `module_configs.target_ip` / `vmid` / `zone` + `ip_allocations` +
|
|
288
288
|
* `module_infrastructure`, but an empty `module_systems` — so `$infra:` and
|
|
@@ -374,7 +374,7 @@ function prefixOf(subnetCidr: string | undefined): number {
|
|
|
374
374
|
* Build the `$infra:<name>.<field>` lookup for a module — one entry per deployed
|
|
375
375
|
* system, keyed by its `name`. `cidr` is derived from `ipv4_address` + the
|
|
376
376
|
* zone's prefix (from `network.<zone>.subnet` in system config). Consumed by the
|
|
377
|
-
* variable resolver at generate time.
|
|
377
|
+
* variable resolver at generate time. openspec/specs/module-systems-addressing/spec.md.
|
|
378
378
|
*/
|
|
379
379
|
export function buildInfraSystemsMap(
|
|
380
380
|
moduleId: string,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* every currently-deployed system in its zones by invoking its OWN
|
|
8
8
|
* `on_system_event` hook once per host. The ongoing case (a system deploying
|
|
9
9
|
* AFTER the provider) is handled by the provider's `system.created.*`
|
|
10
|
-
* subscription. See
|
|
10
|
+
* subscription. See openspec/specs/event-driven-hook-subscriptions/spec.md.
|
|
11
11
|
*
|
|
12
12
|
* All DNS mechanics (which zones, register vs delete) live in the module's
|
|
13
13
|
* hook; this file only decides WHICH hosts and invokes the hook. That is the
|
|
@@ -74,7 +74,7 @@ export async function backfillProviderDns(
|
|
|
74
74
|
const failures: string[] = [];
|
|
75
75
|
|
|
76
76
|
// One register per deployed system across all modules — a module with N
|
|
77
|
-
// hosts backfills N records (
|
|
77
|
+
// hosts backfills N records (openspec/specs/module-systems-addressing/spec.md).
|
|
78
78
|
for (const mod of deployed) {
|
|
79
79
|
for (const sys of getModuleSystems(mod.id, db)) {
|
|
80
80
|
const result = await runNamedHook(moduleId, 'on_system_event' as HookName, db, logger, {
|
|
@@ -171,7 +171,7 @@ export function renderSystemdUnit(input: UnitInputs): string {
|
|
|
171
171
|
: `journalctl --user -u ${SYSTEMD_UNIT_NAME}`;
|
|
172
172
|
return `[Unit]
|
|
173
173
|
Description=Celilo SQLite Event Bus Dispatcher
|
|
174
|
-
Documentation=https://github.com/psbanka/infra/blob/main/
|
|
174
|
+
Documentation=https://github.com/psbanka/infra/blob/main/openspec/specs/event-bus/spec.md
|
|
175
175
|
After=network.target
|
|
176
176
|
|
|
177
177
|
[Service]
|
|
@@ -242,17 +242,17 @@ export async function listMachines(filters?: MachineFilters): Promise<Machine[]>
|
|
|
242
242
|
/**
|
|
243
243
|
* Return every machine in any of the given zones — the discovery
|
|
244
244
|
* query the aspect runner uses to find fan-out targets per
|
|
245
|
-
*
|
|
245
|
+
* openspec/specs/base-module-aspects/spec.md.
|
|
246
246
|
*
|
|
247
247
|
* Filters:
|
|
248
248
|
* - `excludeApiOnly` (default `true`): drops machines marked
|
|
249
249
|
* `api_only` (the greenwave / ISP-modem case). Aspects use
|
|
250
|
-
* Ansible, so api_only systems are unreachable.
|
|
250
|
+
* Ansible, so api_only systems are unreachable. openspec/specs/base-module-aspects/spec.md
|
|
251
251
|
* D8.
|
|
252
252
|
* - `excludeHostnames` (default `[]`): drops named hostnames.
|
|
253
253
|
* Aspect authors that need to skip the system running their own
|
|
254
254
|
* primary deploy pass it here; the framework doesn't auto-skip
|
|
255
|
-
* (
|
|
255
|
+
* (openspec/specs/base-module-aspects/spec.md D3).
|
|
256
256
|
*
|
|
257
257
|
* NOTE on container_service systems: this Phase 1 implementation
|
|
258
258
|
* covers MACHINE-based systems only (the machines table). LXCs/VMs
|
|
@@ -39,6 +39,7 @@ import { E2E_CONFLICT_MESSAGE, runningE2eContainers } from './e2e-guard';
|
|
|
39
39
|
import { resolveInfrastructureVariables } from './infrastructure-variable-resolver';
|
|
40
40
|
import { findMachineForModule } from './machine-pool';
|
|
41
41
|
import { checkProxmoxReachable, formatProxmoxUnreachableError } from './proxmox-preflight';
|
|
42
|
+
import { republishStaticWebConsumers } from './public-web-republish';
|
|
42
43
|
import { deleteTemporarySshKey, writeTemporarySshKey } from './ssh-key-manager';
|
|
43
44
|
import { buildTerraformEnvForService } from './terraform-env';
|
|
44
45
|
|
|
@@ -335,7 +336,7 @@ async function deployModuleImpl(
|
|
|
335
336
|
// events and prompts via clack. Other responder shapes (Claude
|
|
336
337
|
// subagent, `celilo events respond` from another shell, autoresponder
|
|
337
338
|
// daemon) compete on the bus; first reply wins. See
|
|
338
|
-
// infra/
|
|
339
|
+
// infra/openspec/changes/interactive-deploys-via-event-bus/proposal.md.
|
|
339
340
|
const terminalResponder = process.stdin.isTTY
|
|
340
341
|
? (await import('./terminal-responder')).startTerminalResponder()
|
|
341
342
|
: null;
|
|
@@ -772,7 +773,7 @@ async function deployModuleImpl(
|
|
|
772
773
|
|
|
773
774
|
// Fan out the module's base-module aspect (if any) per the
|
|
774
775
|
// on_install trigger. Failures here don't fail the primary
|
|
775
|
-
// deploy — D4 in
|
|
776
|
+
// deploy — D4 in openspec/specs/base-module-aspects/spec.md: aspects are idempotent
|
|
776
777
|
// and forward-progress; a partial fleet update is expected to
|
|
777
778
|
// converge on the next fan-out. We log the result instead.
|
|
778
779
|
const aspectOutcome = await maybeRunAspectForTrigger({
|
|
@@ -789,6 +790,27 @@ async function deployModuleImpl(
|
|
|
789
790
|
log.success(`Base-module aspect fan-out for '${moduleId}' completed`);
|
|
790
791
|
}
|
|
791
792
|
|
|
793
|
+
// A public_web provider deploy rebuilds its host and takes /srv/www with
|
|
794
|
+
// it, while consumers' route rows survive — so the fresh provider would
|
|
795
|
+
// render site blocks pointing at directories that no longer exist. Re-run
|
|
796
|
+
// each static consumer's on_install to refill them. Idempotent; failures
|
|
797
|
+
// are logged, never fatal to the provider's own deploy.
|
|
798
|
+
const republish = await republishStaticWebConsumers({
|
|
799
|
+
providerModuleId: moduleId,
|
|
800
|
+
manifest,
|
|
801
|
+
db,
|
|
802
|
+
});
|
|
803
|
+
if (republish.republished.length > 0) {
|
|
804
|
+
log.success(
|
|
805
|
+
`Re-published ${republish.republished.length} static site(s) to '${moduleId}': ${republish.republished.join(', ')}`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
for (const failure of republish.failures) {
|
|
809
|
+
log.warn(
|
|
810
|
+
`Failed to re-publish '${failure.moduleId}' to '${moduleId}': ${failure.error}. Run \`celilo module deploy ${failure.moduleId}\` to restore its content.`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
792
814
|
// Mirror the infrastructure-path success message at the end of
|
|
793
815
|
// a successful deploy. Without this, config-only deploys end
|
|
794
816
|
// abruptly with whatever the last hook line was — operator sees
|
|
@@ -884,7 +906,7 @@ async function deployModuleImpl(
|
|
|
884
906
|
// Record the module's deployed system(s) now that the IP is known for every
|
|
885
907
|
// provider type (machine / proxmox IPAM / DO outputs). This populates
|
|
886
908
|
// ctx.systems for on_install and is the source of truth for system.created
|
|
887
|
-
// and DNS (
|
|
909
|
+
// and DNS (openspec/specs/module-systems-addressing/spec.md). API-only modules record none.
|
|
888
910
|
{
|
|
889
911
|
const { recordDeployedSystemForModule } = await import('./deployed-systems');
|
|
890
912
|
const recorded = await recordDeployedSystemForModule(
|
|
@@ -1170,7 +1192,7 @@ async function deployModuleImpl(
|
|
|
1170
1192
|
|
|
1171
1193
|
// Inject the machine's IP as ip.primary for hooks that still read it
|
|
1172
1194
|
// (firewall NAT target, etc.). The host's address for hooks now comes
|
|
1173
|
-
// from ctx.systems (
|
|
1195
|
+
// from ctx.systems (openspec/specs/module-systems-addressing/spec.md), recorded into
|
|
1174
1196
|
// module_systems during generate — no target_ip is written here.
|
|
1175
1197
|
if (machineId) {
|
|
1176
1198
|
const { getMachine } = await import('./machine-pool');
|
|
@@ -1235,6 +1257,48 @@ async function deployModuleImpl(
|
|
|
1235
1257
|
error: installResult.error,
|
|
1236
1258
|
};
|
|
1237
1259
|
}
|
|
1260
|
+
|
|
1261
|
+
// Persist on_install string outputs as secrets on this module — same
|
|
1262
|
+
// as the validate_config / container_created paths above. caddy relies
|
|
1263
|
+
// on this to publish its firewall external IP (`public_ip`) so
|
|
1264
|
+
// public_web.register_route can point a new hostname's public A record
|
|
1265
|
+
// at it (M1 / #328).
|
|
1266
|
+
if (Object.keys(installResult.outputs).length > 0) {
|
|
1267
|
+
const { secrets: st } = await import('../db/schema');
|
|
1268
|
+
const { encryptSecret } = await import('../secrets/encryption');
|
|
1269
|
+
const mk = await getOrCreateMasterKey();
|
|
1270
|
+
for (const [key, value] of Object.entries(installResult.outputs)) {
|
|
1271
|
+
if (typeof value !== 'string') continue;
|
|
1272
|
+
const encrypted = encryptSecret(value, mk);
|
|
1273
|
+
const existing = db
|
|
1274
|
+
.select()
|
|
1275
|
+
.from(st)
|
|
1276
|
+
.where(and(eq(st.moduleId, moduleId), eq(st.name, key)))
|
|
1277
|
+
.get();
|
|
1278
|
+
if (existing) {
|
|
1279
|
+
db.update(st)
|
|
1280
|
+
.set({
|
|
1281
|
+
encryptedValue: encrypted.encryptedValue,
|
|
1282
|
+
iv: encrypted.iv,
|
|
1283
|
+
authTag: encrypted.authTag,
|
|
1284
|
+
updatedAt: new Date(),
|
|
1285
|
+
})
|
|
1286
|
+
.where(eq(st.id, existing.id))
|
|
1287
|
+
.run();
|
|
1288
|
+
} else {
|
|
1289
|
+
db.insert(st)
|
|
1290
|
+
.values({
|
|
1291
|
+
moduleId,
|
|
1292
|
+
name: key,
|
|
1293
|
+
encryptedValue: encrypted.encryptedValue,
|
|
1294
|
+
iv: encrypted.iv,
|
|
1295
|
+
authTag: encrypted.authTag,
|
|
1296
|
+
})
|
|
1297
|
+
.run();
|
|
1298
|
+
}
|
|
1299
|
+
log.success(`Stored hook output: ${key}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1238
1302
|
}
|
|
1239
1303
|
|
|
1240
1304
|
// Transition to INSTALLED
|
|
@@ -1274,7 +1338,7 @@ async function deployModuleImpl(
|
|
|
1274
1338
|
// host — the event path below only covers systems that deploy AFTER the
|
|
1275
1339
|
// provider (deliveries bind at emit time). Non-providers skip this
|
|
1276
1340
|
// entirely; their registration rides the system.created event below.
|
|
1277
|
-
//
|
|
1341
|
+
// openspec/specs/event-driven-hook-subscriptions/spec.md.
|
|
1278
1342
|
const { isDnsInternalProvider, backfillProviderDns, backfillWebRouteDns } = await import(
|
|
1279
1343
|
'./dns-provider-backfill'
|
|
1280
1344
|
);
|
|
@@ -1304,8 +1368,8 @@ async function deployModuleImpl(
|
|
|
1304
1368
|
|
|
1305
1369
|
// Announce each deployed system on the bus (D5/D6) — one
|
|
1306
1370
|
// system.created.<module> per host, so a dns_internal provider's
|
|
1307
|
-
// subscription registers every one (
|
|
1308
|
-
//
|
|
1371
|
+
// subscription registers every one (openspec/specs/module-systems-addressing/spec.md,
|
|
1372
|
+
// openspec/specs/internal-dns-split-horizon/spec.md, openspec/specs/event-driven-hook-subscriptions/spec.md).
|
|
1309
1373
|
try {
|
|
1310
1374
|
const { emitSystemCreated } = await import('./celilo-events');
|
|
1311
1375
|
for (const sys of getModuleSystems(moduleId, db)) {
|
|
@@ -1323,7 +1387,7 @@ async function deployModuleImpl(
|
|
|
1323
1387
|
|
|
1324
1388
|
// Fan out the module's base-module aspect (if any) per the
|
|
1325
1389
|
// on_install trigger. Failures here don't fail the primary
|
|
1326
|
-
// deploy — D4 in
|
|
1390
|
+
// deploy — D4 in openspec/specs/base-module-aspects/spec.md: aspects are idempotent
|
|
1327
1391
|
// and forward-progress; a partial fleet update is expected to
|
|
1328
1392
|
// converge on the next fan-out. We log the result instead.
|
|
1329
1393
|
const aspectOutcome = await maybeRunAspectForTrigger({
|
|
@@ -1340,6 +1404,27 @@ async function deployModuleImpl(
|
|
|
1340
1404
|
log.success(`Base-module aspect fan-out for '${moduleId}' completed`);
|
|
1341
1405
|
}
|
|
1342
1406
|
|
|
1407
|
+
// A public_web provider deploy rebuilds its host and takes /srv/www with
|
|
1408
|
+
// it, while consumers' route rows survive — so the fresh provider would
|
|
1409
|
+
// render site blocks pointing at directories that no longer exist. Re-run
|
|
1410
|
+
// each static consumer's on_install to refill them. Idempotent; failures
|
|
1411
|
+
// are logged, never fatal to the provider's own deploy.
|
|
1412
|
+
const republish = await republishStaticWebConsumers({
|
|
1413
|
+
providerModuleId: moduleId,
|
|
1414
|
+
manifest,
|
|
1415
|
+
db,
|
|
1416
|
+
});
|
|
1417
|
+
if (republish.republished.length > 0) {
|
|
1418
|
+
log.success(
|
|
1419
|
+
`Re-published ${republish.republished.length} static site(s) to '${moduleId}': ${republish.republished.join(', ')}`,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
for (const failure of republish.failures) {
|
|
1423
|
+
log.warn(
|
|
1424
|
+
`Failed to re-publish '${failure.moduleId}' to '${moduleId}': ${failure.error}. Run \`celilo module deploy ${failure.moduleId}\` to restore its content.`,
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1343
1428
|
log.success(`Module '${moduleId}' deployed successfully`);
|
|
1344
1429
|
return {
|
|
1345
1430
|
success: true,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `$self` in `pattern` → the module's id
|
|
8
8
|
* - `${MODULE_PATH}` in `handler` → the module's installed targetPath
|
|
9
9
|
* - a `hook:` subscription → a synthesized `celilo events run-hook
|
|
10
|
-
* <module> <sub-name>` handler (
|
|
10
|
+
* <module> <sub-name>` handler (openspec/specs/event-driven-hook-subscriptions/spec.md)
|
|
11
11
|
*
|
|
12
12
|
* The bus subscriber's name is namespaced as `<module-id>.<sub-name>`
|
|
13
13
|
* so two modules can declare a subscription named `smoke` without
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* output files; this module is responsible for the translation only, so
|
|
12
12
|
* unit tests can exercise every branch without touching the filesystem.
|
|
13
13
|
*
|
|
14
|
-
* See `
|
|
14
|
+
* See `openspec/changes/hook-api-v2/proposal.md` D2 for the design rationale.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ModuleManifest, VariableDeclare, VariableImport } from '../manifest/schema';
|
|
@@ -94,7 +94,7 @@ export function checkGitHygiene(modulePath: string, versionSourceKind?: string):
|
|
|
94
94
|
// `celilo module version` and ordered by the +N revision, so source moving
|
|
95
95
|
// past the manifest is the NORMAL case, not drift; for pin modules the version
|
|
96
96
|
// is checked against the upstream resolver, not git ancestry. Skip the gate for
|
|
97
|
-
// both (
|
|
97
|
+
// both (openspec/changes/module-version-semantics/proposal.md / ISS-0151).
|
|
98
98
|
if (versionSourceKind === 'changeset' || versionSourceKind === 'pin') {
|
|
99
99
|
checks.push({
|
|
100
100
|
category: 'git_hygiene',
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { PortForwardStore } from '@celilo/capabilities';
|
|
6
|
+
import type { DbClient } from '../db/client';
|
|
7
|
+
import { setupTestDatabase } from '../test-utils/setup-test-db';
|
|
8
|
+
import { buildPortForwardStore } from './port-forwards';
|
|
9
|
+
|
|
10
|
+
const FW = '192.168.0.254';
|
|
11
|
+
|
|
12
|
+
describe('port-forward store', () => {
|
|
13
|
+
let dir: string;
|
|
14
|
+
let db: DbClient;
|
|
15
|
+
let store: PortForwardStore;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
dir = mkdtempSync(join(tmpdir(), 'pf-'));
|
|
19
|
+
const dbPath = join(dir, 'celilo.db');
|
|
20
|
+
process.env.CELILO_DB_PATH = dbPath;
|
|
21
|
+
db = await setupTestDatabase(dbPath);
|
|
22
|
+
store = buildPortForwardStore(db);
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
db.$client.close();
|
|
26
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
27
|
+
try {
|
|
28
|
+
rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
} catch {
|
|
30
|
+
/* ignore */
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('adds and lists forwards for a firewall', () => {
|
|
35
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 443, protocol: 'TCP', description: 'caddy' });
|
|
36
|
+
store.add(FW, {
|
|
37
|
+
internalIp: '10.0.20.42',
|
|
38
|
+
port: 2222,
|
|
39
|
+
protocol: 'TCP',
|
|
40
|
+
description: 'forgejo',
|
|
41
|
+
});
|
|
42
|
+
const forwards = store.list(FW);
|
|
43
|
+
expect(forwards).toHaveLength(2);
|
|
44
|
+
expect(forwards.map((f) => f.port).sort((a, b) => a - b)).toEqual([443, 2222]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('add is an idempotent upsert — same tuple → one row, description updated', () => {
|
|
48
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 443, protocol: 'TCP', description: 'v1' });
|
|
49
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 443, protocol: 'TCP', description: 'v2' });
|
|
50
|
+
const forwards = store.list(FW);
|
|
51
|
+
expect(forwards).toHaveLength(1);
|
|
52
|
+
expect(forwards[0].description).toBe('v2');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('scopes forwards by firewallIp', () => {
|
|
56
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 443, protocol: 'TCP', description: 'a' });
|
|
57
|
+
store.add('10.0.30.254', {
|
|
58
|
+
internalIp: '10.0.30.5',
|
|
59
|
+
port: 53,
|
|
60
|
+
protocol: 'UDP',
|
|
61
|
+
description: 'b',
|
|
62
|
+
});
|
|
63
|
+
expect(store.list(FW)).toHaveLength(1);
|
|
64
|
+
expect(store.list('10.0.30.254')).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('remove deletes exactly the tuple and leaves others', () => {
|
|
68
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 443, protocol: 'TCP', description: 'x' });
|
|
69
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 80, protocol: 'TCP', description: 'y' });
|
|
70
|
+
store.remove(FW, '10.0.20.5', 443, 'TCP');
|
|
71
|
+
const forwards = store.list(FW);
|
|
72
|
+
expect(forwards).toHaveLength(1);
|
|
73
|
+
expect(forwards[0].port).toBe(80);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('ingressIp forwards are distinct from the public (NULL) ones', () => {
|
|
77
|
+
store.add(FW, { internalIp: '10.0.20.5', port: 53, protocol: 'UDP', description: 'public' });
|
|
78
|
+
store.add(FW, {
|
|
79
|
+
internalIp: '10.0.20.5',
|
|
80
|
+
port: 53,
|
|
81
|
+
protocol: 'UDP',
|
|
82
|
+
ingressIp: '10.0.10.53',
|
|
83
|
+
description: 'ingress',
|
|
84
|
+
});
|
|
85
|
+
expect(store.list(FW)).toHaveLength(2);
|
|
86
|
+
// removing the public (NULL-ingress) one leaves the ingress one intact
|
|
87
|
+
store.remove(FW, '10.0.20.5', 53, 'UDP');
|
|
88
|
+
const forwards = store.list(FW);
|
|
89
|
+
expect(forwards).toHaveLength(1);
|
|
90
|
+
expect(forwards[0].ingressIp).toBe('10.0.10.53');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port-forward registry — the DB-backed `PortForwardStore` (openspec/changes/unified-management-no-ssh/proposal.md).
|
|
3
|
+
*
|
|
4
|
+
* The shared-core desired-state store for the `firewall` capability. The
|
|
5
|
+
* capability-loader constructs one of these and injects it into the firewall
|
|
6
|
+
* provider factory, so `exposeService`/`unexposeService` become register/
|
|
7
|
+
* unregister and the provider's converge renders the complete ruleset from
|
|
8
|
+
* `list(firewallIp)` and applies it atomically (`iptables-restore`). Replaces
|
|
9
|
+
* "read the box back with `iptables -L`" as the source of truth.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PortForward, PortForwardStore, Protocol } from '@celilo/capabilities';
|
|
13
|
+
import { and, eq, isNull } from 'drizzle-orm';
|
|
14
|
+
import type { DbClient } from '../db/client';
|
|
15
|
+
import { portForwards } from '../db/schema';
|
|
16
|
+
|
|
17
|
+
/** NULL-aware match on the optional ingress IP (SQLite treats NULL as distinct). */
|
|
18
|
+
function ingressMatch(ingressIp: string | undefined) {
|
|
19
|
+
return ingressIp ? eq(portForwards.ingressIp, ingressIp) : isNull(portForwards.ingressIp);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildPortForwardStore(db: DbClient): PortForwardStore {
|
|
23
|
+
return {
|
|
24
|
+
list(firewallIp: string): PortForward[] {
|
|
25
|
+
return db
|
|
26
|
+
.select()
|
|
27
|
+
.from(portForwards)
|
|
28
|
+
.where(eq(portForwards.firewallIp, firewallIp))
|
|
29
|
+
.all()
|
|
30
|
+
.map((r) => ({
|
|
31
|
+
internalIp: r.internalIp,
|
|
32
|
+
port: r.port,
|
|
33
|
+
protocol: r.protocol,
|
|
34
|
+
ingressIp: r.ingressIp ?? undefined,
|
|
35
|
+
description: r.description,
|
|
36
|
+
}));
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
add(firewallIp: string, forward: PortForward): void {
|
|
40
|
+
// Idempotent upsert: delete any existing row for this exact tuple, then
|
|
41
|
+
// insert. Matches buildRouteOps' delete-then-insert (keeps the operation
|
|
42
|
+
// atomic w.r.t. the unique index, and NULL-aware on ingress_ip).
|
|
43
|
+
db.delete(portForwards)
|
|
44
|
+
.where(
|
|
45
|
+
and(
|
|
46
|
+
eq(portForwards.firewallIp, firewallIp),
|
|
47
|
+
eq(portForwards.internalIp, forward.internalIp),
|
|
48
|
+
eq(portForwards.port, forward.port),
|
|
49
|
+
eq(portForwards.protocol, forward.protocol),
|
|
50
|
+
ingressMatch(forward.ingressIp),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
.run();
|
|
54
|
+
db.insert(portForwards)
|
|
55
|
+
.values({
|
|
56
|
+
firewallIp,
|
|
57
|
+
internalIp: forward.internalIp,
|
|
58
|
+
port: forward.port,
|
|
59
|
+
protocol: forward.protocol,
|
|
60
|
+
ingressIp: forward.ingressIp ?? null,
|
|
61
|
+
description: forward.description,
|
|
62
|
+
})
|
|
63
|
+
.run();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
remove(
|
|
67
|
+
firewallIp: string,
|
|
68
|
+
internalIp: string,
|
|
69
|
+
port: number,
|
|
70
|
+
protocol: Protocol,
|
|
71
|
+
ingressIp?: string,
|
|
72
|
+
): void {
|
|
73
|
+
db.delete(portForwards)
|
|
74
|
+
.where(
|
|
75
|
+
and(
|
|
76
|
+
eq(portForwards.firewallIp, firewallIp),
|
|
77
|
+
eq(portForwards.internalIp, internalIp),
|
|
78
|
+
eq(portForwards.port, port),
|
|
79
|
+
eq(portForwards.protocol, protocol),
|
|
80
|
+
ingressMatch(ingressIp),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
.run();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Proxmox reconciliation (
|
|
2
|
+
* Proxmox reconciliation (openspec/specs/base-module-aspects/spec.md D5).
|
|
3
3
|
*
|
|
4
4
|
* When a base-module aspect declares `proxmox_reconcile.tfvars` and
|
|
5
5
|
* its fan-out plan touches Proxmox-provisioned LXCs, the running
|
|
@@ -151,6 +151,6 @@ export function executeProxmoxReconcile(plan: ProxmoxReconcilePlan): void {
|
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
log.warn(
|
|
154
|
-
'To persist these values, re-deploy the owning modules after the underlying celilo state catches up. Tracked as Phase 1c in
|
|
154
|
+
'To persist these values, re-deploy the owning modules after the underlying celilo state catches up. Tracked as Phase 1c in openspec/specs/base-module-aspects/spec.md.',
|
|
155
155
|
);
|
|
156
156
|
}
|