@celilo/cli 1.7.0 → 1.8.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/CELILO_CORE_MODULES.md +1 -0
- package/CELILO_SUBSYSTEMS.md +5 -1
- package/drizzle/0027_dns_internal_records_consumer_cascade.sql +43 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +2 -2
- package/src/capabilities/validation.test.ts +51 -0
- package/src/capabilities/validation.ts +22 -8
- package/src/db/dns-internal-cascade-migration.test.ts +184 -0
- package/src/db/schema.ts +21 -4
- package/src/manifest/template-validator.test.ts +47 -0
- package/src/manifest/template-validator.ts +18 -1
- package/src/module/import.ts +19 -1
- package/src/policy/capability-shape-baseline.ts +88 -0
- package/src/policy/capability-shape-drift.test.ts +162 -0
- package/src/policy/capability-shape.ts +117 -0
- package/src/policy/dns-aspect-coverage.test.ts +100 -0
- package/src/policy/module-business-baseline.ts +32 -7
- package/src/services/capability-table-rows.test.ts +191 -0
- package/src/services/capability-table-rows.ts +103 -0
- package/src/services/consumer-cleanup.ts +13 -7
- package/src/services/dns-internal-records.test.ts +72 -1
- package/src/services/module-validator/capability-versions.test.ts +6 -1
- package/src/services/port-forwards.test.ts +6 -2
- package/src/services/port-forwards.ts +0 -11
- package/src/services/trusted-sources.ts +0 -5
- package/src/variables/context.ts +75 -10
- package/src/variables/lxc-nameserver.test.ts +144 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { eq } from 'drizzle-orm';
|
|
4
5
|
import { type DbClient, createDbClient } from '../db/client';
|
|
5
6
|
import { capabilities, moduleConfigs, modules, systemConfig } from '../db/schema';
|
|
6
7
|
import { buildResolutionContext } from './context';
|
|
@@ -67,6 +68,71 @@ describe('lxc_nameserver composition', () => {
|
|
|
67
68
|
.run();
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Put the consumer in `zone` instead of the default `app`. */
|
|
72
|
+
function placeConsumerIn(zone: string): void {
|
|
73
|
+
db.update(modules)
|
|
74
|
+
.set({ manifestData: { requires: { system: { zone } } } })
|
|
75
|
+
.where(eq(modules.id, 'consumer'))
|
|
76
|
+
.run();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A dns_internal provider that advertises a zone-routable endpoint AND
|
|
81
|
+
* declares a base-module aspect, the way `knot-unbound-internal` does. The
|
|
82
|
+
* aspect's `applicable_zones` is what decides whether a system's ongoing DNS
|
|
83
|
+
* has an owner at all (design D5d).
|
|
84
|
+
*/
|
|
85
|
+
function registerManagedPrimary(opts: {
|
|
86
|
+
ip: string;
|
|
87
|
+
internalIp?: string;
|
|
88
|
+
aspectZones: string[];
|
|
89
|
+
}): void {
|
|
90
|
+
db.insert(modules)
|
|
91
|
+
.values({
|
|
92
|
+
id: 'dns-provider',
|
|
93
|
+
name: 'dns-provider',
|
|
94
|
+
version: '1.0.0',
|
|
95
|
+
manifestData: {},
|
|
96
|
+
sourcePath: '/tmp/dns',
|
|
97
|
+
})
|
|
98
|
+
.run();
|
|
99
|
+
db.insert(capabilities)
|
|
100
|
+
.values({
|
|
101
|
+
moduleId: 'dns-provider',
|
|
102
|
+
capabilityName: 'dns_internal',
|
|
103
|
+
version: '1.0.0',
|
|
104
|
+
data: {
|
|
105
|
+
server: { ip: opts.ip, internal_ip: opts.internalIp },
|
|
106
|
+
// The provider DECLARES what its aspect covers, rather than core
|
|
107
|
+
// finding the module row and reading its manifest. A manifest test
|
|
108
|
+
// holds this list and `base_module_aspect.applicable_zones` together.
|
|
109
|
+
aspect: { covered_zones: opts.aspectZones },
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
.run();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The secondary publishes its OWN capability, never a field on the primary's. */
|
|
116
|
+
function registerSecondary(opts: { ip: string; internalIp?: string }): void {
|
|
117
|
+
db.insert(modules)
|
|
118
|
+
.values({
|
|
119
|
+
id: 'dns-secondary',
|
|
120
|
+
name: 'dns-secondary',
|
|
121
|
+
version: '1.0.0',
|
|
122
|
+
manifestData: {},
|
|
123
|
+
sourcePath: '/tmp/dns2',
|
|
124
|
+
})
|
|
125
|
+
.run();
|
|
126
|
+
db.insert(capabilities)
|
|
127
|
+
.values({
|
|
128
|
+
moduleId: 'dns-secondary',
|
|
129
|
+
capabilityName: 'dns_internal_secondary',
|
|
130
|
+
version: '1.0.0',
|
|
131
|
+
data: { server: { ip: opts.ip, internal_ip: opts.internalIp } },
|
|
132
|
+
})
|
|
133
|
+
.run();
|
|
134
|
+
}
|
|
135
|
+
|
|
70
136
|
test('internal resolver first (CIDR stripped) + public fallback', async () => {
|
|
71
137
|
registerInternalDns('192.168.0.151/24');
|
|
72
138
|
const ctx = await buildResolutionContext('consumer', db);
|
|
@@ -83,4 +149,82 @@ describe('lxc_nameserver composition', () => {
|
|
|
83
149
|
const ctx = await buildResolutionContext('consumer', db);
|
|
84
150
|
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.20.30 1.1.1.1 1.0.0.1 8.8.8.8');
|
|
85
151
|
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Terraform injects `lifecycle { ignore_changes = [nameserver] }` into every
|
|
155
|
+
* `proxmox_lxc`, so it cannot correct the birth value afterwards even in
|
|
156
|
+
* principle. `lxc-dns-at-birth` splits ownership accordingly: terraform owns
|
|
157
|
+
* birth DNS, the base-module aspect owns ongoing DNS. A zone the aspect does
|
|
158
|
+
* not cover therefore has NO owner for ongoing DNS — not a late one, none —
|
|
159
|
+
* and the birth list is that system's permanent configuration.
|
|
160
|
+
*
|
|
161
|
+
* That is why dropping the public resolvers is a TIGHTENING in an
|
|
162
|
+
* aspect-covered zone and an OUTRIGHT BREAK outside one, from the same edit
|
|
163
|
+
* (design D5d). These assert on the composed CONFIGURATION rather than on a
|
|
164
|
+
* lookup result, deliberately: a resolution assertion passes whenever the
|
|
165
|
+
* internal resolver happens to be up, which is nearly always, so it would
|
|
166
|
+
* flake in the direction that reads as harness noise. The production symptom
|
|
167
|
+
* is a correct-looking answer from the wrong view, and this string is the
|
|
168
|
+
* only place that is ever visible.
|
|
169
|
+
*/
|
|
170
|
+
describe('a managed primary/secondary pair (design D5, D5d, D5e)', () => {
|
|
171
|
+
/**
|
|
172
|
+
* Both resolvers are reached the SAME way, and that is a consequence of
|
|
173
|
+
* placement rather than a convenience. Design D5e puts the secondary in
|
|
174
|
+
* `dmz` alongside the primary, so from dmz/app/secure both answer at their
|
|
175
|
+
* own zone addresses, and from `internal` — which cannot route into the dmz
|
|
176
|
+
* subnet — both answer at an internal-subnet address the firewall DNATs.
|
|
177
|
+
*
|
|
178
|
+
* This is worth stating because the symmetry is exactly what a reader
|
|
179
|
+
* should check rather than assume. Jeremy's version placed the secondary in
|
|
180
|
+
* `internal` and therefore selected its endpoints the other way round
|
|
181
|
+
* (native address for internal clients, a dmz ingress for everyone else).
|
|
182
|
+
* Under that placement this symmetric selection would be WRONG, and these
|
|
183
|
+
* tests would still pass, because the same premise would be in the test and
|
|
184
|
+
* in the code. If the secondary ever moves out of `dmz`, this block is what
|
|
185
|
+
* has to change first.
|
|
186
|
+
*/
|
|
187
|
+
test('an aspect-covered zone drops the public resolvers', async () => {
|
|
188
|
+
registerManagedPrimary({ ip: '10.0.10.5', aspectZones: ['dmz', 'app', 'secure'] });
|
|
189
|
+
registerSecondary({ ip: '10.0.10.6' });
|
|
190
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
191
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.10.5 10.0.10.6');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('a zone NO aspect covers keeps them, because nothing will ever rewrite it', async () => {
|
|
195
|
+
placeConsumerIn('external');
|
|
196
|
+
registerManagedPrimary({ ip: '10.0.10.5', aspectZones: ['dmz', 'app', 'secure'] });
|
|
197
|
+
registerSecondary({ ip: '10.0.10.6' });
|
|
198
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
199
|
+
// An `external` VPS sits outside the perimeter with no route to a
|
|
200
|
+
// dmz-resident resolver. Handing it two unreachable addresses and nothing
|
|
201
|
+
// else does not tighten anything; it takes DNS away. So the list stays
|
|
202
|
+
// what it has always been here — the primary, then the public resolvers
|
|
203
|
+
// that are the only ones it can actually reach. The secondary is not
|
|
204
|
+
// added either: a second unreachable address costs another timeout.
|
|
205
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.10.5 1.1.1.1 1.0.0.1 8.8.8.8');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('an internal-zone system takes the zone-routable endpoints', async () => {
|
|
209
|
+
placeConsumerIn('internal');
|
|
210
|
+
registerManagedPrimary({
|
|
211
|
+
ip: '10.0.10.5',
|
|
212
|
+
internalIp: '192.168.0.5',
|
|
213
|
+
aspectZones: ['dmz', 'app', 'secure', 'internal'],
|
|
214
|
+
});
|
|
215
|
+
registerSecondary({ ip: '10.0.10.6', internalIp: '192.168.0.6' });
|
|
216
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
217
|
+
// `internal` cannot route into the dmz subnet, so it uses the ingress
|
|
218
|
+
// addresses the firewall DNATs — not the resolvers' own zone addresses.
|
|
219
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('192.168.0.5 192.168.0.6');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('a primary with no secondary keeps the public fallback', async () => {
|
|
223
|
+
registerManagedPrimary({ ip: '10.0.10.5', aspectZones: ['dmz', 'app', 'secure'] });
|
|
224
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
225
|
+
// Removing the fallback and shipping a secondary are ONE decision (D5a).
|
|
226
|
+
// Without a secondary, a resolver redeploy would blank fleet DNS.
|
|
227
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.10.5 1.1.1.1 1.0.0.1 8.8.8.8');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
86
230
|
});
|