@celilo/cli 0.1.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/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPAM Auto-Allocator
|
|
3
|
+
* Automatic VMID and IP allocation for container-based modules
|
|
4
|
+
*
|
|
5
|
+
* - Modules declare zone, Celilo auto-allocates vmid/IP
|
|
6
|
+
* - Sequential allocation with reservation support
|
|
7
|
+
* - Persistent storage in ip_allocations table
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { eq } from 'drizzle-orm';
|
|
11
|
+
import type { DbClient } from '../db/client';
|
|
12
|
+
import { ipAllocations } from '../db/schema';
|
|
13
|
+
|
|
14
|
+
export interface IpamAllocation {
|
|
15
|
+
moduleId: string;
|
|
16
|
+
vmid: number;
|
|
17
|
+
containerIp: string;
|
|
18
|
+
zone: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get existing allocation for module
|
|
23
|
+
*
|
|
24
|
+
* @param moduleId - Module identifier
|
|
25
|
+
* @param db - Drizzle database instance
|
|
26
|
+
* @returns Existing allocation or null
|
|
27
|
+
*/
|
|
28
|
+
export function getAllocation(moduleId: string, db: DbClient): IpamAllocation | null {
|
|
29
|
+
const allocation = db
|
|
30
|
+
.select()
|
|
31
|
+
.from(ipAllocations)
|
|
32
|
+
.where(eq(ipAllocations.moduleId, moduleId))
|
|
33
|
+
.get();
|
|
34
|
+
|
|
35
|
+
if (!allocation) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
moduleId: allocation.moduleId,
|
|
41
|
+
vmid: allocation.vmid,
|
|
42
|
+
containerIp: allocation.containerIp,
|
|
43
|
+
zone: allocation.zone,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Allocate VMID from available pool
|
|
49
|
+
*
|
|
50
|
+
* Algorithm:
|
|
51
|
+
* - Start from 200 (100-199 reserved for infrastructure)
|
|
52
|
+
* - Skip already allocated VMIDs
|
|
53
|
+
* - Skip reserved VMID ranges
|
|
54
|
+
* - Return first available VMID
|
|
55
|
+
*
|
|
56
|
+
* @param db - SQLite database instance (for raw queries)
|
|
57
|
+
* @returns Next available VMID
|
|
58
|
+
* @throws Error if no VMIDs available
|
|
59
|
+
*/
|
|
60
|
+
function allocateVMID(db: DbClient['$client']): number {
|
|
61
|
+
// Get all allocated VMIDs
|
|
62
|
+
const allocatedRows = db.prepare('SELECT vmid FROM ip_allocations').all() as Array<{
|
|
63
|
+
vmid: number;
|
|
64
|
+
}>;
|
|
65
|
+
|
|
66
|
+
// Get all reserved VMIDs (including ranges)
|
|
67
|
+
const reservedRows = db.prepare('SELECT vmid FROM vmid_reservations').all() as Array<{
|
|
68
|
+
vmid: number;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
const used = new Set<number>();
|
|
72
|
+
|
|
73
|
+
// Add allocated VMIDs
|
|
74
|
+
for (const row of allocatedRows) {
|
|
75
|
+
used.add(row.vmid);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add reserved VMIDs
|
|
79
|
+
for (const row of reservedRows) {
|
|
80
|
+
used.add(row.vmid);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find first available VMID starting from 200
|
|
84
|
+
const START_VMID = 200;
|
|
85
|
+
const MAX_VMID = 999999999;
|
|
86
|
+
|
|
87
|
+
for (let candidate = START_VMID; candidate <= MAX_VMID; candidate++) {
|
|
88
|
+
if (!used.has(candidate)) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error('No available VMIDs in pool (200-999999999)');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse CIDR subnet to get network address and prefix
|
|
98
|
+
*
|
|
99
|
+
* @param subnet - CIDR subnet (e.g., "10.0.10.0/24")
|
|
100
|
+
* @returns Parsed subnet components
|
|
101
|
+
*/
|
|
102
|
+
function parseSubnet(subnet: string): {
|
|
103
|
+
octets: number[];
|
|
104
|
+
prefix: number;
|
|
105
|
+
} {
|
|
106
|
+
const [network, prefixStr] = subnet.split('/');
|
|
107
|
+
const octets = network.split('.').map(Number);
|
|
108
|
+
const prefix = Number.parseInt(prefixStr, 10);
|
|
109
|
+
|
|
110
|
+
return { octets, prefix };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Allocate IP address from zone subnet
|
|
115
|
+
*
|
|
116
|
+
* Algorithm:
|
|
117
|
+
* - Get zone subnet from system config
|
|
118
|
+
* - Parse CIDR to get network range
|
|
119
|
+
* - Skip .0 (network), .1 (gateway), .255 (broadcast)
|
|
120
|
+
* - Start allocation from .10 (reserve .2-.9 for infrastructure)
|
|
121
|
+
* - Skip already allocated IPs
|
|
122
|
+
* - Skip reserved IP ranges
|
|
123
|
+
* - Return first available IP
|
|
124
|
+
*
|
|
125
|
+
* @param zone - Network zone (dmz, app, secure)
|
|
126
|
+
* @param db - SQLite database instance
|
|
127
|
+
* @returns Next available IP address
|
|
128
|
+
* @throws Error if no IPs available or subnet not configured
|
|
129
|
+
*/
|
|
130
|
+
function allocateIP(zone: string, db: DbClient['$client']): string {
|
|
131
|
+
// Get zone subnet from system config
|
|
132
|
+
const subnetRow = db
|
|
133
|
+
.prepare('SELECT value FROM system_config WHERE key = ?')
|
|
134
|
+
.get(`network.${zone}.subnet`) as { value: string } | undefined;
|
|
135
|
+
|
|
136
|
+
if (!subnetRow) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Zone subnet not configured: network.${zone}.subnet\n` +
|
|
139
|
+
`Run: celilo system config set network.${zone}.subnet <cidr>`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const subnet = subnetRow.value;
|
|
144
|
+
const { octets, prefix } = parseSubnet(subnet);
|
|
145
|
+
const [a, b, c] = octets;
|
|
146
|
+
|
|
147
|
+
// IP allocation range: .10 to .254
|
|
148
|
+
// .0 = network address
|
|
149
|
+
// .1 = gateway (reserved)
|
|
150
|
+
// .2-.9 = infrastructure (reserved for manual use)
|
|
151
|
+
// .10-.254 = auto-allocation pool
|
|
152
|
+
// .255 = broadcast address
|
|
153
|
+
const START_OCTET = 10;
|
|
154
|
+
const END_OCTET = 254;
|
|
155
|
+
|
|
156
|
+
// Get all allocated IPs in this zone
|
|
157
|
+
const allocatedRows = db
|
|
158
|
+
.prepare('SELECT container_ip FROM ip_allocations WHERE zone = ?')
|
|
159
|
+
.all(zone) as Array<{ container_ip: string }>;
|
|
160
|
+
|
|
161
|
+
// Get all reserved IPs in this zone
|
|
162
|
+
const reservedRows = db
|
|
163
|
+
.prepare('SELECT ip_start, ip_end FROM ip_reservations WHERE zone = ?')
|
|
164
|
+
.all(zone) as Array<{ ip_start: string; ip_end: string | null }>;
|
|
165
|
+
|
|
166
|
+
const used = new Set<string>();
|
|
167
|
+
|
|
168
|
+
// Add allocated IPs (strip CIDR suffix for comparison)
|
|
169
|
+
for (const row of allocatedRows) {
|
|
170
|
+
const ipOnly = row.container_ip.split('/')[0];
|
|
171
|
+
used.add(ipOnly);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Add reserved IPs (including ranges)
|
|
175
|
+
for (const row of reservedRows) {
|
|
176
|
+
if (row.ip_end) {
|
|
177
|
+
// Handle IP range: "10.0.10.1-10.0.10.9"
|
|
178
|
+
const startOctet = Number.parseInt(row.ip_start.split('.')[3], 10);
|
|
179
|
+
const endOctet = Number.parseInt(row.ip_end.split('.')[3], 10);
|
|
180
|
+
|
|
181
|
+
for (let i = startOctet; i <= endOctet; i++) {
|
|
182
|
+
used.add(`${a}.${b}.${c}.${i}`);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Single IP reservation
|
|
186
|
+
used.add(row.ip_start);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Find first available IP
|
|
191
|
+
for (let octet = START_OCTET; octet <= END_OCTET; octet++) {
|
|
192
|
+
const candidate = `${a}.${b}.${c}.${octet}`;
|
|
193
|
+
if (!used.has(candidate)) {
|
|
194
|
+
return `${candidate}/${prefix}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
throw new Error(
|
|
199
|
+
`No available IPs in zone ${zone} subnet ${subnet}\n` +
|
|
200
|
+
`Allocated: ${allocatedRows.length}, Reserved: ${reservedRows.length}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Allocate VMID and IP for module
|
|
206
|
+
*
|
|
207
|
+
* Main allocation function - coordinates VMID and IP allocation
|
|
208
|
+
* and stores result in database.
|
|
209
|
+
*
|
|
210
|
+
* @param moduleId - Module identifier
|
|
211
|
+
* @param zone - Network zone (dmz, app, secure)
|
|
212
|
+
* @param drizzleDb - Drizzle database instance (for inserts)
|
|
213
|
+
* @param sqliteDb - SQLite database instance (for raw queries)
|
|
214
|
+
* @returns Allocated VMID and IP
|
|
215
|
+
*/
|
|
216
|
+
export async function allocateForModule(
|
|
217
|
+
moduleId: string,
|
|
218
|
+
zone: string,
|
|
219
|
+
drizzleDb: DbClient,
|
|
220
|
+
sqliteDb: DbClient['$client'],
|
|
221
|
+
): Promise<IpamAllocation> {
|
|
222
|
+
// Check if already allocated
|
|
223
|
+
const existing = getAllocation(moduleId, drizzleDb);
|
|
224
|
+
if (existing) {
|
|
225
|
+
return existing;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Allocate VMID
|
|
229
|
+
const vmid = allocateVMID(sqliteDb);
|
|
230
|
+
|
|
231
|
+
// Allocate IP from zone subnet
|
|
232
|
+
const containerIp = allocateIP(zone, sqliteDb);
|
|
233
|
+
|
|
234
|
+
// Store allocation
|
|
235
|
+
await drizzleDb.insert(ipAllocations).values({
|
|
236
|
+
moduleId,
|
|
237
|
+
vmid,
|
|
238
|
+
containerIp,
|
|
239
|
+
zone: zone as 'dmz' | 'app' | 'secure' | 'internal',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
moduleId,
|
|
244
|
+
vmid,
|
|
245
|
+
containerIp,
|
|
246
|
+
zone,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Deallocate VMID and IP for module
|
|
252
|
+
*
|
|
253
|
+
* Removes allocation from database, making VMID and IP available
|
|
254
|
+
* for future allocations.
|
|
255
|
+
*
|
|
256
|
+
* @param moduleId - Module identifier
|
|
257
|
+
* @param db - Drizzle database instance
|
|
258
|
+
* @returns True if allocation was removed, false if none existed
|
|
259
|
+
*/
|
|
260
|
+
export async function deallocateForModule(moduleId: string, db: DbClient): Promise<boolean> {
|
|
261
|
+
// Check if allocation exists before deleting
|
|
262
|
+
const existing = getAllocation(moduleId, db);
|
|
263
|
+
if (!existing) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await db.delete(ipAllocations).where(eq(ipAllocations.moduleId, moduleId));
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
addCIDR,
|
|
4
|
+
generateIPsInSubnet,
|
|
5
|
+
isIPInRange,
|
|
6
|
+
isInSubnet,
|
|
7
|
+
parseSubnet,
|
|
8
|
+
stripCIDR,
|
|
9
|
+
} from './subnet-parser';
|
|
10
|
+
|
|
11
|
+
describe('Subnet Parser', () => {
|
|
12
|
+
describe('parseSubnet', () => {
|
|
13
|
+
test('should parse valid /24 subnet', () => {
|
|
14
|
+
const result = parseSubnet('10.0.10.0/24');
|
|
15
|
+
|
|
16
|
+
expect(result.network).toBe('10.0.10.0');
|
|
17
|
+
expect(result.maskBits).toBe(24);
|
|
18
|
+
expect(result.octets).toEqual([10, 0, 10, 0]);
|
|
19
|
+
expect(result.firstUsableIp).toBe('10.0.10.10');
|
|
20
|
+
expect(result.lastUsableIp).toBe('10.0.10.254');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should reject invalid subnet format', () => {
|
|
24
|
+
expect(() => parseSubnet('invalid')).toThrow('Invalid subnet format');
|
|
25
|
+
expect(() => parseSubnet('10.0.10.0')).toThrow('Invalid subnet format');
|
|
26
|
+
expect(() => parseSubnet('10.0.10.0/abc')).toThrow('Invalid subnet format');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should reject invalid IP address', () => {
|
|
30
|
+
expect(() => parseSubnet('256.0.10.0/24')).toThrow('Invalid IP address');
|
|
31
|
+
expect(() => parseSubnet('10.0.10/24')).toThrow('Invalid IP address');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should reject subnets smaller than /24', () => {
|
|
35
|
+
expect(() => parseSubnet('10.0.10.0/25')).toThrow('Subnet too small');
|
|
36
|
+
expect(() => parseSubnet('10.0.10.0/30')).toThrow('Subnet too small');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('stripCIDR', () => {
|
|
41
|
+
test('should strip CIDR mask', () => {
|
|
42
|
+
expect(stripCIDR('10.0.10.10/24')).toBe('10.0.10.10');
|
|
43
|
+
expect(stripCIDR('192.168.1.1/16')).toBe('192.168.1.1');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should return IP unchanged if no mask', () => {
|
|
47
|
+
expect(stripCIDR('10.0.10.10')).toBe('10.0.10.10');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('addCIDR', () => {
|
|
52
|
+
test('should add CIDR mask', () => {
|
|
53
|
+
expect(addCIDR('10.0.10.10', 24)).toBe('10.0.10.10/24');
|
|
54
|
+
expect(addCIDR('192.168.1.1', 16)).toBe('192.168.1.1/16');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('isInSubnet', () => {
|
|
59
|
+
test('should return true for IP in subnet', () => {
|
|
60
|
+
expect(isInSubnet('10.0.10.50/24', '10.0.10.0/24')).toBe(true);
|
|
61
|
+
expect(isInSubnet('10.0.10.10/24', '10.0.10.0/24')).toBe(true);
|
|
62
|
+
expect(isInSubnet('10.0.10.254/24', '10.0.10.0/24')).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should return false for IP outside subnet', () => {
|
|
66
|
+
expect(isInSubnet('10.0.11.50/24', '10.0.10.0/24')).toBe(false);
|
|
67
|
+
expect(isInSubnet('10.0.20.10/24', '10.0.10.0/24')).toBe(false);
|
|
68
|
+
expect(isInSubnet('192.168.1.1/24', '10.0.10.0/24')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('generateIPsInSubnet', () => {
|
|
73
|
+
test('should generate IPs from .10 to .254', () => {
|
|
74
|
+
const ips = Array.from(generateIPsInSubnet('10.0.10.0/24'));
|
|
75
|
+
|
|
76
|
+
expect(ips[0]).toBe('10.0.10.10/24'); // First usable (reserve .1-.9)
|
|
77
|
+
expect(ips[ips.length - 1]).toBe('10.0.10.254/24'); // Last usable
|
|
78
|
+
expect(ips.length).toBe(245); // 254 - 10 + 1 = 245 IPs
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should generate correct IPs for different subnet', () => {
|
|
82
|
+
const ips = Array.from(generateIPsInSubnet('192.168.1.0/24'));
|
|
83
|
+
|
|
84
|
+
expect(ips[0]).toBe('192.168.1.10/24');
|
|
85
|
+
expect(ips[ips.length - 1]).toBe('192.168.1.254/24');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('isIPInRange', () => {
|
|
90
|
+
test('should match single IP reservation', () => {
|
|
91
|
+
expect(isIPInRange('10.0.10.50', '10.0.10.50', null)).toBe(true);
|
|
92
|
+
expect(isIPInRange('10.0.10.51', '10.0.10.50', null)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should match IP range reservation', () => {
|
|
96
|
+
expect(isIPInRange('10.0.10.50', '10.0.10.50', '10.0.10.60')).toBe(true);
|
|
97
|
+
expect(isIPInRange('10.0.10.55', '10.0.10.50', '10.0.10.60')).toBe(true);
|
|
98
|
+
expect(isIPInRange('10.0.10.60', '10.0.10.50', '10.0.10.60')).toBe(true);
|
|
99
|
+
expect(isIPInRange('10.0.10.49', '10.0.10.50', '10.0.10.60')).toBe(false);
|
|
100
|
+
expect(isIPInRange('10.0.10.61', '10.0.10.50', '10.0.10.60')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should handle IPs with CIDR notation', () => {
|
|
104
|
+
expect(isIPInRange('10.0.10.50/24', '10.0.10.50', '10.0.10.60')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subnet parsing utilities for IPAM system
|
|
3
|
+
* Handles CIDR notation and IP address operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SubnetInfo {
|
|
7
|
+
network: string;
|
|
8
|
+
maskBits: number;
|
|
9
|
+
octets: [number, number, number, number];
|
|
10
|
+
firstUsableIp: string;
|
|
11
|
+
lastUsableIp: string;
|
|
12
|
+
totalHosts: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse subnet CIDR notation (e.g., "10.0.10.0/24")
|
|
17
|
+
* Returns network information needed for IP allocation
|
|
18
|
+
*/
|
|
19
|
+
export function parseSubnet(subnet: string): SubnetInfo {
|
|
20
|
+
const [network, maskBitsStr] = subnet.split('/');
|
|
21
|
+
const maskBits = Number.parseInt(maskBitsStr, 10);
|
|
22
|
+
|
|
23
|
+
if (!network || Number.isNaN(maskBits) || maskBits < 0 || maskBits > 32) {
|
|
24
|
+
throw new Error(`Invalid subnet format: ${subnet}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const octets = network.split('.').map((o) => {
|
|
28
|
+
const octet = Number.parseInt(o, 10);
|
|
29
|
+
if (Number.isNaN(octet) || octet < 0 || octet > 255) {
|
|
30
|
+
throw new Error(`Invalid IP address: ${network}`);
|
|
31
|
+
}
|
|
32
|
+
return octet;
|
|
33
|
+
}) as [number, number, number, number];
|
|
34
|
+
|
|
35
|
+
if (octets.length !== 4) {
|
|
36
|
+
throw new Error(`Invalid IP address: ${network}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Calculate total hosts (2^(32-maskBits) - 2 for network and broadcast)
|
|
40
|
+
const totalHosts = 2 ** (32 - maskBits) - 2;
|
|
41
|
+
|
|
42
|
+
// For simplicity, we only support /24 and larger subnets
|
|
43
|
+
if (maskBits > 24) {
|
|
44
|
+
throw new Error(`Subnet too small: ${subnet}. Celilo requires /24 or larger subnets.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Calculate first and last usable IPs
|
|
48
|
+
// We reserve .1-.9 for infrastructure, so first usable is .10
|
|
49
|
+
const firstUsableIp = `${octets[0]}.${octets[1]}.${octets[2]}.10`;
|
|
50
|
+
const lastUsableIp = `${octets[0]}.${octets[1]}.${octets[2]}.254`;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
network,
|
|
54
|
+
maskBits,
|
|
55
|
+
octets,
|
|
56
|
+
firstUsableIp,
|
|
57
|
+
lastUsableIp,
|
|
58
|
+
totalHosts,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Strip CIDR mask from IP address
|
|
64
|
+
* Example: "10.0.10.10/24" → "10.0.10.10"
|
|
65
|
+
*/
|
|
66
|
+
export function stripCIDR(ipWithMask: string): string {
|
|
67
|
+
return ipWithMask.split('/')[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Add CIDR mask to IP address
|
|
72
|
+
* Example: "10.0.10.10", 24 → "10.0.10.10/24"
|
|
73
|
+
*/
|
|
74
|
+
export function addCIDR(ip: string, maskBits: number): string {
|
|
75
|
+
return `${ip}/${maskBits}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if an IP address belongs to a subnet
|
|
80
|
+
* Example: isInSubnet("10.0.10.50/24", "10.0.10.0/24") → true
|
|
81
|
+
*/
|
|
82
|
+
export function isInSubnet(ipWithMask: string, subnet: string): boolean {
|
|
83
|
+
const ip = stripCIDR(ipWithMask);
|
|
84
|
+
const subnetInfo = parseSubnet(subnet);
|
|
85
|
+
|
|
86
|
+
const ipOctets = ip.split('.').map((o) => Number.parseInt(o, 10));
|
|
87
|
+
|
|
88
|
+
// For /24 subnet, just check first 3 octets
|
|
89
|
+
return (
|
|
90
|
+
ipOctets[0] === subnetInfo.octets[0] &&
|
|
91
|
+
ipOctets[1] === subnetInfo.octets[1] &&
|
|
92
|
+
ipOctets[2] === subnetInfo.octets[2]
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generate all possible IPs in a subnet range
|
|
98
|
+
* For /24 subnet, returns IPs from .10 to .254 (reserved .1-.9 for infrastructure)
|
|
99
|
+
*/
|
|
100
|
+
export function* generateIPsInSubnet(subnet: string): Generator<string> {
|
|
101
|
+
const subnetInfo = parseSubnet(subnet);
|
|
102
|
+
const [a, b, c] = subnetInfo.octets;
|
|
103
|
+
|
|
104
|
+
// Start from .10 (reserve .1-.9 for infrastructure)
|
|
105
|
+
// End at .254 (reserve .255 for broadcast)
|
|
106
|
+
for (let lastOctet = 10; lastOctet <= 254; lastOctet++) {
|
|
107
|
+
yield addCIDR(`${a}.${b}.${c}.${lastOctet}`, subnetInfo.maskBits);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if IP is within a reserved range
|
|
113
|
+
*/
|
|
114
|
+
export function isIPInRange(ip: string, rangeStart: string, rangeEnd: string | null): boolean {
|
|
115
|
+
const ipNum = ipToNumber(ip);
|
|
116
|
+
const startNum = ipToNumber(rangeStart);
|
|
117
|
+
|
|
118
|
+
if (!rangeEnd) {
|
|
119
|
+
// Single IP reservation
|
|
120
|
+
return ipNum === startNum;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const endNum = ipToNumber(rangeEnd);
|
|
124
|
+
return ipNum >= startNum && ipNum <= endNum;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Convert IP address to number for comparison
|
|
129
|
+
* Example: "10.0.10.50" → 167773234
|
|
130
|
+
*/
|
|
131
|
+
function ipToNumber(ip: string): number {
|
|
132
|
+
const octets = stripCIDR(ip)
|
|
133
|
+
.split('.')
|
|
134
|
+
.map((o) => Number.parseInt(o, 10));
|
|
135
|
+
return (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3];
|
|
136
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Celilo Module Contract Registry
|
|
3
|
+
*
|
|
4
|
+
* Maps contract version strings to their canonical hook signatures. The
|
|
5
|
+
* manifest's `celilo_contract` field selects which contract applies; the
|
|
6
|
+
* registry is the single source of truth for what each version promises.
|
|
7
|
+
*
|
|
8
|
+
* To mint a new contract version: add a new file (e.g. `./v2.ts`), import its
|
|
9
|
+
* contract object here, and register it in `CONTRACTS`. Old contracts stay
|
|
10
|
+
* registered indefinitely so legacy modules continue to validate.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { V1_CONTRACT } from './v1';
|
|
14
|
+
import type { ContractHookSignature, ContractHooks } from './v1';
|
|
15
|
+
|
|
16
|
+
export type { ContractHooks, ContractHookSignature };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The shape of a registered contract.
|
|
20
|
+
*/
|
|
21
|
+
export interface Contract {
|
|
22
|
+
/** Semver-style version string (e.g. "1.0"). */
|
|
23
|
+
version: string;
|
|
24
|
+
/** Canonical hook signatures for this version. */
|
|
25
|
+
hooks: ContractHooks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Registry of every contract version Celilo knows about.
|
|
30
|
+
* The keys are the strings users write in `celilo_contract`.
|
|
31
|
+
*
|
|
32
|
+
* When minting a new version, also add its string literal to
|
|
33
|
+
* `SUPPORTED_CONTRACT_VERSIONS` below so Zod's enum picks it up.
|
|
34
|
+
*/
|
|
35
|
+
export const CONTRACTS: Record<string, Contract> = {
|
|
36
|
+
'1.0': V1_CONTRACT,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Const tuple of supported contract version strings, used to drive the Zod
|
|
41
|
+
* enum that validates the manifest's `celilo_contract` field.
|
|
42
|
+
*/
|
|
43
|
+
export const SUPPORTED_CONTRACT_VERSIONS = ['1.0'] as const;
|
|
44
|
+
export type SupportedContractVersion = (typeof SUPPORTED_CONTRACT_VERSIONS)[number];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Look up a contract by version string.
|
|
48
|
+
*
|
|
49
|
+
* @returns The contract, or `undefined` if the version is not registered.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveContract(version: string): Contract | undefined {
|
|
52
|
+
return CONTRACTS[version];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List of all registered contract version strings, used for error messages
|
|
57
|
+
* and for validating the `celilo_contract` field's enum.
|
|
58
|
+
*/
|
|
59
|
+
export function supportedContractVersions(): string[] {
|
|
60
|
+
return Object.keys(CONTRACTS);
|
|
61
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Celilo Module Contract — v1.0
|
|
3
|
+
*
|
|
4
|
+
* Defines the canonical signature of every lifecycle hook in contract version
|
|
5
|
+
* "1.0". A module that declares `celilo_contract: "1.0"` in its manifest
|
|
6
|
+
* promises to implement these hooks with these exact inputs and outputs.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists:
|
|
9
|
+
* Before contract versioning, every module redeclared inputs/outputs arrays
|
|
10
|
+
* for every hook in its manifest. That meant if Celilo changed the
|
|
11
|
+
* canonical signature of a hook (say, added a required input to on_backup),
|
|
12
|
+
* we had to hand-edit every manifest to match. This file is now the single
|
|
13
|
+
* source of truth.
|
|
14
|
+
*
|
|
15
|
+
* The contract is enforced by `validateHookInputs` and `validateHookOutputs`
|
|
16
|
+
* in `apps/celilo/src/hooks/executor.ts` — at execution time the executor
|
|
17
|
+
* looks up the canonical inputs/outputs from the contract registered for the
|
|
18
|
+
* manifest's declared version.
|
|
19
|
+
*
|
|
20
|
+
* Adding a new optional input or output is a v1.x additive change.
|
|
21
|
+
* Adding a new required input or removing a required output is a breaking
|
|
22
|
+
* change and requires a v2.0 contract.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Per-input/output metadata.
|
|
27
|
+
*
|
|
28
|
+
* `required: true` — the executor enforces presence at runtime.
|
|
29
|
+
* `required: false` — the value is allowed but not mandatory; modules may
|
|
30
|
+
* produce or accept it without violating the contract.
|
|
31
|
+
*/
|
|
32
|
+
export interface ContractField {
|
|
33
|
+
required: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Signature for a single lifecycle hook within a contract version.
|
|
38
|
+
*/
|
|
39
|
+
export interface ContractHookSignature {
|
|
40
|
+
/** Inputs the executor must supply when calling the hook. */
|
|
41
|
+
inputs: Record<string, ContractField>;
|
|
42
|
+
/** Outputs the executor enforces against the hook's return value. */
|
|
43
|
+
outputs: Record<string, ContractField>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Full contract for a version: a map of canonical hook name → signature.
|
|
48
|
+
* Only hook names listed here are valid; declaring a hook not in this map is
|
|
49
|
+
* a manifest validation error.
|
|
50
|
+
*/
|
|
51
|
+
export type ContractHooks = Record<string, ContractHookSignature>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Contract v1.0 — current canonical hook signatures.
|
|
55
|
+
*
|
|
56
|
+
* These mirror the inputs/outputs that pre-Phase-2 manifests declared
|
|
57
|
+
* inline. The values were derived from a survey of every active manifest in
|
|
58
|
+
* the repo at the time the contract was minted.
|
|
59
|
+
*/
|
|
60
|
+
export const V1_HOOKS: ContractHooks = {
|
|
61
|
+
container_created: {
|
|
62
|
+
inputs: {},
|
|
63
|
+
outputs: {},
|
|
64
|
+
},
|
|
65
|
+
on_install: {
|
|
66
|
+
inputs: {},
|
|
67
|
+
outputs: {},
|
|
68
|
+
},
|
|
69
|
+
on_uninstall: {
|
|
70
|
+
inputs: {},
|
|
71
|
+
outputs: {},
|
|
72
|
+
},
|
|
73
|
+
health_check: {
|
|
74
|
+
inputs: {},
|
|
75
|
+
outputs: {},
|
|
76
|
+
},
|
|
77
|
+
validate_config: {
|
|
78
|
+
inputs: {},
|
|
79
|
+
outputs: {},
|
|
80
|
+
},
|
|
81
|
+
on_backup: {
|
|
82
|
+
inputs: {
|
|
83
|
+
backup_dir: { required: true },
|
|
84
|
+
},
|
|
85
|
+
outputs: {
|
|
86
|
+
artifact_count: { required: true },
|
|
87
|
+
size_bytes: { required: true },
|
|
88
|
+
schema_version: { required: true },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
on_backup_analyze: {
|
|
92
|
+
inputs: {
|
|
93
|
+
artifact_path: { required: true },
|
|
94
|
+
},
|
|
95
|
+
outputs: {
|
|
96
|
+
artifact_count: { required: true },
|
|
97
|
+
size_bytes: { required: true },
|
|
98
|
+
schema_version: { required: true },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
on_restore: {
|
|
102
|
+
inputs: {
|
|
103
|
+
restore_dir: { required: true },
|
|
104
|
+
schema_version: { required: true },
|
|
105
|
+
},
|
|
106
|
+
outputs: {
|
|
107
|
+
restored_items: { required: true },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Contract v1.0 metadata.
|
|
114
|
+
*/
|
|
115
|
+
export const V1_CONTRACT = {
|
|
116
|
+
version: '1.0' as const,
|
|
117
|
+
hooks: V1_HOOKS,
|
|
118
|
+
};
|