@celilo/cli 0.11.0-alpha.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 +47 -5
- 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 +8 -8
- package/schemas/system_config.json +1 -1
- package/src/ansible/inventory.test.ts +1 -1
- package/src/ansible/inventory.ts +3 -3
- 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/commands/api.ts +1 -1
- package/src/cli/commands/backup-pull.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-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/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 +19 -0
- package/src/cli/index.ts +155 -4
- 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/registry/client.ts
CHANGED
|
@@ -40,6 +40,14 @@ export interface ModuleMetadata {
|
|
|
40
40
|
versions: Array<{ num: string; yanked: boolean; created_at: string }>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** A module-owner table entry (ce-1ch). Mirrors the registry-server shape. */
|
|
44
|
+
export interface ModuleOwner {
|
|
45
|
+
moduleName: string;
|
|
46
|
+
ownerSub: string;
|
|
47
|
+
claimedAt: string;
|
|
48
|
+
sourceGroup: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
export class RegistryClient {
|
|
44
52
|
readonly baseUrl: string;
|
|
45
53
|
|
|
@@ -158,10 +166,68 @@ export class RegistryClient {
|
|
|
158
166
|
return resp.json() as Promise<{ ok: boolean; name: string; vers: string }>;
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
// ── Module ownership (admin — ce-1ch) ──────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** List the whole module-owner table. Requires an admin token. */
|
|
172
|
+
async listOwners(token: string): Promise<ModuleOwner[]> {
|
|
173
|
+
const resp = await this.authed(`${this.baseUrl}/api/v1/modules/owners`, token);
|
|
174
|
+
const body = (await resp.json()) as { owners: ModuleOwner[] };
|
|
175
|
+
return body.owners;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Show the owner of one module name, or null if unclaimed. Requires an admin token. */
|
|
179
|
+
async getOwner(name: string, token: string): Promise<ModuleOwner | null> {
|
|
180
|
+
const url = `${this.baseUrl}/api/v1/modules/owners/${encodeURIComponent(name)}`;
|
|
181
|
+
const resp = await fetch(url, {
|
|
182
|
+
headers: { Authorization: token },
|
|
183
|
+
signal: AbortSignal.timeout(30_000),
|
|
184
|
+
});
|
|
185
|
+
if (resp.status === 404) return null;
|
|
186
|
+
if (!resp.ok) throw new Error(await this.errorDetail(resp));
|
|
187
|
+
const body = (await resp.json()) as { owner: ModuleOwner };
|
|
188
|
+
return body.owner;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Reassign a module name to `ownerSub` (admin overwrite). Requires an admin token. */
|
|
192
|
+
async setOwner(name: string, ownerSub: string, token: string): Promise<ModuleOwner> {
|
|
193
|
+
const url = `${this.baseUrl}/api/v1/modules/owners/${encodeURIComponent(name)}`;
|
|
194
|
+
const resp = await fetch(url, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { Authorization: token, 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({ ownerSub }),
|
|
198
|
+
signal: AbortSignal.timeout(30_000),
|
|
199
|
+
});
|
|
200
|
+
if (!resp.ok) throw new Error(await this.errorDetail(resp));
|
|
201
|
+
const body = (await resp.json()) as { owner: ModuleOwner };
|
|
202
|
+
return body.owner;
|
|
203
|
+
}
|
|
204
|
+
|
|
161
205
|
// ── Internal ──────────────────────────────────────────────────────────────
|
|
162
206
|
|
|
207
|
+
/** GET with an Authorization header, throwing the server's error detail on failure. */
|
|
208
|
+
private async authed(url: string, token: string): Promise<Response> {
|
|
209
|
+
const resp = await fetch(url, {
|
|
210
|
+
headers: { Authorization: token },
|
|
211
|
+
signal: AbortSignal.timeout(30_000),
|
|
212
|
+
});
|
|
213
|
+
if (!resp.ok) throw new Error(await this.errorDetail(resp));
|
|
214
|
+
return resp;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Extract `errors[0].detail` from a Cargo-protocol error body, or fall back to the status. */
|
|
218
|
+
private async errorDetail(resp: Response): Promise<string> {
|
|
219
|
+
const body = (await resp.json().catch(() => ({}))) as {
|
|
220
|
+
errors?: Array<{ detail: string }>;
|
|
221
|
+
};
|
|
222
|
+
return body.errors?.[0]?.detail ?? `HTTP ${resp.status}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
163
225
|
private async get(url: string): Promise<Response> {
|
|
164
|
-
|
|
226
|
+
// 30s (was 15s): a module .netapp can be tens of MB and the download must
|
|
227
|
+
// finish within one window. 15s was too aggressive on slow links — the
|
|
228
|
+
// download of a ~19MB module over a multi-hop path (e2e sim NAT, real WAN)
|
|
229
|
+
// intermittently timed out mid-transfer.
|
|
230
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
165
231
|
if (!resp.ok) {
|
|
166
232
|
throw new Error(`Registry error at ${url}: HTTP ${resp.status}`);
|
|
167
233
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote API access control (see
|
|
2
|
+
* Remote API access control (see openspec/changes/replace-ssh-cli-api/proposal.md, Slice 2a).
|
|
3
3
|
*
|
|
4
4
|
* Stores API principals (name + SSH public key + grants), answers authz
|
|
5
5
|
* queries (deny-by-default), and renders the API account's `authorized_keys`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Aspect approvals — operator consent for a module's base-module aspect.
|
|
3
3
|
*
|
|
4
|
-
* Per
|
|
4
|
+
* Per openspec/specs/base-module-aspects/spec.md D2: when an operator imports a module that
|
|
5
5
|
* declares a `base_module_aspect`, they consent to that aspect's
|
|
6
6
|
* scope (applicable_zones + triggers) ONCE at import time. The
|
|
7
7
|
* consent is recorded in the `aspect_approvals` table and consulted
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Aspect runner — executes a module's base-module aspect across
|
|
3
3
|
* the fleet.
|
|
4
4
|
*
|
|
5
|
-
* Per
|
|
5
|
+
* Per openspec/specs/base-module-aspects/spec.md Phase 1: when a module with an approved
|
|
6
6
|
* `base_module_aspect` triggers a fan-out event, the runner walks
|
|
7
7
|
* every non-`api_only` system in the aspect's `applicable_zones`,
|
|
8
8
|
* materializes an Ansible inventory + playbook in a scratch
|
|
@@ -97,7 +97,7 @@ export interface AspectRunOptions {
|
|
|
97
97
|
/**
|
|
98
98
|
* Hostnames to exclude from fan-out, on top of `api_only`. The
|
|
99
99
|
* primary deploy's own host(s) typically don't need the aspect
|
|
100
|
-
* (per
|
|
100
|
+
* (per openspec/specs/base-module-aspects/spec.md D3 — aspect authors handle this when
|
|
101
101
|
* they want it, the framework doesn't force a skip).
|
|
102
102
|
*/
|
|
103
103
|
excludeHostnames?: string[];
|
|
@@ -301,7 +301,7 @@ export async function materializeAspectAnsible(args: {
|
|
|
301
301
|
/**
|
|
302
302
|
* Execution-phase function: orchestrates plan + materialize + run.
|
|
303
303
|
*
|
|
304
|
-
* Failure semantics (
|
|
304
|
+
* Failure semantics (openspec/specs/base-module-aspects/spec.md D4): aspects are idempotent
|
|
305
305
|
* and forward-progress only. A failed fan-out is reported and the
|
|
306
306
|
* partial state (some systems updated, others not) is preserved —
|
|
307
307
|
* no rollback. The caller (deploy planner, in SC4) decides how to
|
|
@@ -14,7 +14,7 @@ const TEST_DB_PATH = './test-aspect-resolver.db';
|
|
|
14
14
|
* resolver (separate from variables/resolver.ts); it previously handled
|
|
15
15
|
* only $self:/$system:/$capability:, so knot's migrated
|
|
16
16
|
* `knot_server_ip: $infra:main.ipv4_address` leaked into resolv.conf
|
|
17
|
-
* unresolved and broke DNS on every fleet machine.
|
|
17
|
+
* unresolved and broke DNS on every fleet machine. openspec/specs/module-systems-addressing/spec.md.
|
|
18
18
|
*/
|
|
19
19
|
function seedModuleWithSystem(db: DbClient, moduleId: string, ip: string): void {
|
|
20
20
|
db.insert(modules)
|
|
@@ -59,7 +59,7 @@ export async function resolveAspectTemplate(
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
// $infra:<system-name>.<field> — the provider's deployed system, by name
|
|
62
|
-
// (
|
|
62
|
+
// (openspec/specs/module-systems-addressing/spec.md). This is how an aspect references the
|
|
63
63
|
// provider's own host IP (e.g. knot's dns-client-config aspect sets every
|
|
64
64
|
// fleet machine's nameserver to $infra:main.ipv4_address). Without this the
|
|
65
65
|
// literal template string would leak into resolv.conf.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local-bus event emitters for build-bus delivery outcomes.
|
|
3
3
|
*
|
|
4
|
-
* Phase 6 of [[
|
|
4
|
+
* Phase 6 of [[openspec/changes/build-bus-poll-cd/proposal.md]] wants `celilo subscribers status`
|
|
5
5
|
* to show last-delivery state + recent failures per subscriber. The
|
|
6
6
|
* data source for that view is the local SQLite event bus — every
|
|
7
7
|
* publisher-side fan-out call emits one `webhook.delivered` or
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Webhook fan-out for the build bus ([[
|
|
2
|
+
* Webhook fan-out for the build bus ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 2).
|
|
3
3
|
*
|
|
4
4
|
* Given a PublishEvent and a subscriber list, this module:
|
|
5
5
|
* 1. Filters subscribers whose match rule fires for the event.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook-dispatch planning + execution for the build bus
|
|
3
|
-
* ([[
|
|
3
|
+
* ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 4).
|
|
4
4
|
*
|
|
5
5
|
* Pure: planHookDispatch(event, modules) → HookDispatchPlan[].
|
|
6
6
|
* Decides which module hooks match the event without spawning
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP receiver daemon for the build bus
|
|
3
|
-
* ([[
|
|
3
|
+
* ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 3).
|
|
4
4
|
*
|
|
5
5
|
* Verifies signed webhook envelopes against a per-receiver shared
|
|
6
6
|
* secret, then re-emits the contained PublishEvent onto the local
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Status-aggregator tests — Phase 6 of [[
|
|
2
|
+
* Status-aggregator tests — Phase 6 of [[openspec/changes/build-bus-poll-cd/proposal.md]].
|
|
3
3
|
*
|
|
4
4
|
* The aggregator is pure: takes subscribers + delivery records,
|
|
5
5
|
* returns per-subscriber summaries. The formatter is pure too.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Delivery-history aggregator for `celilo subscribers status`
|
|
3
|
-
* ([[
|
|
3
|
+
* ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 6).
|
|
4
4
|
*
|
|
5
5
|
* Reads recent `webhook.delivered` + `webhook.failed` events from
|
|
6
6
|
* the local event bus, groups by subscriber URL, and produces a
|
|
@@ -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',
|