@wtfalch/foundry 0.12.0 → 0.14.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/dist/apply.d.ts +0 -16
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -1
- package/dist/migrations/0032_drop_adoptions.sql +16 -0
- package/dist/migrations/0033_declared_org_version.sql +13 -0
- package/dist/org.d.ts +6 -19
- package/dist/org.js +0 -52
- package/dist/plan/agora.d.ts +63 -1
- package/dist/plan/agora.js +91 -1
- package/dist/plan/broker-entries.d.ts +1 -5
- package/dist/plan/cloudflared-connector.d.ts +2 -10
- package/dist/plan/cloudflared-connector.js +16 -24
- package/dist/plan/diff.d.ts +0 -12
- package/dist/plan/diff.js +77 -239
- package/dist/plan/executor.d.ts +20 -93
- package/dist/plan/executor.js +208 -918
- package/dist/plan/plan-apply.d.ts +3 -47
- package/dist/plan/plan-apply.js +9 -125
- package/dist/plan/platform-diff.d.ts +0 -6
- package/dist/plan/platform-diff.js +0 -8
- package/dist/plan/providers-readers.d.ts +1 -8
- package/dist/plan/providers-readers.js +4 -58
- package/dist/plan.d.ts +0 -6
- package/dist/store/postgres.d.ts +1 -6
- package/dist/store/postgres.js +25 -74
- package/dist/store/schema.d.ts +17 -162
- package/dist/store/schema.js +3 -19
- package/dist/store.d.ts +18 -37
- package/package.json +1 -1
- package/dist/plan/adopt.d.ts +0 -112
- package/dist/plan/adopt.js +0 -252
- package/dist/plan/adoption-vault.d.ts +0 -9
- package/dist/plan/adoption-vault.js +0 -1
package/dist/plan/diff.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
2
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
|
-
import {
|
|
4
|
-
import { NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraHostMissing, desiredAgoraEnv, } from './agora.js';
|
|
3
|
+
import { CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraHostMissing, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, desiredAgoraEnv, } from './agora.js';
|
|
5
4
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
5
|
import { appHostname, entryHostnames, tunnelDnsHostnames } from './hostnames.js';
|
|
7
6
|
import { LOGIN_BINDING_SCOPE_KEY, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
8
7
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
9
|
-
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY,
|
|
8
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, appEnvAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, versionNotAppliedLine, } from './platform-diff.js';
|
|
10
9
|
import { DEFAULT_ORG_PLATFORM_READINESS } from './platform-org.js';
|
|
11
10
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
12
11
|
export function toPlan(detail) {
|
|
@@ -118,7 +117,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
118
117
|
// (`executor.ts`'s `coolifyCallsService`) reads the Synapse/MAS addresses
|
|
119
118
|
// and MAS client credentials `coolifyCalls` just recorded, the same
|
|
120
119
|
// ordering `planOwner` relies on for `zitadel:issuer`.
|
|
121
|
-
const callsService = planCallsService(org, byKey, claimed, platformReadiness.callsReady);
|
|
120
|
+
const callsService = planCallsService(org, byKey, claimed, platformReadiness.callsReady, recordedIds, calls.blocked !== null, 'error' in agoraHost ? null : callsTokenHostFrom(agoraHost));
|
|
122
121
|
if (callsService.step)
|
|
123
122
|
plannedSteps.push(callsService.step);
|
|
124
123
|
// #251: same mechanism as the platform's own version-change lines -- see
|
|
@@ -174,10 +173,6 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
174
173
|
plannedSteps.push(customers);
|
|
175
174
|
plannedSteps.push(...planIssuerApps(org, byKey, claimed));
|
|
176
175
|
plannedSteps.push(...planEntryClients(org, byKey, claimed));
|
|
177
|
-
// #311: the snapshot of each adopted resource, before the database and app
|
|
178
|
-
// steps that first write to them.
|
|
179
|
-
const adoption = planAdoptions(org, options.adoption);
|
|
180
|
-
plannedSteps.push(...adoption.steps);
|
|
181
176
|
// The Access gate before the database, app, tunnel and zone (#84): on a
|
|
182
177
|
// first apply the hostname is gated before anything answers on it.
|
|
183
178
|
plannedSteps.push(...planAccess(org, recordedIds));
|
|
@@ -187,90 +182,16 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
187
182
|
// string into the app's environment once the database exists -- it can
|
|
188
183
|
// only do that if the database step already ran and recorded the
|
|
189
184
|
// database's resource id.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const notAdoptedPark = (s) => {
|
|
193
|
-
const held = adoptedPark(org, options.adoption?.adopted ?? [], recordedIds, s);
|
|
194
|
-
if (held)
|
|
195
|
-
parks.push(held);
|
|
196
|
-
return !held;
|
|
197
|
-
};
|
|
198
|
-
// An adopt step plans its resource's step after it, whatever the markers
|
|
199
|
-
// say: the adopted resource has not been configured by foundry yet.
|
|
200
|
-
const adopting = new Set(adoption.steps.map((s) => s.action.key));
|
|
201
|
-
const database = planDatabase(org, byKey, claimed) ??
|
|
202
|
-
(adopting.has(`coolify:database:${org.id}`)
|
|
203
|
-
? {
|
|
204
|
-
action: {
|
|
205
|
-
system: 'coolify',
|
|
206
|
-
kind: 'database',
|
|
207
|
-
key: org.id,
|
|
208
|
-
desired: { enabled: true },
|
|
209
|
-
},
|
|
210
|
-
step: {
|
|
211
|
-
description: `record ${org.slug}'s adopted database`,
|
|
212
|
-
destroys: false,
|
|
213
|
-
migrates: false,
|
|
214
|
-
},
|
|
215
|
-
}
|
|
216
|
-
: null);
|
|
217
|
-
if (database &&
|
|
218
|
-
notAdoptedPark(database) &&
|
|
219
|
-
!adoption.withheld.has(`coolify:database:${org.id}`)) {
|
|
185
|
+
const database = planDatabase(org, byKey, claimed);
|
|
186
|
+
if (database)
|
|
220
187
|
plannedSteps.push(database);
|
|
221
|
-
}
|
|
222
188
|
const authIssuer = 'error' in agoraHost ? null : agoraHost.issuer;
|
|
223
|
-
const app = planApps(org, byKey, claimed, recordedIds, plannedSteps,
|
|
224
|
-
plannedSteps.push(...app.steps
|
|
189
|
+
const app = planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer);
|
|
190
|
+
plannedSteps.push(...app.steps);
|
|
225
191
|
// After the apps: the broker entry's key is minted into each app's env.
|
|
226
192
|
const brokerEntries = planBrokerEntries(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
227
193
|
plannedSteps.push(...brokerEntries.steps);
|
|
228
|
-
|
|
229
|
-
// it, whatever the markers say: foundry has not merged its routes yet (#312).
|
|
230
|
-
const tunnelKey = adoptedTunnelKey(org);
|
|
231
|
-
const connectorKey = adoptedConnectorKey(org);
|
|
232
|
-
const tunnelHeld = adoption.withheld.has(tunnelKey) || adoption.withheld.has(connectorKey);
|
|
233
|
-
const planned = planTunnel(org, byKey, claimed, recordedIds);
|
|
234
|
-
const merging = org.hosting.adopt?.tunnel !== undefined &&
|
|
235
|
-
(planned !== null || adopting.has(tunnelKey) || adopting.has(connectorKey));
|
|
236
|
-
const tunnelPlanned = merging && planned?.action.desired.enabled !== false
|
|
237
|
-
? {
|
|
238
|
-
action: {
|
|
239
|
-
system: 'cloudflare',
|
|
240
|
-
kind: 'tunnel',
|
|
241
|
-
key: org.id,
|
|
242
|
-
desired: { enabled: true },
|
|
243
|
-
},
|
|
244
|
-
step: {
|
|
245
|
-
description: `merge ${org.slug}'s routes into its adopted Cloudflare tunnel and keep the others`,
|
|
246
|
-
destroys: false,
|
|
247
|
-
migrates: false,
|
|
248
|
-
},
|
|
249
|
-
}
|
|
250
|
-
: planned;
|
|
251
|
-
// An adopted tunnel with no active snapshot and no adopt step in this plan
|
|
252
|
-
// (hosting off plans none) cannot be written to: reported blocked, no tunnel
|
|
253
|
-
// step, or the step would throw on every apply (rule 2, #312).
|
|
254
|
-
const adoptedRows = options.adoption?.adopted ?? [];
|
|
255
|
-
const activeRow = (key, uuid) => isActiveSnapshot(adoptedRows.find((r) => r.resourceKey === key), uuid ?? '');
|
|
256
|
-
const adoptable = org.hosting.adopt?.tunnel !== undefined &&
|
|
257
|
-
!tunnelHeld &&
|
|
258
|
-
!adopting.has(tunnelKey) &&
|
|
259
|
-
!adopting.has(connectorKey) &&
|
|
260
|
-
(!org.hosting.enabled ||
|
|
261
|
-
!(activeRow(tunnelKey, org.hosting.adopt.tunnel) &&
|
|
262
|
-
activeRow(connectorKey, org.hosting.adopt.connectorApp)));
|
|
263
|
-
if (adoptable) {
|
|
264
|
-
adoption.blocked.push({
|
|
265
|
-
description: `${org.slug}'s ${tunnelKey} is blocked, not planned: hosting is off, so its routes are not merged into the adopted tunnel`,
|
|
266
|
-
key: `coolify:adopt:${tunnelKey}`,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
// Neither a snapshot-less adopted tunnel nor an undeclared one is written to
|
|
270
|
-
// or parked (#312); a blocked adoption plans no tunnel step (rule 2).
|
|
271
|
-
const tunnel = tunnelPlanned && notAdoptedPark(tunnelPlanned) && !tunnelHeld && !adoptable
|
|
272
|
-
? tunnelPlanned
|
|
273
|
-
: null;
|
|
194
|
+
const tunnel = planTunnel(org, byKey, claimed, recordedIds);
|
|
274
195
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
275
196
|
// one never does.
|
|
276
197
|
if (tunnel && zoneWaiting && tunnel.action.desired.enabled !== false) {
|
|
@@ -354,6 +275,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
354
275
|
}
|
|
355
276
|
if (calls.blocked)
|
|
356
277
|
unmanaged.push({ description: calls.blocked, key: 'coolify:calls' });
|
|
278
|
+
if (callsService.blocked) {
|
|
279
|
+
unmanaged.push({ description: callsService.blocked, key: 'coolify:calls-service' });
|
|
280
|
+
}
|
|
357
281
|
if (filesOrganisation.blocked) {
|
|
358
282
|
unmanaged.push({ description: filesOrganisation.blocked, key: 'files:organisation' });
|
|
359
283
|
}
|
|
@@ -371,7 +295,6 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
371
295
|
unmanaged.push(...zoneBlocked);
|
|
372
296
|
// #243: an appEnv source nothing will record is reported, not silently skipped.
|
|
373
297
|
unmanaged.push(...app.blocked);
|
|
374
|
-
unmanaged.push(...adoption.blocked, ...parks);
|
|
375
298
|
unmanaged.push(...brokerEntries.blocked);
|
|
376
299
|
return { orgId: org.id, plannedSteps, unmanaged, versionChanges };
|
|
377
300
|
}
|
|
@@ -911,15 +834,73 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
911
834
|
* stack it rides beside is itself blocked on `callsReady` -- {@link planCalls}
|
|
912
835
|
* already reports the platform's shared SFU as missing once.
|
|
913
836
|
*/
|
|
914
|
-
function planCallsService(org, byKey, claimed, callsReady) {
|
|
837
|
+
function planCallsService(org, byKey, claimed, callsReady, recordedIds, callsBlocked, tokenHost) {
|
|
915
838
|
const key = resourceKey('coolify', 'calls-service', org.id);
|
|
916
839
|
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
917
840
|
return { step: null, blocked: null };
|
|
918
841
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
842
|
+
// The introspection secret is shown once. One minted but never written to
|
|
843
|
+
// the app is lost, so this reports it and mints no second (platform rule 1).
|
|
844
|
+
const undelivered = recordedIds[CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY];
|
|
845
|
+
if (org.calls.enabled &&
|
|
846
|
+
undelivered &&
|
|
847
|
+
!recordedIds[CALLS_INTROSPECTION_SCOPE_KEY] &&
|
|
848
|
+
byKey.has(key)) {
|
|
849
|
+
return {
|
|
850
|
+
step: null,
|
|
851
|
+
blocked: `${org.slug}'s calls service is blocked, not planned: its introspection client secret (ZITADEL app ${undelivered}) was minted but never written to the app and cannot be shown again; remove the recorded key ${CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY} (and delete the ZITADEL client) to mint a new one`,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const moved = org.calls.enabled && byKey.has(key)
|
|
855
|
+
? callsTokenEnvMovedMessage(org, tokenHost, recordedIds)
|
|
856
|
+
: null;
|
|
857
|
+
if (moved)
|
|
858
|
+
return { step: null, blocked: moved };
|
|
859
|
+
const toggle = planHostingToggle('coolify', 'calls-service', org.calls.enabled, org, byKey, claimed, () => `provision ${org.slug}'s calls service`, () => `park ${org.slug}'s calls service (stops serving; data is retained, not deleted -- #7 corrections 1)`);
|
|
860
|
+
// The host values ride in the step, as Agora's do, so the env written is
|
|
861
|
+
// the env the plan showed. Only while enabled: a park step carries none.
|
|
862
|
+
const withHost = (step) => tokenHost && org.calls.enabled
|
|
863
|
+
? {
|
|
864
|
+
...step,
|
|
865
|
+
action: {
|
|
866
|
+
...step.action,
|
|
867
|
+
desired: { ...step.action.desired, host: tokenHost },
|
|
868
|
+
},
|
|
869
|
+
}
|
|
870
|
+
: step;
|
|
871
|
+
if (toggle)
|
|
872
|
+
return { step: withHost(toggle), blocked: null };
|
|
873
|
+
// A live, enabled app made before the internal name or the token env
|
|
874
|
+
// existed has no step from the toggle. Run the step again until the
|
|
875
|
+
// executor has recorded both markers, but only while it can succeed: the
|
|
876
|
+
// SFU ready and the Matrix stack not blocked.
|
|
877
|
+
const missingName = !callsServiceHasName(org, recordedIds);
|
|
878
|
+
const missingTokenEnv = !callsServiceHasTokenEnv(tokenHost, recordedIds);
|
|
879
|
+
if (org.calls.enabled &&
|
|
880
|
+
callsReady &&
|
|
881
|
+
!callsBlocked &&
|
|
882
|
+
byKey.has(key) &&
|
|
883
|
+
(missingName || missingTokenEnv)) {
|
|
884
|
+
return {
|
|
885
|
+
step: withHost({
|
|
886
|
+
action: {
|
|
887
|
+
system: 'coolify',
|
|
888
|
+
kind: 'calls-service',
|
|
889
|
+
key: org.id,
|
|
890
|
+
desired: { enabled: true },
|
|
891
|
+
},
|
|
892
|
+
step: {
|
|
893
|
+
description: missingName
|
|
894
|
+
? `set the calls service's internal name for ${org.slug}`
|
|
895
|
+
: `set the calls service's ZITADEL token env for ${org.slug}`,
|
|
896
|
+
destroys: false,
|
|
897
|
+
migrates: false,
|
|
898
|
+
},
|
|
899
|
+
}),
|
|
900
|
+
blocked: null,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
return { step: null, blocked: null };
|
|
923
904
|
}
|
|
924
905
|
/**
|
|
925
906
|
* Deduped `ai.*` permissions offered to every organisation by default
|
|
@@ -1411,117 +1392,6 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
1411
1392
|
blocked: null,
|
|
1412
1393
|
};
|
|
1413
1394
|
}
|
|
1414
|
-
/**
|
|
1415
|
-
* #311: one `coolify:adopt` step per adopted resource that has no adoption row
|
|
1416
|
-
* yet. It `migrates`, so apply needs the org's name typed. With no vault and
|
|
1417
|
-
* store wired, each is reported blocked instead, and `withheld` names the
|
|
1418
|
-
* resources whose own step must not plan either: it would write to a live
|
|
1419
|
-
* resource nothing has snapshotted (rule 2).
|
|
1420
|
-
*/
|
|
1421
|
-
function planAdoptions(org, adoption) {
|
|
1422
|
-
const steps = [];
|
|
1423
|
-
const blocked = [];
|
|
1424
|
-
const withheld = new Set();
|
|
1425
|
-
for (const resource of adoptedResources(org)) {
|
|
1426
|
-
const what = resource.kind === 'app'
|
|
1427
|
-
? `${resource.entry.key} app`
|
|
1428
|
-
: resource.kind === 'connector'
|
|
1429
|
-
? 'cloudflared connector'
|
|
1430
|
-
: resource.kind;
|
|
1431
|
-
if (!adoption?.wired) {
|
|
1432
|
-
withheld.add(resource.resourceKey);
|
|
1433
|
-
blocked.push({
|
|
1434
|
-
description: `${org.slug}'s adoption of its ${what} is blocked, not planned: no adoption vault is wired`,
|
|
1435
|
-
key: `coolify:adopt:${resource.resourceKey}`,
|
|
1436
|
-
});
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
const row = adoption.adopted.find((r) => r.resourceKey === resource.resourceKey);
|
|
1440
|
-
// A new snapshot of another uuid would record foundry's own earlier writes.
|
|
1441
|
-
if (row?.state === 'adopted' && row.settings.uuid !== resource.uuid) {
|
|
1442
|
-
withheld.add(resource.resourceKey);
|
|
1443
|
-
blocked.push({
|
|
1444
|
-
description: `${org.slug}'s ${resource.resourceKey} is adopted as ${String(row.settings.uuid)}, not ${resource.uuid}: restore the adopted ${resource.kind} first, not planned`,
|
|
1445
|
-
key: `coolify:adopt:${resource.resourceKey}`,
|
|
1446
|
-
});
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
if (isActiveSnapshot(row, resource.uuid))
|
|
1450
|
-
continue;
|
|
1451
|
-
// A snapshot of a uuid another key holds would record foundry's own writes.
|
|
1452
|
-
const holder = adoption.adopted.find((r) => r.resourceKey !== resource.resourceKey &&
|
|
1453
|
-
protectsKey(r) &&
|
|
1454
|
-
r.settings.uuid === resource.uuid);
|
|
1455
|
-
if (holder) {
|
|
1456
|
-
withheld.add(resource.resourceKey);
|
|
1457
|
-
blocked.push({
|
|
1458
|
-
description: `${org.slug}'s ${resource.resourceKey} declares ${resource.uuid}, which ${holder.resourceKey} already adopted: declare it under one key, not planned`,
|
|
1459
|
-
key: `coolify:adopt:${resource.resourceKey}`,
|
|
1460
|
-
});
|
|
1461
|
-
continue;
|
|
1462
|
-
}
|
|
1463
|
-
steps.push({
|
|
1464
|
-
action: {
|
|
1465
|
-
system: 'coolify',
|
|
1466
|
-
kind: 'adopt',
|
|
1467
|
-
key: resource.resourceKey,
|
|
1468
|
-
desired: { uuid: resource.uuid },
|
|
1469
|
-
},
|
|
1470
|
-
step: {
|
|
1471
|
-
description: `adopt ${org.slug}'s ${what} (Coolify ${resource.uuid}): snapshot it, then change only what the org declares`,
|
|
1472
|
-
destroys: false,
|
|
1473
|
-
migrates: true,
|
|
1474
|
-
},
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
// An entry that was adopted and no longer declares `adopt` is not managed
|
|
1478
|
-
// as foundry's own app or database (rule 2), restored or not.
|
|
1479
|
-
const held = (key) => adoption?.adopted.some((r) => r.resourceKey === key && protectsKey(r));
|
|
1480
|
-
const undeclared = [
|
|
1481
|
-
...(org.hosting.enabled
|
|
1482
|
-
? [
|
|
1483
|
-
...(org.hosting.adopt?.database ? [] : [`coolify:database:${org.id}`]),
|
|
1484
|
-
...org.hosting.entries
|
|
1485
|
-
.filter((entry) => !entry.adopt)
|
|
1486
|
-
.map((entry) => `coolify:app:${appStepKey(org, entry.key)}`),
|
|
1487
|
-
]
|
|
1488
|
-
: []),
|
|
1489
|
-
// The tunnel and its connector are held with hosting off too (#312).
|
|
1490
|
-
...(org.hosting.adopt?.tunnel ? [] : [adoptedTunnelKey(org)]),
|
|
1491
|
-
...(org.hosting.adopt?.connectorApp ? [] : [adoptedConnectorKey(org)]),
|
|
1492
|
-
];
|
|
1493
|
-
for (const key of undeclared.filter(held)) {
|
|
1494
|
-
withheld.add(key);
|
|
1495
|
-
blocked.push({
|
|
1496
|
-
description: `${org.slug}'s ${key} was adopted, so foundry does not manage it as its own: declare adopt again, or run restore to put it back as it was, not planned`,
|
|
1497
|
-
key: `coolify:adopt:${key}`,
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
return { steps, blocked, withheld };
|
|
1501
|
-
}
|
|
1502
|
-
/**
|
|
1503
|
-
* The blocked item for a park step on a resource foundry adopted: its key has
|
|
1504
|
-
* a row that protects it (`adopted`, `confirmed` or `restored`), or its recorded id is a uuid the org
|
|
1505
|
-
* declares as adopted. `null` for any other step (#311).
|
|
1506
|
-
*/
|
|
1507
|
-
function adoptedPark(org, rows, recordedIds, { action }) {
|
|
1508
|
-
if (action.desired.enabled !== false)
|
|
1509
|
-
return null;
|
|
1510
|
-
const parkable = action.system === 'coolify'
|
|
1511
|
-
? action.kind === 'app' || action.kind === 'database'
|
|
1512
|
-
: action.system === 'cloudflare' && action.kind === 'tunnel';
|
|
1513
|
-
if (!parkable)
|
|
1514
|
-
return null;
|
|
1515
|
-
const key = `${action.system}:${action.kind}:${action.key}`;
|
|
1516
|
-
const id = recordedIds[key];
|
|
1517
|
-
const held = rows.some((r) => r.resourceKey === key && protectsKey(r));
|
|
1518
|
-
if (!held && !(id && declaredAdoptUuids(org).has(id)))
|
|
1519
|
-
return null;
|
|
1520
|
-
return {
|
|
1521
|
-
description: `${org.slug}'s ${key} is adopted, not stopped: foundry never stops a resource it adopted`,
|
|
1522
|
-
key: `coolify:adopt:${key}`,
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
1395
|
/**
|
|
1526
1396
|
* The shared on/off shape behind {@link planApp}, {@link planDatabase} and
|
|
1527
1397
|
* {@link planTunnel} (#71): a per-org singleton resource, keyed by the org
|
|
@@ -1626,24 +1496,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
1626
1496
|
key: stepKey,
|
|
1627
1497
|
desired: { enabled: true },
|
|
1628
1498
|
};
|
|
1629
|
-
|
|
1630
|
-
// markers say (#311).
|
|
1631
|
-
if (entry.adopt && recordedIds[appDeployPendingScopeKey(entryKey)]) {
|
|
1632
|
-
return {
|
|
1633
|
-
step: {
|
|
1634
|
-
action,
|
|
1635
|
-
step: {
|
|
1636
|
-
description: `deploy ${org.slug}'s ${label} settings to the adopted app`,
|
|
1637
|
-
destroys: false,
|
|
1638
|
-
migrates: false,
|
|
1639
|
-
},
|
|
1640
|
-
},
|
|
1641
|
-
blocked,
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
// An adopted app gets no volume (`executor.ts`'s `coolifyApp`), so a missing
|
|
1645
|
-
// one is not a drift (#311).
|
|
1646
|
-
if (live.config.hasLogVolume === false && !entry.adopt) {
|
|
1499
|
+
if (live.config.hasLogVolume === false) {
|
|
1647
1500
|
return {
|
|
1648
1501
|
step: {
|
|
1649
1502
|
action,
|
|
@@ -1662,11 +1515,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
1662
1515
|
// names (`envNames` absent) reads as compliant, like `hasLogVolume`.
|
|
1663
1516
|
const envNames = live.config.envNames;
|
|
1664
1517
|
const dbLive = byKey.get(resourceKey('coolify', 'database', org.id));
|
|
1665
|
-
|
|
1666
|
-
const dbReady = entry.database &&
|
|
1667
|
-
!entry.adopt &&
|
|
1668
|
-
!org.hosting.adopt?.database &&
|
|
1669
|
-
Boolean(recordedIds[`coolify:database:${org.id}`]);
|
|
1518
|
+
const dbReady = entry.database && Boolean(recordedIds[`coolify:database:${org.id}`]);
|
|
1670
1519
|
if (dbReady && dbLive?.config.hasInternalUrl === false) {
|
|
1671
1520
|
// Rule 2: the push cannot succeed yet, so report it rather than replan
|
|
1672
1521
|
// forever.
|
|
@@ -1776,7 +1625,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
1776
1625
|
* {@link planApp} once per hosting entry. A key with no declared entry still
|
|
1777
1626
|
* runs for `app`, so an org that turns hosting off parks the app it has live.
|
|
1778
1627
|
*/
|
|
1779
|
-
function planApps(org, byKey, claimed, recordedIds, plannedSteps,
|
|
1628
|
+
function planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer) {
|
|
1780
1629
|
const steps = [];
|
|
1781
1630
|
const blocked = [];
|
|
1782
1631
|
// An entry removed from the list while its app is recorded is parked like a
|
|
@@ -1798,17 +1647,6 @@ function planApps(org, byKey, claimed, recordedIds, plannedSteps, adopting, auth
|
|
|
1798
1647
|
continue;
|
|
1799
1648
|
}
|
|
1800
1649
|
const app = planApp(org, key, byKey, claimed, recordedIds, plannedSteps, authIssuer);
|
|
1801
|
-
const stepKey = appStepKey(org, key);
|
|
1802
|
-
if (!app.step && adopting.has(`coolify:app:${stepKey}`)) {
|
|
1803
|
-
app.step = {
|
|
1804
|
-
action: { system: 'coolify', kind: 'app', key: stepKey, desired: { enabled: true } },
|
|
1805
|
-
step: {
|
|
1806
|
-
description: `apply ${org.slug}'s ${key === APP_ENTRY_KEY ? 'app' : `${key} app`} settings, hostnames and env to the adopted app`,
|
|
1807
|
-
destroys: false,
|
|
1808
|
-
migrates: false,
|
|
1809
|
-
},
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
1650
|
// The executor pushes the issuer the plan read, the way Agora gets its host values.
|
|
1813
1651
|
if (app.step && app.step.action.desired.enabled !== false) {
|
|
1814
1652
|
const signsIn = org.hosting.entries.some((e) => e.key === key && e.uses?.auth);
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/postmaster';
|
|
2
2
|
import type { DeclaredOrg, HostingBuildPack } from '../org.js';
|
|
3
|
-
import type {
|
|
4
|
-
import type { Adoption, AdoptionStore, DeclaredOrgStore } from '../store.js';
|
|
5
|
-
import { type IngressRoute } from './adopt.js';
|
|
6
|
-
import type { AdoptionVault } from './adoption-vault.js';
|
|
3
|
+
import type { DeclaredOrgStore } from '../store.js';
|
|
7
4
|
import { type BrokerPort } from './broker-entries.js';
|
|
8
5
|
import type { StepAction } from './diff.js';
|
|
9
6
|
import { type LoginHostBindingsPort } from './login-host.js';
|
|
@@ -184,13 +181,9 @@ export interface CoolifyPort {
|
|
|
184
181
|
* `is_preview === false` the same way a deployed-config reader must.
|
|
185
182
|
*/
|
|
186
183
|
listEnvs(applicationUuid: string): Promise<Array<{
|
|
187
|
-
uuid?: string;
|
|
188
184
|
key: string;
|
|
189
185
|
value: string;
|
|
190
186
|
is_preview: boolean;
|
|
191
|
-
is_buildtime?: boolean;
|
|
192
|
-
is_literal?: boolean;
|
|
193
|
-
is_multiline?: boolean;
|
|
194
187
|
}>>;
|
|
195
188
|
/** Escape hatch (`CoolifyClient.raw`) — used for stop/start, which have no typed method yet. */
|
|
196
189
|
raw(method: string, path: string, body?: unknown): Promise<unknown>;
|
|
@@ -387,18 +380,6 @@ export interface CloudflarePort {
|
|
|
387
380
|
name: string;
|
|
388
381
|
}>;
|
|
389
382
|
putTunnelConfig(tunnelId: string, config: unknown): Promise<unknown>;
|
|
390
|
-
/**
|
|
391
|
-
* #312: the tunnel's live configuration, `config` null for a tunnel never
|
|
392
|
-
* configured. Matches `@wtfalch/providers`' `CloudflareClient.getTunnelConfig`.
|
|
393
|
-
* `cloudflareTunnel` reads it on every apply of an adopted tunnel, to merge
|
|
394
|
-
* its routes into the live ingress list rather than overwrite it.
|
|
395
|
-
*/
|
|
396
|
-
getTunnelConfig(tunnelId: string): Promise<{
|
|
397
|
-
config: {
|
|
398
|
-
ingress: IngressRoute[];
|
|
399
|
-
[field: string]: unknown;
|
|
400
|
-
} | null;
|
|
401
|
-
}>;
|
|
402
383
|
/**
|
|
403
384
|
* The tunnel's connector token (#197) -- a `cloudflared` container reads
|
|
404
385
|
* this at boot to dial into the tunnel. A live credential: matches
|
|
@@ -1223,17 +1204,6 @@ export interface FoundryStepExecutorPorts {
|
|
|
1223
1204
|
* passes the same store instance it constructs `FoundryPlanApply` with.
|
|
1224
1205
|
*/
|
|
1225
1206
|
declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone'>;
|
|
1226
|
-
/**
|
|
1227
|
-
* #311: where an adoption keeps the env values of the live app it takes
|
|
1228
|
-
* over. Optional with `adoptionStore`: without both, the planner reports
|
|
1229
|
-
* adoption as blocked and plans no `coolify:adopt` step
|
|
1230
|
-
* (`FoundryPlanApplyOptions`).
|
|
1231
|
-
*/
|
|
1232
|
-
adoptionVault?: AdoptionVault;
|
|
1233
|
-
/** #311: the rows that hold the non-secret part of each snapshot. */
|
|
1234
|
-
adoptionStore?: AdoptionStore;
|
|
1235
|
-
/** #311: the clock of a snapshot's `takenAt`; the host passes `FoundryPlanApply`'s own. Defaults to the real clock. */
|
|
1236
|
-
now?: () => Date;
|
|
1237
1207
|
}
|
|
1238
1208
|
/**
|
|
1239
1209
|
* Thrown by name for a step this executor deliberately does not implement:
|
|
@@ -1404,49 +1374,6 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1404
1374
|
private ensureZitadelProject;
|
|
1405
1375
|
private ensureCoolifyProject;
|
|
1406
1376
|
private resolveServer;
|
|
1407
|
-
/** The live app Coolify holds under `uuid`. The error names the id only (rule 6). */
|
|
1408
|
-
private readLiveApp;
|
|
1409
|
-
/** The live ingress routes of the adopted tunnel `id`. The error names the id only (rule 6). */
|
|
1410
|
-
private readAdoptedIngress;
|
|
1411
|
-
/**
|
|
1412
|
-
* #311: for each adopted resource, the lines its `coolify:adopt` step shows
|
|
1413
|
-
* (a setting with old value beside new; an env var by name only) and what
|
|
1414
|
-
* is blocked. Read from the live app on every plan. No value of an env var
|
|
1415
|
-
* leaves this method (rule 6).
|
|
1416
|
-
*/
|
|
1417
|
-
adoptionReport(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): Promise<{
|
|
1418
|
-
details: Record<string, string[]>;
|
|
1419
|
-
blocked: UnmanagedItem[];
|
|
1420
|
-
withheld: string[];
|
|
1421
|
-
}>;
|
|
1422
|
-
/**
|
|
1423
|
-
* #311: puts each adopted app back as its snapshot recorded it, touching
|
|
1424
|
-
* only what foundry writes: the settings it patches and the env names it
|
|
1425
|
-
* owns (an edit a person made to anything else stays). Owned names the
|
|
1426
|
-
* snapshot has are written back with its value and flags; owned names it
|
|
1427
|
-
* lacks are deleted; preview rows are left alone. Every vault entry and
|
|
1428
|
-
* live read happens first, so a missing entry or a row that cannot be
|
|
1429
|
-
* deleted refuses before any Coolify write. Errors name ids only, and the
|
|
1430
|
-
* vault's env values go to `bulkUpsertEnvs` and nowhere else (rule 6).
|
|
1431
|
-
*/
|
|
1432
|
-
restoreAdoption(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, rows: readonly Adoption[]): Promise<{
|
|
1433
|
-
envDeleted: string[];
|
|
1434
|
-
tokenLeft: string[];
|
|
1435
|
-
}>;
|
|
1436
|
-
/**
|
|
1437
|
-
* #311: records what a live app or database looked like before foundry
|
|
1438
|
-
* first writes to it, in this order: the env values to the vault, the
|
|
1439
|
-
* non-secret settings to the row, then both read back and compared. The
|
|
1440
|
-
* step writes nothing to Coolify, so a snapshot that fails stops the apply
|
|
1441
|
-
* before any later step does. Every error names ids only (rule 6).
|
|
1442
|
-
*/
|
|
1443
|
-
private coolifyAdopt;
|
|
1444
|
-
/** #311: whether the resource was adopted: its row is `adopted` or `confirmed`, or `id` is a declared adopt uuid. */
|
|
1445
|
-
private isAdopted;
|
|
1446
|
-
/** #311: refuses to manage as foundry's own a resource whose row protects its key and whose declaration has no `adopt`. */
|
|
1447
|
-
private refuseTakeover;
|
|
1448
|
-
/** #311: refuses to write to a declared adopt uuid that has no active snapshot. */
|
|
1449
|
-
private requireSnapshot;
|
|
1450
1377
|
private coolifyApp;
|
|
1451
1378
|
/**
|
|
1452
1379
|
* #75: mounts a persistent volume at {@link LOG_DIR} on the app, unless one
|
|
@@ -1840,6 +1767,21 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1840
1767
|
* 1); its own database is never touched from that branch.
|
|
1841
1768
|
*/
|
|
1842
1769
|
private coolifyCallsService;
|
|
1770
|
+
/**
|
|
1771
|
+
* The seven ZITADEL token env vars the calls service needs to accept
|
|
1772
|
+
* Agora's sign-in token (`calls/docs/member-auth.md`). Only called with all
|
|
1773
|
+
* inputs present, so the set is whole or absent.
|
|
1774
|
+
*
|
|
1775
|
+
* The introspection client is an API app in the messaging project (the
|
|
1776
|
+
* audience the token carries), minted once the way `platform-providers.ts`
|
|
1777
|
+
* mints the files one: its id is recorded under an undelivered key and
|
|
1778
|
+
* persisted before the secret leaves this method, the secret goes to
|
|
1779
|
+
* `deliverSecret` with retries and to the app's env, never to a log, error
|
|
1780
|
+
* or return value, and the guard is recorded by the caller after the env
|
|
1781
|
+
* write. A re-run reads the client id and secret back from the app's own
|
|
1782
|
+
* env and mints nothing.
|
|
1783
|
+
*/
|
|
1784
|
+
private callsTokenEnvs;
|
|
1843
1785
|
/**
|
|
1844
1786
|
* Reads back the calls Matrix stack's MAS client id/secret from the MAS
|
|
1845
1787
|
* app's own envs ({@link CALLS_SECRET_ENV_KEYS}) -- never mints: minting
|
|
@@ -1882,7 +1824,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1882
1824
|
* `agora` port overrides it) -- absent otherwise. The chat upstreams are
|
|
1883
1825
|
* still never pushed: chat has no admin API yet
|
|
1884
1826
|
* (https://github.com/wtfalch/chat/issues/40). Agora answers 502 on those
|
|
1885
|
-
* routes rather than serving anything wrong.
|
|
1827
|
+
* routes rather than serving anything wrong. `AGORA_CALLS_UPSTREAM` is
|
|
1828
|
+
* pushed once calls are on and this org's calls service app carries its
|
|
1829
|
+
* internal name (`desiredAgoraEnv` reads that marker from the recorded ids,
|
|
1830
|
+
* not the host values).
|
|
1886
1831
|
*/
|
|
1887
1832
|
private coolifyAgora;
|
|
1888
1833
|
/**
|
|
@@ -1910,24 +1855,6 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1910
1855
|
/** `deliverSecret`, tried up to {@link JOBS_DELIVERY_ATTEMPTS} times; true once one attempt succeeds. */
|
|
1911
1856
|
private deliverWithRetry;
|
|
1912
1857
|
private cloudflareTunnel;
|
|
1913
|
-
/**
|
|
1914
|
-
* Runs `call`; for an adopted resource a failure that is not one of this
|
|
1915
|
-
* file's own messages (which name hostnames and record types only) is
|
|
1916
|
-
* replaced by `label`, because a client's error text carries response
|
|
1917
|
-
* bodies (rule 6, #312).
|
|
1918
|
-
*/
|
|
1919
|
-
private guarded;
|
|
1920
|
-
/**
|
|
1921
|
-
* #312: writes the org's routes into the adopted tunnel's LIVE ingress list
|
|
1922
|
-
* (`mergeIngress`), not the snapshot, so a route a person added since is
|
|
1923
|
-
* kept too. The declared names go into the tunnel row's `written` only after
|
|
1924
|
-
* the write that carried them succeeded, so a crash in between leaves a
|
|
1925
|
-
* route taken as foreign and kept. Writes nothing when the merged list is the
|
|
1926
|
-
* live one. Errors name the tunnel id and the operation only.
|
|
1927
|
-
*/
|
|
1928
|
-
private mergeAdoptedIngress;
|
|
1929
|
-
/** #312: replaces the `settings` of an adoption row, read fresh so no other key is lost. The state and `takenAt` stay. */
|
|
1930
|
-
private updateAdoptionSettings;
|
|
1931
1858
|
/**
|
|
1932
1859
|
* #236: when this tunnel's DNS was last published under a domain that
|
|
1933
1860
|
* differs from the one `org` now declares, deletes every CNAME in the
|