@celilo/cli 0.5.0-alpha.5 → 0.5.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/ansible/inventory.test.ts +10 -10
- package/src/ansible/validation.test.ts +25 -15
- package/src/api-clients/proxmox.test.ts +123 -1
- package/src/api-clients/proxmox.ts +292 -7
- package/src/cli/commands/events.test.ts +4 -4
- package/src/cli/commands/events.ts +2 -2
- package/src/cli/commands/proxmox-vm-template-build.ts +164 -0
- package/src/cli/commands/service-add-proxmox.ts +62 -3
- package/src/cli/commands/service-reconfigure.test.ts +115 -0
- package/src/cli/commands/service-reconfigure.ts +110 -5
- package/src/cli/index.ts +2 -2
- package/src/config/paths.test.ts +61 -48
- package/src/hooks/capability-loader-firewall.test.ts +3 -3
- package/src/infrastructure/property-extractor.test.ts +15 -0
- package/src/infrastructure/property-extractor.ts +12 -0
- package/src/manifest/schema.ts +7 -0
- package/src/manifest/validate.test.ts +53 -0
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-secret-flow.test.ts +2 -2
- package/src/services/celilo-mgmt-hooks.test.ts +3 -2
- package/src/services/deploy-validation.test.ts +2 -2
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-registrations.test.ts +10 -10
- package/src/services/module-build.test.ts +43 -38
- package/src/templates/generator.test.ts +62 -12
- package/src/templates/generator.ts +48 -50
- package/src/test-utils/fixtures.test.ts +1 -1
- package/src/test-utils/integration-guard.ts +33 -0
- package/src/types/infrastructure.ts +6 -0
- package/src/variables/computed/computed-integration.test.ts +3 -3
- package/src/variables/computed/computed.test.ts +5 -5
- package/src/variables/declarative-derivation.test.ts +6 -6
package/src/config/paths.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { skipIntegration } from '../test-utils/integration-guard';
|
|
2
3
|
import { getDataDir, getDbPath, getMasterKeyPath, getModuleStoragePath } from './paths';
|
|
3
4
|
|
|
4
5
|
describe('paths configuration', () => {
|
|
@@ -34,18 +35,21 @@ describe('paths configuration', () => {
|
|
|
34
35
|
expect(path).toMatch(/celilo-data\/modules$/);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
|
-
it(
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
39
|
+
'returns platform-specific path by default',
|
|
40
|
+
() => {
|
|
41
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
42
|
+
process.env.ENVIRONMENT = undefined;
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
const path = getModuleStoragePath();
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if (process.platform === 'darwin') {
|
|
47
|
+
expect(path).toMatch(/Library\/Application Support\/celilo\/modules$/);
|
|
48
|
+
} else {
|
|
49
|
+
expect(path).toBe('/var/lib/celilo/modules');
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
);
|
|
49
53
|
|
|
50
54
|
it('prioritizes CELILO_DATA_DIR over ENVIRONMENT=dev', () => {
|
|
51
55
|
process.env.CELILO_DATA_DIR = '/custom/data';
|
|
@@ -73,18 +77,21 @@ describe('paths configuration', () => {
|
|
|
73
77
|
expect(path).toMatch(/celilo-data$/);
|
|
74
78
|
});
|
|
75
79
|
|
|
76
|
-
it(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
81
|
+
'returns platform-specific path by default',
|
|
82
|
+
() => {
|
|
83
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
84
|
+
process.env.ENVIRONMENT = undefined;
|
|
85
|
+
|
|
86
|
+
const path = getDataDir();
|
|
87
|
+
|
|
88
|
+
if (process.platform === 'darwin') {
|
|
89
|
+
expect(path).toMatch(/Library\/Application Support\/celilo$/);
|
|
90
|
+
} else {
|
|
91
|
+
expect(path).toBe('/var/lib/celilo');
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
describe('getMasterKeyPath', () => {
|
|
@@ -103,19 +110,22 @@ describe('paths configuration', () => {
|
|
|
103
110
|
expect(path).toBe('/custom/data/master.key');
|
|
104
111
|
});
|
|
105
112
|
|
|
106
|
-
it(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
114
|
+
'uses platform-specific data dir by default',
|
|
115
|
+
() => {
|
|
116
|
+
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
117
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
118
|
+
process.env.ENVIRONMENT = undefined;
|
|
119
|
+
|
|
120
|
+
const path = getMasterKeyPath();
|
|
121
|
+
|
|
122
|
+
if (process.platform === 'darwin') {
|
|
123
|
+
expect(path).toMatch(/Library\/Application Support\/celilo\/master\.key$/);
|
|
124
|
+
} else {
|
|
125
|
+
expect(path).toBe('/var/lib/celilo/master.key');
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
);
|
|
119
129
|
});
|
|
120
130
|
|
|
121
131
|
describe('getDbPath', () => {
|
|
@@ -128,19 +138,22 @@ describe('paths configuration', () => {
|
|
|
128
138
|
expect(path).toBe('/custom/path/celilo.db');
|
|
129
139
|
});
|
|
130
140
|
|
|
131
|
-
it(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
it.skipIf(skipIntegration({ platform: 'darwin' }))(
|
|
142
|
+
'returns data dir + celilo.db on macOS in production',
|
|
143
|
+
() => {
|
|
144
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
145
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
146
|
+
process.env.ENVIRONMENT = undefined;
|
|
147
|
+
|
|
148
|
+
const path = getDbPath();
|
|
149
|
+
|
|
150
|
+
if (process.platform === 'darwin') {
|
|
151
|
+
expect(path).toMatch(/Library\/Application Support\/celilo\/celilo.db$/);
|
|
152
|
+
} else {
|
|
153
|
+
expect(path).toBe('/var/lib/celilo/celilo.db');
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
);
|
|
144
157
|
|
|
145
158
|
it('returns celilo-data/celilo.db in development mode', () => {
|
|
146
159
|
process.env.CELILO_DB_PATH = undefined;
|
|
@@ -37,7 +37,7 @@ export default defineCapabilityFunction({
|
|
|
37
37
|
capability: 'firewall',
|
|
38
38
|
handler: ({ config, secrets }) => ({
|
|
39
39
|
exposeService: async (opts) => ({
|
|
40
|
-
externalIp: '
|
|
40
|
+
externalIp: '203.0.113.10',
|
|
41
41
|
natIp: opts.internalIp,
|
|
42
42
|
}),
|
|
43
43
|
unexposeService: async () => {},
|
|
@@ -135,7 +135,7 @@ describe('Firewall Chain Building', () => {
|
|
|
135
135
|
ports: [80],
|
|
136
136
|
description: 'test',
|
|
137
137
|
});
|
|
138
|
-
expect(exposed.externalIp).toBe('
|
|
138
|
+
expect(exposed.externalIp).toBe('203.0.113.10');
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
test('builds chain with two providers (iptables → greenwave)', async () => {
|
|
@@ -206,7 +206,7 @@ describe('Firewall Chain Building', () => {
|
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
// External IP came from greenwave (leaf)
|
|
209
|
-
expect(exposed.externalIp).toBe('
|
|
209
|
+
expect(exposed.externalIp).toBe('203.0.113.10');
|
|
210
210
|
// NAT IP is iptables' NAT address
|
|
211
211
|
expect(exposed.natIp).toBe('192.168.0.253');
|
|
212
212
|
// iptables created local rules
|
|
@@ -78,6 +78,21 @@ describe('extractProxmoxProperties', () => {
|
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
test('omits vm_template when the service config has none (LXC services)', () => {
|
|
82
|
+
const properties = extractProxmoxProperties(100, '10.0.10.5', 'caddy', mockProxmoxConfig);
|
|
83
|
+
// Omitted, not empty — so a required-infrastructure `vm_template` var fails
|
|
84
|
+
// loudly rather than resolving to "".
|
|
85
|
+
expect('vm_template' in properties).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('includes vm_template when the service config provides one (VM services)', () => {
|
|
89
|
+
const properties = extractProxmoxProperties(100, '10.0.10.5', 'builder', {
|
|
90
|
+
...mockProxmoxConfig,
|
|
91
|
+
vm_template: 'ubuntu-2204-cloudinit',
|
|
92
|
+
});
|
|
93
|
+
expect(properties.vm_template).toBe('ubuntu-2204-cloudinit');
|
|
94
|
+
});
|
|
95
|
+
|
|
81
96
|
test('converts vmid number to string', () => {
|
|
82
97
|
const properties = extractProxmoxProperties(12345, '10.0.20.15', 'caddy', mockProxmoxConfig);
|
|
83
98
|
|
|
@@ -47,6 +47,13 @@ export interface ProxmoxProviderConfig {
|
|
|
47
47
|
default_target_node: string;
|
|
48
48
|
lxc_template: string;
|
|
49
49
|
storage: string;
|
|
50
|
+
/**
|
|
51
|
+
* Cloud-init VM template to clone for `requires.system.type: vm` modules — the
|
|
52
|
+
* VM analogue of `lxc_template`. Optional: only Proxmox services that host VM
|
|
53
|
+
* modules configure it. A VM module declares `vm_template` as a *required*
|
|
54
|
+
* infrastructure var, so a service missing it fails loudly at resolution.
|
|
55
|
+
*/
|
|
56
|
+
vm_template?: string;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/**
|
|
@@ -72,6 +79,11 @@ export function extractProxmoxProperties(
|
|
|
72
79
|
target_node: providerConfig.default_target_node,
|
|
73
80
|
lxc_template: providerConfig.lxc_template,
|
|
74
81
|
storage: providerConfig.storage,
|
|
82
|
+
// VM clone source — present only when the service configures it. VM modules
|
|
83
|
+
// declare `vm_template` as a required infrastructure var (resolution errors
|
|
84
|
+
// if absent); LXC modules never reference it. Omitted (not empty) when unset
|
|
85
|
+
// so the resolver's required/optional handling stays correct.
|
|
86
|
+
...(providerConfig.vm_template ? { vm_template: providerConfig.vm_template } : {}),
|
|
75
87
|
};
|
|
76
88
|
}
|
|
77
89
|
|
package/src/manifest/schema.ts
CHANGED
|
@@ -311,6 +311,13 @@ export const SystemResourceSchema = z.object({
|
|
|
311
311
|
memory: z.number().int().positive().optional().describe('Recommended memory in MB'),
|
|
312
312
|
disk: z.number().int().positive().optional().describe('Recommended disk size in GB'),
|
|
313
313
|
storage: z.string().optional().describe('Proxmox storage backend (defaults to system config)'),
|
|
314
|
+
type: z
|
|
315
|
+
.enum(['lxc', 'vm'])
|
|
316
|
+
.default('lxc')
|
|
317
|
+
.describe(
|
|
318
|
+
'Proxmox provisioning type: lxc (default) or vm (qemu, for Docker / kernel-module workloads). ' +
|
|
319
|
+
'Modules declare this explicitly; celilo never infers it. Moot for machine-pool / external infra.',
|
|
320
|
+
),
|
|
314
321
|
zone: z
|
|
315
322
|
.enum(['internal', 'dmz', 'app', 'secure', 'external'])
|
|
316
323
|
.describe('Required security zone for this module'),
|
|
@@ -77,6 +77,59 @@ variables:
|
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
test('requires.system.type defaults to lxc when omitted', () => {
|
|
81
|
+
const yaml = `
|
|
82
|
+
${CONTRACT_LINE}
|
|
83
|
+
id: homebridge
|
|
84
|
+
name: Homebridge
|
|
85
|
+
version: 1.0.0
|
|
86
|
+
requires:
|
|
87
|
+
system:
|
|
88
|
+
cpu: 1
|
|
89
|
+
zone: app
|
|
90
|
+
`;
|
|
91
|
+
const result = validateManifest(yaml);
|
|
92
|
+
expect(result.success).toBe(true);
|
|
93
|
+
if (result.success) {
|
|
94
|
+
expect(result.data.requires.system?.type).toBe('lxc');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('requires.system.type accepts an explicit vm', () => {
|
|
99
|
+
const yaml = `
|
|
100
|
+
${CONTRACT_LINE}
|
|
101
|
+
id: forgejo-runner
|
|
102
|
+
name: Forgejo Runner
|
|
103
|
+
version: 1.0.0
|
|
104
|
+
requires:
|
|
105
|
+
system:
|
|
106
|
+
cpu: 4
|
|
107
|
+
memory: 8192
|
|
108
|
+
type: vm
|
|
109
|
+
zone: dmz
|
|
110
|
+
`;
|
|
111
|
+
const result = validateManifest(yaml);
|
|
112
|
+
expect(result.success).toBe(true);
|
|
113
|
+
if (result.success) {
|
|
114
|
+
expect(result.data.requires.system?.type).toBe('vm');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('requires.system.type rejects an unknown infra type', () => {
|
|
119
|
+
const yaml = `
|
|
120
|
+
${CONTRACT_LINE}
|
|
121
|
+
id: homebridge
|
|
122
|
+
name: Homebridge
|
|
123
|
+
version: 1.0.0
|
|
124
|
+
requires:
|
|
125
|
+
system:
|
|
126
|
+
type: container
|
|
127
|
+
zone: app
|
|
128
|
+
`;
|
|
129
|
+
const result = validateManifest(yaml);
|
|
130
|
+
expect(result.success).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
80
133
|
test('should validate dns-external manifest with capability provider', () => {
|
|
81
134
|
const yaml = `
|
|
82
135
|
${CONTRACT_LINE}
|
|
@@ -42,7 +42,7 @@ describe('busInterview', () => {
|
|
|
42
42
|
const watch = responderBus.watch('config.required.lunacycle.domain', (event) => {
|
|
43
43
|
responderBus.emitRaw(
|
|
44
44
|
`${event.type}.reply`,
|
|
45
|
-
{ value: '
|
|
45
|
+
{ value: 'example.net' },
|
|
46
46
|
{ replyFor: event.id, emittedBy: 'test-responder' },
|
|
47
47
|
);
|
|
48
48
|
});
|
|
@@ -58,7 +58,7 @@ describe('busInterview', () => {
|
|
|
58
58
|
payload,
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
-
expect(reply.value).toBe('
|
|
61
|
+
expect(reply.value).toBe('example.net');
|
|
62
62
|
|
|
63
63
|
watch.close();
|
|
64
64
|
responderBus.close();
|
|
@@ -350,7 +350,7 @@ describe('bus-mediated interviewForMissingSecrets', () => {
|
|
|
350
350
|
const { seen, close } = startTestResponder(
|
|
351
351
|
bus,
|
|
352
352
|
'secret.required.testmod.ddns_passwords',
|
|
353
|
-
{ value: JSON.stringify({ '
|
|
353
|
+
{ value: JSON.stringify({ 'example.net': 'pw1', 'celilo.computer': 'pw2' }) },
|
|
354
354
|
testDb,
|
|
355
355
|
);
|
|
356
356
|
|
|
@@ -380,7 +380,7 @@ describe('bus-mediated interviewForMissingSecrets', () => {
|
|
|
380
380
|
|
|
381
381
|
const masterKey = await getOrCreateMasterKey();
|
|
382
382
|
const stored = await readModuleSecretKey('testmod', 'ddns_passwords', testDb, masterKey);
|
|
383
|
-
expect(stored).toBe(JSON.stringify({ '
|
|
383
|
+
expect(stored).toBe(JSON.stringify({ 'example.net': 'pw1', 'celilo.computer': 'pw2' }));
|
|
384
384
|
|
|
385
385
|
close();
|
|
386
386
|
});
|
|
@@ -18,6 +18,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
|
18
18
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { tmpdir } from 'node:os';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
+
import { skipIntegration } from '../test-utils/integration-guard';
|
|
21
22
|
|
|
22
23
|
import type { HookContext } from '@celilo/capabilities';
|
|
23
24
|
|
|
@@ -77,7 +78,7 @@ function buildContext(extras: Record<string, unknown>): HookContext {
|
|
|
77
78
|
} as HookContext;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
describe('celilo-mgmt on_backup', () => {
|
|
81
|
+
describe.skipIf(skipIntegration({ tools: ['wg'] }))('celilo-mgmt on_backup', () => {
|
|
81
82
|
let dir: string;
|
|
82
83
|
let backupDir: string;
|
|
83
84
|
let crossModuleRoot: string;
|
|
@@ -214,7 +215,7 @@ describe('celilo-mgmt on_backup', () => {
|
|
|
214
215
|
});
|
|
215
216
|
});
|
|
216
217
|
|
|
217
|
-
describe('celilo-mgmt on_restore', () => {
|
|
218
|
+
describe.skipIf(skipIntegration({ tools: ['wg'] }))('celilo-mgmt on_restore', () => {
|
|
218
219
|
let dir: string;
|
|
219
220
|
let restoreDir: string;
|
|
220
221
|
let crossModuleWriteRoot: string;
|
|
@@ -265,8 +265,8 @@ describe('findMissingSecrets (shared)', () => {
|
|
|
265
265
|
// The terminal-responder reads these off the bus payload to apply
|
|
266
266
|
// input-time regex validation. Without manifest → MissingVariable
|
|
267
267
|
// propagation, the responder never sees them and operators end
|
|
268
|
-
// up entering invalid keys (e.g. 'www.
|
|
269
|
-
// the apex '
|
|
268
|
+
// up entering invalid keys (e.g. 'www.example.net' instead of
|
|
269
|
+
// the apex 'example.net').
|
|
270
270
|
const missing = await findMissingSecrets(
|
|
271
271
|
'testmod',
|
|
272
272
|
{
|
|
@@ -105,14 +105,14 @@ describe('backfillWebRouteDns (ISS-0029)', () => {
|
|
|
105
105
|
seedFirewall('100.64.0.1');
|
|
106
106
|
seedRoute('apt.celilo.computer', '/');
|
|
107
107
|
seedRoute('apt.celilo.computer', '/-/publish'); // same host, different path → deduped
|
|
108
|
-
seedRoute('registry.
|
|
108
|
+
seedRoute('registry.example.net', '/');
|
|
109
109
|
|
|
110
110
|
const calls: DnsRecordRequest[] = [];
|
|
111
111
|
await backfillWebRouteDns('technitium', getDb(), silentLogger, stubLoader(calls));
|
|
112
112
|
|
|
113
113
|
expect(calls.map((c) => c.host).sort()).toEqual([
|
|
114
114
|
'apt.celilo.computer',
|
|
115
|
-
'registry.
|
|
115
|
+
'registry.example.net',
|
|
116
116
|
]);
|
|
117
117
|
expect(calls.every((c) => c.value === '100.64.0.1' && c.type === 'A')).toBe(true);
|
|
118
118
|
});
|
|
@@ -50,19 +50,19 @@ describe('dns_registrations ledger', () => {
|
|
|
50
50
|
recordDnsRegistration(db, {
|
|
51
51
|
providerModuleId: 'namecheap',
|
|
52
52
|
consumerModuleId: 'caddy',
|
|
53
|
-
fqdn: 'www.
|
|
53
|
+
fqdn: 'www.example.net',
|
|
54
54
|
ip: '198.51.100.7',
|
|
55
55
|
});
|
|
56
56
|
recordDnsRegistration(db, {
|
|
57
57
|
providerModuleId: 'namecheap',
|
|
58
58
|
consumerModuleId: 'caddy',
|
|
59
|
-
fqdn: 'www.
|
|
59
|
+
fqdn: 'www.example.net',
|
|
60
60
|
ip: '198.51.100.8',
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const rows = listDnsRegistrations(db, { providerModuleId: 'namecheap' });
|
|
64
64
|
expect(rows.length).toBe(1);
|
|
65
|
-
expect(rows[0].fqdn).toBe('www.
|
|
65
|
+
expect(rows[0].fqdn).toBe('www.example.net');
|
|
66
66
|
expect(rows[0].ip).toBe('198.51.100.8');
|
|
67
67
|
expect(rows[0].consumerModuleId).toBe('caddy');
|
|
68
68
|
expect(rows[0].refreshedAt).toBeNull();
|
|
@@ -72,7 +72,7 @@ describe('dns_registrations ledger', () => {
|
|
|
72
72
|
recordDnsRegistration(db, {
|
|
73
73
|
providerModuleId: 'namecheap',
|
|
74
74
|
consumerModuleId: 'caddy',
|
|
75
|
-
fqdn: 'git.
|
|
75
|
+
fqdn: 'git.example.net',
|
|
76
76
|
ip: null,
|
|
77
77
|
});
|
|
78
78
|
stampDnsRegistrationsRefreshed(db, 'namecheap');
|
|
@@ -84,7 +84,7 @@ describe('dns_registrations ledger', () => {
|
|
|
84
84
|
recordDnsRegistration(db, {
|
|
85
85
|
providerModuleId: 'namecheap',
|
|
86
86
|
consumerModuleId: 'caddy',
|
|
87
|
-
fqdn: 'www.
|
|
87
|
+
fqdn: 'www.example.net',
|
|
88
88
|
ip: '198.51.100.7',
|
|
89
89
|
});
|
|
90
90
|
db.delete(modules).where(eq(modules.id, 'caddy')).run();
|
|
@@ -106,15 +106,15 @@ describe('dns_registrations ledger', () => {
|
|
|
106
106
|
consumerModuleId: 'caddy',
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
await wrapped.registerHost({ fqdn: 'www.
|
|
110
|
-
await wrapped.registerHost({ fqdn: 'fail.
|
|
111
|
-
await wrapped.registerHost({ fqdn: 'auto.
|
|
109
|
+
await wrapped.registerHost({ fqdn: 'www.example.net', ip: '198.51.100.7' });
|
|
110
|
+
await wrapped.registerHost({ fqdn: 'fail.example.net', ip: '198.51.100.7' });
|
|
111
|
+
await wrapped.registerHost({ fqdn: 'auto.example.net' });
|
|
112
112
|
|
|
113
113
|
expect(calls.length).toBe(3);
|
|
114
114
|
const rows = listDnsRegistrations(db);
|
|
115
115
|
const fqdns = rows.map((r) => r.fqdn).sort();
|
|
116
|
-
expect(fqdns).toEqual(['auto.
|
|
117
|
-
const auto = rows.find((r) => r.fqdn === 'auto.
|
|
116
|
+
expect(fqdns).toEqual(['auto.example.net', 'www.example.net']);
|
|
117
|
+
const auto = rows.find((r) => r.fqdn === 'auto.example.net');
|
|
118
118
|
expect(auto?.ip).toBeNull();
|
|
119
119
|
});
|
|
120
120
|
});
|
|
@@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm';
|
|
|
5
5
|
import { log } from '../cli/prompts';
|
|
6
6
|
import { type DbClient, createDbClient } from '../db/client';
|
|
7
7
|
import { moduleBuilds, modules } from '../db/schema';
|
|
8
|
+
import { skipIntegration } from '../test-utils/integration-guard';
|
|
8
9
|
import { buildModuleFromSource, getModuleBuildStatus, verifyArtifactsExist } from './module-build';
|
|
9
10
|
|
|
10
11
|
const TEST_DB_PATH = './test-module-build.db';
|
|
@@ -156,12 +157,14 @@ describe('Module Build Service', () => {
|
|
|
156
157
|
// We're just testing that detection happens (message logged)
|
|
157
158
|
}, 10000); // 10 second timeout
|
|
158
159
|
|
|
159
|
-
test(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`${TEST_MODULE_DIR}/build
|
|
164
|
-
|
|
160
|
+
test.skipIf(skipIntegration({ tools: ['ansible'] }))(
|
|
161
|
+
'should record build metadata in database',
|
|
162
|
+
async () => {
|
|
163
|
+
await mkdir(TEST_MODULE_DIR, { recursive: true });
|
|
164
|
+
await mkdir(`${TEST_MODULE_DIR}/build`, { recursive: true });
|
|
165
|
+
await writeFile(
|
|
166
|
+
`${TEST_MODULE_DIR}/build/playbook.yml`,
|
|
167
|
+
`---
|
|
165
168
|
- name: Quick test build
|
|
166
169
|
hosts: localhost
|
|
167
170
|
gather_facts: false
|
|
@@ -170,43 +173,45 @@ describe('Module Build Service', () => {
|
|
|
170
173
|
ansible.builtin.debug:
|
|
171
174
|
msg: "Build test"
|
|
172
175
|
`,
|
|
173
|
-
|
|
176
|
+
);
|
|
174
177
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
id: 'record-test',
|
|
178
|
-
name: 'Record Test',
|
|
179
|
-
version: '1.0.0',
|
|
180
|
-
sourcePath: TEST_MODULE_DIR,
|
|
181
|
-
manifestData: {
|
|
178
|
+
db.insert(modules)
|
|
179
|
+
.values({
|
|
182
180
|
id: 'record-test',
|
|
183
181
|
name: 'Record Test',
|
|
184
182
|
version: '1.0.0',
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
sourcePath: TEST_MODULE_DIR,
|
|
184
|
+
manifestData: {
|
|
185
|
+
id: 'record-test',
|
|
186
|
+
name: 'Record Test',
|
|
187
|
+
version: '1.0.0',
|
|
188
|
+
build: {
|
|
189
|
+
script: 'build/playbook.yml',
|
|
190
|
+
},
|
|
187
191
|
},
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
192
|
+
})
|
|
193
|
+
.run();
|
|
194
|
+
|
|
195
|
+
const result = await buildModuleFromSource('record-test', db);
|
|
196
|
+
|
|
197
|
+
// Build should succeed (simple debug task)
|
|
198
|
+
expect(result.success).toBe(true);
|
|
199
|
+
|
|
200
|
+
// Verify build metadata was recorded
|
|
201
|
+
const buildRecord = await db
|
|
202
|
+
.select()
|
|
203
|
+
.from(moduleBuilds)
|
|
204
|
+
.where(eq(moduleBuilds.moduleId, 'record-test'))
|
|
205
|
+
.get();
|
|
206
|
+
|
|
207
|
+
expect(buildRecord).toBeDefined();
|
|
208
|
+
expect(buildRecord?.moduleId).toBe('record-test');
|
|
209
|
+
expect(buildRecord?.version).toBe('1.0.0');
|
|
210
|
+
expect(buildRecord?.status).toBe('success');
|
|
211
|
+
expect(buildRecord?.environment).toBe('system'); // No flake.nix
|
|
212
|
+
},
|
|
213
|
+
10000,
|
|
214
|
+
); // 10 second timeout for ansible execution
|
|
210
215
|
|
|
211
216
|
// Regression: a build.command that references $CELILO_MODULE_SOURCE_DIR
|
|
212
217
|
// (the variable celilo-registry's manifest uses to find sibling packages
|