@wtfalch/foundry 0.17.1 → 0.18.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/bootstrap/declare.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0045_names_to_edge.sql +107 -0
- package/dist/org.d.ts +17 -17
- package/dist/org.js +48 -58
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +16 -16
- package/dist/plan/entry-edge-key.d.ts +79 -0
- package/dist/plan/{entry-names-key.js → entry-edge-key.js} +90 -66
- package/dist/plan/executor.d.ts +48 -37
- package/dist/plan/executor.js +166 -98
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +3 -3
- package/dist/plan/plan-apply.d.ts +6 -5
- package/dist/plan/plan-apply.js +40 -38
- package/dist/plan/platform-diff.d.ts +22 -22
- package/dist/plan/platform-diff.js +45 -45
- package/dist/plan/platform-org.d.ts +8 -8
- package/dist/plan/platform-org.js +7 -7
- package/dist/plan/platform-providers.d.ts +14 -14
- package/dist/plan/platform-providers.js +107 -107
- package/dist/plan/step-system.js +1 -1
- package/dist/plan/{edge-worker.d.ts → worker.d.ts} +53 -33
- package/dist/plan/{edge-worker.js → worker.js} +92 -54
- package/dist/platform.d.ts +3 -3
- package/dist/platform.js +1 -1
- package/dist/store/postgres.js +7 -7
- package/dist/store/schema.d.ts +6 -6
- package/dist/store/schema.js +4 -4
- package/package.json +1 -1
- package/dist/plan/entry-names-key.d.ts +0 -73
package/dist/plan/plan-apply.js
CHANGED
|
@@ -7,15 +7,15 @@ import { StaleLeaseError, } from '../store.js';
|
|
|
7
7
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
8
8
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
9
9
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
10
|
-
import { edgeWorkerTraceScopeKeys, liveEdgeWorkers } from './edge-worker.js';
|
|
11
10
|
import { entryAiUndeliveredScopeKey } from './entry-ai-key.js';
|
|
11
|
+
import { entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey } from './entry-edge-key.js';
|
|
12
12
|
import { FILES_ROOT_UNDELIVERED_SCOPE_KEY, entryFilesUndeliveredScopeKey, } from './entry-files-key.js';
|
|
13
|
-
import { entryNamesRetiredScopeKey, entryNamesUndeliveredScopeKey } from './entry-names-key.js';
|
|
14
13
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
15
14
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
16
15
|
import { PLATFORM_ARCHON_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_KEY, appStepKey, planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
17
16
|
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
18
17
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
18
|
+
import { liveWorkers, workerTraceScopeKeys } from './worker.js';
|
|
19
19
|
/** #441: the lease scope that serializes every mutation of one company, and the one for the shared platform. */
|
|
20
20
|
const companyScope = (orgId) => `company:${orgId}`;
|
|
21
21
|
const PLATFORM_SCOPE = 'platform';
|
|
@@ -100,7 +100,7 @@ const HOSTNAME_STEPS = new Set([
|
|
|
100
100
|
'cloudflare:access',
|
|
101
101
|
'jobs:registration',
|
|
102
102
|
// #318 (F8): the route is itself hostname-shaped, validated against the
|
|
103
|
-
// org's own domain by `
|
|
103
|
+
// org's own domain by `validateEntryWorker`.
|
|
104
104
|
'cloudflare:worker',
|
|
105
105
|
]);
|
|
106
106
|
/**
|
|
@@ -124,7 +124,7 @@ export class FoundryPlanApply {
|
|
|
124
124
|
edgeWorkers;
|
|
125
125
|
hostFiles;
|
|
126
126
|
hostAi;
|
|
127
|
-
|
|
127
|
+
hostEdge;
|
|
128
128
|
now;
|
|
129
129
|
leaseTtlMs;
|
|
130
130
|
appliedVersions;
|
|
@@ -145,7 +145,7 @@ export class FoundryPlanApply {
|
|
|
145
145
|
this.edgeWorkers = options.edgeWorkers ?? false;
|
|
146
146
|
this.hostFiles = options.filesOrganisation ?? false;
|
|
147
147
|
this.hostAi = options.aiOrganisation ?? false;
|
|
148
|
-
this.
|
|
148
|
+
this.hostEdge = options.edge ?? false;
|
|
149
149
|
this.now = options.now ?? (() => new Date());
|
|
150
150
|
this.leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
151
151
|
this.appliedVersions = options.appliedVersions;
|
|
@@ -543,7 +543,7 @@ export class FoundryPlanApply {
|
|
|
543
543
|
// #318: its partial-deploy traces too, or a retry after a later
|
|
544
544
|
// step fails would read this Worker as live again.
|
|
545
545
|
const entryKey = target.key.slice(id.length + 1);
|
|
546
|
-
for (const scopeKey of
|
|
546
|
+
for (const scopeKey of workerTraceScopeKeys(entryKey)) {
|
|
547
547
|
delete serviceIds[scopeKey];
|
|
548
548
|
}
|
|
549
549
|
}
|
|
@@ -869,6 +869,7 @@ export class FoundryPlanApply {
|
|
|
869
869
|
this.orgArchonUrl(),
|
|
870
870
|
]);
|
|
871
871
|
const otherSlugs = (await this.store.list()).filter((o) => o.id !== id).map((o) => o.slug);
|
|
872
|
+
const platformHostnames = await this.platformServiceHostnames();
|
|
872
873
|
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, {
|
|
873
874
|
loginHostBindings: this.loginHostBindings,
|
|
874
875
|
broker: this.broker,
|
|
@@ -876,6 +877,7 @@ export class FoundryPlanApply {
|
|
|
876
877
|
otherSlugs,
|
|
877
878
|
now: () => this.now().getTime(),
|
|
878
879
|
archonUrl,
|
|
880
|
+
platformHostnames,
|
|
879
881
|
});
|
|
880
882
|
const invalid = await this.invalidHosting(org);
|
|
881
883
|
if (invalid) {
|
|
@@ -895,15 +897,15 @@ export class FoundryPlanApply {
|
|
|
895
897
|
* which only the plan can read.
|
|
896
898
|
*/
|
|
897
899
|
async invalidHosting(org) {
|
|
898
|
-
const platform = await this.platformDeps?.store.get();
|
|
899
|
-
const platformHostnames = platform
|
|
900
|
-
? PLATFORM_SERVICE_KEYS.map((k) => platform[k].hostname)
|
|
901
|
-
: [];
|
|
902
900
|
return validateHosting(org.hosting, {
|
|
903
901
|
domain: org.domain,
|
|
904
|
-
locked: lockedHostnames(org,
|
|
902
|
+
locked: lockedHostnames(org, await this.platformServiceHostnames()),
|
|
905
903
|
});
|
|
906
904
|
}
|
|
905
|
+
async platformServiceHostnames() {
|
|
906
|
+
const platform = await this.platformDeps?.store.get();
|
|
907
|
+
return platform ? PLATFORM_SERVICE_KEYS.map((k) => platform[k].hostname) : [];
|
|
908
|
+
}
|
|
907
909
|
/**
|
|
908
910
|
* #177: an org's plan reads the shared platform's own addresses and
|
|
909
911
|
* readiness through the very `platform` deps `planPlatform`/`applyPlatform`
|
|
@@ -927,7 +929,7 @@ export class FoundryPlanApply {
|
|
|
927
929
|
refs.add(ORG_PLATFORM_KEY_REFS.ai);
|
|
928
930
|
if (this.jobsScheduler)
|
|
929
931
|
refs.add(ORG_PLATFORM_KEY_REFS.jobs);
|
|
930
|
-
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys, this.
|
|
932
|
+
const readiness = orgPlatformReadinessFrom(platform, recorded, refs, !heldKeys, this.hostEdge);
|
|
931
933
|
// Entry env from the platform's mail key or a shared secret (#T8).
|
|
932
934
|
let mailKeyHeld = false;
|
|
933
935
|
try {
|
|
@@ -1138,7 +1140,7 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
1138
1140
|
'zitadel:login-host:',
|
|
1139
1141
|
'cloudflare:tunnel:',
|
|
1140
1142
|
'cloudflare:access:',
|
|
1141
|
-
// #318: an entry's
|
|
1143
|
+
// #318: an entry's Worker and its route.
|
|
1142
1144
|
'cloudflare:worker:',
|
|
1143
1145
|
'coolify:database:',
|
|
1144
1146
|
'coolify:app:',
|
|
@@ -1147,18 +1149,18 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
1147
1149
|
'ai:key:',
|
|
1148
1150
|
// F6b: an entry's files key, `<orgId>:<entry>`; the org's own key has no `:` and is left alone.
|
|
1149
1151
|
'files:key:',
|
|
1150
|
-
// F7 (#317): an entry's
|
|
1151
|
-
'
|
|
1152
|
+
// F7 (#317): an entry's edge key, `<orgId>:<entry>` -- edge has no org-root key.
|
|
1153
|
+
'edge:key:',
|
|
1152
1154
|
];
|
|
1153
1155
|
const targets = [];
|
|
1154
1156
|
const adopted = adoptedResourceIds(serviceIds);
|
|
1155
1157
|
for (const prefix of ORDERED_PREFIXES) {
|
|
1156
1158
|
if (prefix === 'cloudflare:worker:') {
|
|
1157
|
-
// #318 re-review: not the main ids alone -- `
|
|
1159
|
+
// #318 re-review: not the main ids alone -- `liveWorkers` also
|
|
1158
1160
|
// finds an upload-only or route-only partial deploy, and leaves out an
|
|
1159
1161
|
// entry whose Worker is already retired (nothing live, so a host with
|
|
1160
1162
|
// no WorkersPort is not blocked from destroying the org by it).
|
|
1161
|
-
for (const [entryKey, scriptName] of
|
|
1163
|
+
for (const [entryKey, scriptName] of liveWorkers(orgId, serviceIds)) {
|
|
1162
1164
|
targets.push({
|
|
1163
1165
|
system: 'cloudflare',
|
|
1164
1166
|
kind: 'worker',
|
|
@@ -1181,16 +1183,16 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
1181
1183
|
// A key a retire step already revoked before org destroy ever ran
|
|
1182
1184
|
// (#317 review): `runSteps` never clears this main id on a step that
|
|
1183
1185
|
// returns no resourceId, so without this check destroy would target
|
|
1184
|
-
// an already-revoked key and get stuck on
|
|
1185
|
-
if (prefix === '
|
|
1186
|
+
// an already-revoked key and get stuck on edge' 404 forever.
|
|
1187
|
+
if (prefix === 'edge:key:') {
|
|
1186
1188
|
// `resourceKey.slice(prefix.length)` is `<orgId>:<entryKey>`, but
|
|
1187
|
-
// `
|
|
1188
|
-
// every caller in entry-
|
|
1189
|
+
// `entryEdgeRetiredScopeKey` takes the bare entry key alone (as
|
|
1190
|
+
// every caller in entry-edge-key.ts/executor.ts gives it) --
|
|
1189
1191
|
// entry keys hold no `:` of their own, so the first one found is
|
|
1190
1192
|
// exactly the org id/entry key boundary.
|
|
1191
1193
|
const fullKey = resourceKey.slice(prefix.length);
|
|
1192
1194
|
const entryKey = fullKey.slice(fullKey.indexOf(':') + 1);
|
|
1193
|
-
if (serviceIds[
|
|
1195
|
+
if (serviceIds[entryEdgeRetiredScopeKey(entryKey)] === id)
|
|
1194
1196
|
continue;
|
|
1195
1197
|
}
|
|
1196
1198
|
const [system, kind] = prefix.split(':');
|
|
@@ -1198,7 +1200,7 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
1198
1200
|
}
|
|
1199
1201
|
}
|
|
1200
1202
|
// F7 (#317): a key minted but never delivered never gets the main
|
|
1201
|
-
// `
|
|
1203
|
+
// `edge:key:<org>:<entry>` id recorded (the mint step throws before
|
|
1202
1204
|
// `runSteps` sees a `resourceId`), so the loop above never finds it to
|
|
1203
1205
|
// revoke on destroy. Skipped if a later mint already succeeded and holds
|
|
1204
1206
|
// its own, still-live main id -- entry keys hold no `:`, so matching the
|
|
@@ -1207,19 +1209,19 @@ function destroyTargets(serviceIds, orgId) {
|
|
|
1207
1209
|
// marker A); re-declare, mint B, fail delivery three times (undelivered
|
|
1208
1210
|
// B, main id stays A). Without this check, A's stale-but-truthy main id
|
|
1209
1211
|
// hid B from destroy, leaking it the same way the gone-scan in
|
|
1210
|
-
// `entry-
|
|
1211
|
-
const
|
|
1212
|
+
// `entry-edge-key.ts` once did (#317 re-review).
|
|
1213
|
+
const undeliveredEdgePrefix = 'edge_key_undelivered_';
|
|
1212
1214
|
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1213
|
-
if (!id || !resourceKey.startsWith(
|
|
1215
|
+
if (!id || !resourceKey.startsWith(undeliveredEdgePrefix))
|
|
1214
1216
|
continue;
|
|
1215
|
-
const entryKey = resourceKey.slice(
|
|
1217
|
+
const entryKey = resourceKey.slice(undeliveredEdgePrefix.length);
|
|
1216
1218
|
const hasLaterMainId = Object.entries(serviceIds).some(([k, v]) => v &&
|
|
1217
|
-
k.startsWith('
|
|
1219
|
+
k.startsWith('edge:key:') &&
|
|
1218
1220
|
k.endsWith(`:${entryKey}`) &&
|
|
1219
|
-
serviceIds[
|
|
1221
|
+
serviceIds[entryEdgeRetiredScopeKey(entryKey)] !== v);
|
|
1220
1222
|
if (hasLaterMainId)
|
|
1221
1223
|
continue;
|
|
1222
|
-
targets.push({ system: '
|
|
1224
|
+
targets.push({ system: 'edge', kind: 'key', key: entryKey, id });
|
|
1223
1225
|
}
|
|
1224
1226
|
for (const [resourceKey, id] of Object.entries(serviceIds)) {
|
|
1225
1227
|
if (!resourceKey.startsWith('zitadel:app:'))
|
|
@@ -1302,7 +1304,7 @@ const STEP_FIELDS = {
|
|
|
1302
1304
|
'cloudflare:zone': 'hosting.usesDomain',
|
|
1303
1305
|
'cloudflare:access': 'hosting.accessEmails',
|
|
1304
1306
|
'cloudflare:ratelimit': 'hosting.usesDomain',
|
|
1305
|
-
'cloudflare:worker': 'hosting.entries.
|
|
1307
|
+
'cloudflare:worker': 'hosting.entries.worker',
|
|
1306
1308
|
};
|
|
1307
1309
|
function stepField(action) {
|
|
1308
1310
|
// An entry's ai key (F6a) is keyed `<orgId>:<entry>`; the org's own is `<orgId>`.
|
|
@@ -1312,9 +1314,9 @@ function stepField(action) {
|
|
|
1312
1314
|
// The org's own files key (F6b) is keyed `<orgId>`, an entry's `<orgId>:<entry>`.
|
|
1313
1315
|
if (action.system === 'files' && action.kind === 'key')
|
|
1314
1316
|
return 'hosting.entries.uses.files';
|
|
1315
|
-
// An entry's
|
|
1316
|
-
if (action.system === '
|
|
1317
|
-
return 'hosting.entries.uses.
|
|
1317
|
+
// An entry's edge key (F7): always `<orgId>:<entry>`, edge has no org-root key.
|
|
1318
|
+
if (action.system === 'edge' && action.kind === 'key')
|
|
1319
|
+
return 'hosting.entries.uses.edge';
|
|
1318
1320
|
// An org declares several issuer apps, each keyed by its name (#71).
|
|
1319
1321
|
if (action.system === 'zitadel' && action.kind === 'app') {
|
|
1320
1322
|
return `hosting.issuerApps.${action.key}`;
|
|
@@ -1363,15 +1365,15 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
1363
1365
|
}
|
|
1364
1366
|
}
|
|
1365
1367
|
}
|
|
1366
|
-
// The same for an entry's
|
|
1368
|
+
// The same for an entry's edge key (F7): minted, never delivered is no
|
|
1367
1369
|
// longer blocked forever on its own (the next apply revokes it and
|
|
1368
1370
|
// mints again, #317 re-review) -- but while that retry step is itself
|
|
1369
|
-
// not planned this run (no
|
|
1371
|
+
// not planned this run (no edge service wired, say), the leftover
|
|
1370
1372
|
// undelivered id must still read as unresolved rather than the org
|
|
1371
1373
|
// converging silently around it.
|
|
1372
1374
|
for (const entry of org.hosting.entries) {
|
|
1373
|
-
if (entry.uses?.
|
|
1374
|
-
failed.add('hosting.entries.uses.
|
|
1375
|
+
if (entry.uses?.edge && serviceIds[entryEdgeUndeliveredScopeKey(entry.key)]) {
|
|
1376
|
+
failed.add('hosting.entries.uses.edge');
|
|
1375
1377
|
}
|
|
1376
1378
|
}
|
|
1377
1379
|
// The same for a jobs registration whose credentials never reached the host.
|
|
@@ -622,32 +622,32 @@ export declare const PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY = "files_ma
|
|
|
622
622
|
export declare const PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY = "files_applied_hostname";
|
|
623
623
|
export declare const PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY = "files_applied_git_ref";
|
|
624
624
|
/**
|
|
625
|
-
* #478: the resource key `
|
|
625
|
+
* #478: the resource key `planEdge` plans under and
|
|
626
626
|
* `FoundryPlatformStepExecutor` dispatches on -- the {@link PLATFORM_FILES_KEY}
|
|
627
|
-
* shape for the
|
|
628
|
-
* records the app's uuid under `coolify:app:platform:
|
|
629
|
-
* succeeds, which is what stops `
|
|
627
|
+
* shape for the edge host (https://github.com/wtfalch/edge). `runPlatformSteps`
|
|
628
|
+
* records the app's uuid under `coolify:app:platform:edge` once the step
|
|
629
|
+
* succeeds, which is what stops `planEdge` planning it again.
|
|
630
630
|
*/
|
|
631
|
-
export declare const
|
|
631
|
+
export declare const PLATFORM_EDGE_KEY = "platform:edge";
|
|
632
632
|
/**
|
|
633
|
-
* #478: the held-key ref the
|
|
633
|
+
* #478: the held-key ref the edge platform secret's full value is
|
|
634
634
|
* delivered under, once, by {@link FoundryStepExecutorPorts.deliverSecret} --
|
|
635
635
|
* never the host's app env, which gets only its SHA-256 hash
|
|
636
|
-
* (`
|
|
637
|
-
* secret back from this ref to call
|
|
636
|
+
* (`EDGE_PLATFORM_KEY_HASH`). Archon's own `EdgePort` reads the full
|
|
637
|
+
* secret back from this ref to call edge' `mintKey`/`revokeKey`/
|
|
638
638
|
* `setReservedLabels`.
|
|
639
639
|
*/
|
|
640
|
-
export declare const
|
|
640
|
+
export declare const PLATFORM_EDGE_KEY_REF = "edge:platform:key";
|
|
641
641
|
/**
|
|
642
|
-
* #478: the held-key ref for
|
|
642
|
+
* #478: the held-key ref for edge' own Cloudflare API token (tunnel edit +
|
|
643
643
|
* DNS edit on one account) -- the issue's own "never Archon's token": this
|
|
644
|
-
* is a narrower,
|
|
645
|
-
* separately from whatever broader token `
|
|
644
|
+
* is a narrower, edge-only credential a human provisions and holds
|
|
645
|
+
* separately from whatever broader token `PlatformEdgeDeps.cloudflare`
|
|
646
646
|
* uses to stand the host itself up on Coolify.
|
|
647
647
|
*/
|
|
648
|
-
export declare const
|
|
648
|
+
export declare const PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF = "cloudflare:edge:apiToken";
|
|
649
649
|
/**
|
|
650
|
-
* #478: scope keys (colon-free) for the
|
|
650
|
+
* #478: scope keys (colon-free) for the edge platform key, the
|
|
651
651
|
* {@link PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY} pattern once:
|
|
652
652
|
* the undelivered id is recorded and persisted the moment the secret is
|
|
653
653
|
* minted, before delivery; the guard is recorded only once it was delivered
|
|
@@ -655,16 +655,16 @@ export declare const PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF = "cloudflare:names:api
|
|
|
655
655
|
* means the secret is lost (never kept past that point): the planner
|
|
656
656
|
* reports it and never mints again. Neither key holds the secret itself.
|
|
657
657
|
*/
|
|
658
|
-
export declare const
|
|
659
|
-
export declare const
|
|
658
|
+
export declare const PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY = "edge_platform_key_undelivered";
|
|
659
|
+
export declare const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = "edge_platform_key_set";
|
|
660
660
|
/**
|
|
661
661
|
* #478: the hostname and `gitRef` the last successful names run applied.
|
|
662
|
-
* `
|
|
662
|
+
* `planEdge` plans the step again when either differs from the declared
|
|
663
663
|
* value; a re-run updates `git_branch` and the runner env and redeploys,
|
|
664
664
|
* and mints no new platform key (the guards above hold).
|
|
665
665
|
*/
|
|
666
|
-
export declare const
|
|
667
|
-
export declare const
|
|
666
|
+
export declare const PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = "edge_applied_hostname";
|
|
667
|
+
export declare const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = "edge_applied_git_ref";
|
|
668
668
|
/**
|
|
669
669
|
* Options for {@link planPlatform}, replacing four positional arguments that
|
|
670
670
|
* shifted on every merge that added a platform service (#186, #187, #188) --
|
|
@@ -703,8 +703,8 @@ export interface PlanPlatformOptions {
|
|
|
703
703
|
ai?: string;
|
|
704
704
|
archon?: string;
|
|
705
705
|
mail?: string;
|
|
706
|
-
/** #478: why the
|
|
707
|
-
|
|
706
|
+
/** #478: why the edge host is blocked, or absent when it is not. */
|
|
707
|
+
edge?: string;
|
|
708
708
|
/**
|
|
709
709
|
* #257: true once a service's database exists but the host built the
|
|
710
710
|
* platform executor with no backup dependencies -- every such service's
|
|
@@ -756,7 +756,7 @@ export interface PlanPlatformOptions {
|
|
|
756
756
|
* Scope for #167: the frame plus one step, {@link planApexZone}. #173 adds
|
|
757
757
|
* {@link planJobs}, #170 adds {@link planSfu}, #168 adds {@link planAuth},
|
|
758
758
|
* #169 adds {@link planMail}, #174 adds {@link planIntegrations}, #171 adds
|
|
759
|
-
* {@link planFiles}, #172 adds {@link planAi}, #478 adds {@link
|
|
759
|
+
* {@link planFiles}, #172 adds {@link planAi}, #478 adds {@link planEdge}
|
|
760
760
|
* and #175 adds
|
|
761
761
|
* {@link planArchon}, the same way #19 shipped org plan/apply against
|
|
762
762
|
* three singletons before #27, #71, #80 and the rest added the remaining
|
|
@@ -698,32 +698,32 @@ export const PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY = 'files_management
|
|
|
698
698
|
export const PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY = 'files_applied_hostname';
|
|
699
699
|
export const PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY = 'files_applied_git_ref';
|
|
700
700
|
/**
|
|
701
|
-
* #478: the resource key `
|
|
701
|
+
* #478: the resource key `planEdge` plans under and
|
|
702
702
|
* `FoundryPlatformStepExecutor` dispatches on -- the {@link PLATFORM_FILES_KEY}
|
|
703
|
-
* shape for the
|
|
704
|
-
* records the app's uuid under `coolify:app:platform:
|
|
705
|
-
* succeeds, which is what stops `
|
|
703
|
+
* shape for the edge host (https://github.com/wtfalch/edge). `runPlatformSteps`
|
|
704
|
+
* records the app's uuid under `coolify:app:platform:edge` once the step
|
|
705
|
+
* succeeds, which is what stops `planEdge` planning it again.
|
|
706
706
|
*/
|
|
707
|
-
export const
|
|
707
|
+
export const PLATFORM_EDGE_KEY = 'platform:edge';
|
|
708
708
|
/**
|
|
709
|
-
* #478: the held-key ref the
|
|
709
|
+
* #478: the held-key ref the edge platform secret's full value is
|
|
710
710
|
* delivered under, once, by {@link FoundryStepExecutorPorts.deliverSecret} --
|
|
711
711
|
* never the host's app env, which gets only its SHA-256 hash
|
|
712
|
-
* (`
|
|
713
|
-
* secret back from this ref to call
|
|
712
|
+
* (`EDGE_PLATFORM_KEY_HASH`). Archon's own `EdgePort` reads the full
|
|
713
|
+
* secret back from this ref to call edge' `mintKey`/`revokeKey`/
|
|
714
714
|
* `setReservedLabels`.
|
|
715
715
|
*/
|
|
716
|
-
export const
|
|
716
|
+
export const PLATFORM_EDGE_KEY_REF = 'edge:platform:key';
|
|
717
717
|
/**
|
|
718
|
-
* #478: the held-key ref for
|
|
718
|
+
* #478: the held-key ref for edge' own Cloudflare API token (tunnel edit +
|
|
719
719
|
* DNS edit on one account) -- the issue's own "never Archon's token": this
|
|
720
|
-
* is a narrower,
|
|
721
|
-
* separately from whatever broader token `
|
|
720
|
+
* is a narrower, edge-only credential a human provisions and holds
|
|
721
|
+
* separately from whatever broader token `PlatformEdgeDeps.cloudflare`
|
|
722
722
|
* uses to stand the host itself up on Coolify.
|
|
723
723
|
*/
|
|
724
|
-
export const
|
|
724
|
+
export const PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF = 'cloudflare:edge:apiToken';
|
|
725
725
|
/**
|
|
726
|
-
* #478: scope keys (colon-free) for the
|
|
726
|
+
* #478: scope keys (colon-free) for the edge platform key, the
|
|
727
727
|
* {@link PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY} pattern once:
|
|
728
728
|
* the undelivered id is recorded and persisted the moment the secret is
|
|
729
729
|
* minted, before delivery; the guard is recorded only once it was delivered
|
|
@@ -731,16 +731,16 @@ export const PLATFORM_NAMES_CLOUDFLARE_TOKEN_REF = 'cloudflare:names:apiToken';
|
|
|
731
731
|
* means the secret is lost (never kept past that point): the planner
|
|
732
732
|
* reports it and never mints again. Neither key holds the secret itself.
|
|
733
733
|
*/
|
|
734
|
-
export const
|
|
735
|
-
export const
|
|
734
|
+
export const PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY = 'edge_platform_key_undelivered';
|
|
735
|
+
export const PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY = 'edge_platform_key_set';
|
|
736
736
|
/**
|
|
737
737
|
* #478: the hostname and `gitRef` the last successful names run applied.
|
|
738
|
-
* `
|
|
738
|
+
* `planEdge` plans the step again when either differs from the declared
|
|
739
739
|
* value; a re-run updates `git_branch` and the runner env and redeploys,
|
|
740
740
|
* and mints no new platform key (the guards above hold).
|
|
741
741
|
*/
|
|
742
|
-
export const
|
|
743
|
-
export const
|
|
742
|
+
export const PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY = 'edge_applied_hostname';
|
|
743
|
+
export const PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY = 'edge_applied_git_ref';
|
|
744
744
|
/**
|
|
745
745
|
* #167: the platform's own diff engine, mirroring `diff.ts`'s
|
|
746
746
|
* `computePlanDetail` for the one shared {@link DeclaredPlatform} record
|
|
@@ -755,7 +755,7 @@ export const PLATFORM_NAMES_APPLIED_GIT_REF_SCOPE_KEY = 'names_applied_git_ref';
|
|
|
755
755
|
* Scope for #167: the frame plus one step, {@link planApexZone}. #173 adds
|
|
756
756
|
* {@link planJobs}, #170 adds {@link planSfu}, #168 adds {@link planAuth},
|
|
757
757
|
* #169 adds {@link planMail}, #174 adds {@link planIntegrations}, #171 adds
|
|
758
|
-
* {@link planFiles}, #172 adds {@link planAi}, #478 adds {@link
|
|
758
|
+
* {@link planFiles}, #172 adds {@link planAi}, #478 adds {@link planEdge}
|
|
759
759
|
* and #175 adds
|
|
760
760
|
* {@link planArchon}, the same way #19 shipped org plan/apply against
|
|
761
761
|
* three singletons before #27, #71, #80 and the rest added the remaining
|
|
@@ -805,10 +805,10 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
805
805
|
const ai = planAi(platform, recordedIds, blocked.ai);
|
|
806
806
|
if (ai.step)
|
|
807
807
|
plannedSteps.push(ai.step);
|
|
808
|
-
// #478: independent of auth and files -- the
|
|
809
|
-
const
|
|
810
|
-
if (
|
|
811
|
-
plannedSteps.push(
|
|
808
|
+
// #478: independent of auth and files -- the edge host needs neither.
|
|
809
|
+
const edge = planEdge(platform, recordedIds, blocked.edge);
|
|
810
|
+
if (edge.step)
|
|
811
|
+
plannedSteps.push(edge.step);
|
|
812
812
|
// #175: last, on purpose. Archon's sign-in client lives in the sign-in
|
|
813
813
|
// `planAuth` builds, and its env reads what every other service's step
|
|
814
814
|
// records or delivers, so in one apply it runs after all of them.
|
|
@@ -842,9 +842,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
842
842
|
integrations.blocked
|
|
843
843
|
? null
|
|
844
844
|
: versionChangeLine('integrations', recordedIds[PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY], platform.integrations.gitRef, 'git ref, redeploy'),
|
|
845
|
-
|
|
845
|
+
edge.blocked
|
|
846
846
|
? null
|
|
847
|
-
: versionChangeLine('
|
|
847
|
+
: versionChangeLine('edge', recordedIds[PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY], platform.edge.gitRef, 'git ref, redeploy'),
|
|
848
848
|
auth.blocked
|
|
849
849
|
? null
|
|
850
850
|
: versionNotAppliedLine('zitadel', Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_AUTH_KEY)]), recordedIds[PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.zitadel),
|
|
@@ -876,8 +876,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
876
876
|
unmanaged.push({ description: files.blocked, key: 'files' });
|
|
877
877
|
if (ai.blocked)
|
|
878
878
|
unmanaged.push({ description: ai.blocked, key: 'ai' });
|
|
879
|
-
if (
|
|
880
|
-
unmanaged.push({ description:
|
|
879
|
+
if (edge.blocked)
|
|
880
|
+
unmanaged.push({ description: edge.blocked, key: 'edge' });
|
|
881
881
|
if (archon.blocked)
|
|
882
882
|
unmanaged.push({ description: archon.blocked, key: 'archon' });
|
|
883
883
|
// #398: the old integrations key stays pinned until Archon holds the new
|
|
@@ -1747,13 +1747,13 @@ function planFiles(platform, recordedIds, executorBlocked, authReady, drifted) {
|
|
|
1747
1747
|
};
|
|
1748
1748
|
}
|
|
1749
1749
|
/**
|
|
1750
|
-
* #478: the
|
|
1750
|
+
* #478: the edge host (https://github.com/wtfalch/edge) -- its database
|
|
1751
1751
|
* (owner and runtime roles), its one Coolify app (no separate migrate app:
|
|
1752
1752
|
* the Dockerfile migrates inside the runner's own entrypoint, see
|
|
1753
|
-
* `
|
|
1753
|
+
* `provisionPlatformEdge`'s own doc comment), foundry's own platform key
|
|
1754
1754
|
* (minted once, its hash alone written to the runner) and a Cloudflare
|
|
1755
|
-
* tunnel + DNS record at `platform.
|
|
1756
|
-
* keyed {@link
|
|
1755
|
+
* tunnel + DNS record at `platform.edge.hostname`. One `coolify:app` step
|
|
1756
|
+
* keyed {@link PLATFORM_EDGE_KEY}, the {@link planFiles} bundling shape,
|
|
1757
1757
|
* public like {@link planMail}/{@link planAuth} rather than internal-only
|
|
1758
1758
|
* like {@link planFiles}/{@link planAi}.
|
|
1759
1759
|
*
|
|
@@ -1764,23 +1764,23 @@ function planFiles(platform, recordedIds, executorBlocked, authReady, drifted) {
|
|
|
1764
1764
|
*
|
|
1765
1765
|
* Blocked, not planned: a platform key minted but never delivered (lost,
|
|
1766
1766
|
* `ServiceError`-free the way {@link planFiles}'s own management-key check
|
|
1767
|
-
* is), or `executorBlocked` -- the host built no
|
|
1767
|
+
* is), or `executorBlocked` -- the host built no edge dependencies, or
|
|
1768
1768
|
* (CLAUDE.md's "Human steps") no Cloudflare API token is held yet under
|
|
1769
|
-
* {@link
|
|
1769
|
+
* {@link PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF}.
|
|
1770
1770
|
*/
|
|
1771
|
-
function
|
|
1772
|
-
if (!platform.
|
|
1771
|
+
function planEdge(platform, recordedIds, executorBlocked) {
|
|
1772
|
+
if (!platform.edge.enabled)
|
|
1773
1773
|
return { step: null, blocked: null };
|
|
1774
|
-
const undelivered = recordedIds[
|
|
1775
|
-
if (undelivered && !recordedIds[
|
|
1774
|
+
const undelivered = recordedIds[PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY];
|
|
1775
|
+
if (undelivered && !recordedIds[PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY]) {
|
|
1776
1776
|
return {
|
|
1777
1777
|
step: null,
|
|
1778
|
-
blocked: `the
|
|
1778
|
+
blocked: `the edge host is blocked, not planned: its platform key (${undelivered}) was minted but never delivered and cannot be shown again; remove the recorded key ${PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY} to mint a new one`,
|
|
1779
1779
|
};
|
|
1780
1780
|
}
|
|
1781
|
-
const applied = Boolean(recordedIds[resourceKey('coolify', 'app',
|
|
1782
|
-
recordedIds[
|
|
1783
|
-
recordedIds[
|
|
1781
|
+
const applied = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_EDGE_KEY)]) &&
|
|
1782
|
+
recordedIds[PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY] === platform.edge.hostname &&
|
|
1783
|
+
recordedIds[PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY] === platform.edge.gitRef;
|
|
1784
1784
|
if (applied)
|
|
1785
1785
|
return { step: null, blocked: null };
|
|
1786
1786
|
if (executorBlocked)
|
|
@@ -1790,11 +1790,11 @@ function planNames(platform, recordedIds, executorBlocked) {
|
|
|
1790
1790
|
action: {
|
|
1791
1791
|
system: 'coolify',
|
|
1792
1792
|
kind: 'app',
|
|
1793
|
-
key:
|
|
1794
|
-
desired: { hostname: platform.
|
|
1793
|
+
key: PLATFORM_EDGE_KEY,
|
|
1794
|
+
desired: { hostname: platform.edge.hostname, gitRef: platform.edge.gitRef },
|
|
1795
1795
|
},
|
|
1796
1796
|
step: {
|
|
1797
|
-
description: `provision the
|
|
1797
|
+
description: `provision the edge host at ${platform.edge.hostname}, with its own database and foundry's platform key`,
|
|
1798
1798
|
destroys: false,
|
|
1799
1799
|
migrates: false,
|
|
1800
1800
|
},
|
|
@@ -88,16 +88,16 @@ export interface OrgPlatformReadiness {
|
|
|
88
88
|
/** The jobs scheduler (#173) is enabled, applied, and its admin token is held. */
|
|
89
89
|
jobsReady: boolean;
|
|
90
90
|
/**
|
|
91
|
-
* #317 (F7) and #478: the
|
|
92
|
-
* `coolify:app` deploy step has applied (`
|
|
93
|
-
* the host's executor was built with
|
|
91
|
+
* #317 (F7) and #478: the edge service is declared `enabled`, its own
|
|
92
|
+
* `coolify:app` deploy step has applied (`provisionPlatformEdge`), and
|
|
93
|
+
* the host's executor was built with an `EdgePort` -- the same
|
|
94
94
|
* `filesReady`/`aiReady` "declared and applied" shape now that #478 built
|
|
95
95
|
* the deploy step #317 shipped ahead of (this field's own doc comment
|
|
96
96
|
* used to say there was none; N1 is no longer deployed by hand).
|
|
97
97
|
*/
|
|
98
|
-
|
|
99
|
-
/** `https://<platform.
|
|
100
|
-
|
|
98
|
+
edgeReady: boolean;
|
|
99
|
+
/** `https://<platform.edge.hostname>`, which an entry with `uses.edge` gets as `EDGE_URL`. `null` until `edgeReady`. */
|
|
100
|
+
edgeUrl?: string | null;
|
|
101
101
|
/**
|
|
102
102
|
* The ref of a platform key an applied service needs that is not held yet,
|
|
103
103
|
* `null` when it is held or when the service is not applied. A ref only,
|
|
@@ -166,8 +166,8 @@ export declare function orgPlatformReadinessFrom(platform: DeclaredPlatform, pla
|
|
|
166
166
|
heldRefs?: ReadonlySet<string>,
|
|
167
167
|
/** True when the host wired no held-keys reader ({@link OrgPlatformReadiness.noKeyReader}). */
|
|
168
168
|
noKeyReader?: boolean,
|
|
169
|
-
/** #317: true when the host's executor was built with
|
|
170
|
-
|
|
169
|
+
/** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
|
|
170
|
+
edgePortPresent?: boolean): OrgPlatformReadiness;
|
|
171
171
|
/**
|
|
172
172
|
* #177: {@link AgoraHostValues} derived from the same platform state, rather
|
|
173
173
|
* than requiring a bespoke host-supplied `AgoraHostPort` for values the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JOBS_SCHEDULER_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY,
|
|
1
|
+
import { JOBS_SCHEDULER_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AUTH_KEY, PLATFORM_EDGE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
|
|
2
2
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './platform-providers.js';
|
|
3
3
|
/**
|
|
4
4
|
* #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
|
|
@@ -32,7 +32,7 @@ export const DEFAULT_ORG_PLATFORM_READINESS = {
|
|
|
32
32
|
callsReady: false,
|
|
33
33
|
authReady: false,
|
|
34
34
|
jobsReady: false,
|
|
35
|
-
|
|
35
|
+
edgeReady: false,
|
|
36
36
|
missingKeys: { files: null, ai: null, jobs: null },
|
|
37
37
|
};
|
|
38
38
|
/** The held-key refs an org step needs from the platform, one per service. */
|
|
@@ -80,13 +80,13 @@ export function orgPlatformReadinessFrom(platform, platformRecordedIds,
|
|
|
80
80
|
heldRefs,
|
|
81
81
|
/** True when the host wired no held-keys reader ({@link OrgPlatformReadiness.noKeyReader}). */
|
|
82
82
|
noKeyReader = false,
|
|
83
|
-
/** #317: true when the host's executor was built with
|
|
84
|
-
|
|
83
|
+
/** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
|
|
84
|
+
edgePortPresent = false) {
|
|
85
85
|
const mailApplied = platform.mail.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_MAIL_KEY);
|
|
86
86
|
const filesApplied = platform.files.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_FILES_KEY);
|
|
87
87
|
const aiApplied = platform.ai.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_AI_KEY);
|
|
88
88
|
const jobsApplied = platform.jobs.enabled && Boolean(platformRecordedIds[`coolify:jobs:${JOBS_SCHEDULER_KEY}`]);
|
|
89
|
-
const
|
|
89
|
+
const edgeApplied = platform.edge.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_EDGE_KEY);
|
|
90
90
|
const missing = (applied, ref) => applied && heldRefs !== undefined && !heldRefs.has(ref) ? ref : null;
|
|
91
91
|
const missingKeys = {
|
|
92
92
|
files: missing(filesApplied, ORG_PLATFORM_KEY_REFS.files),
|
|
@@ -113,8 +113,8 @@ namesPortPresent = false) {
|
|
|
113
113
|
callsReady: platform.sfu.enabled,
|
|
114
114
|
authReady: platform.auth.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_AUTH_KEY),
|
|
115
115
|
jobsReady: jobsApplied && missingKeys.jobs === null,
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
edgeReady: edgeApplied && edgePortPresent,
|
|
117
|
+
edgeUrl: edgeApplied && edgePortPresent ? `https://${platform.edge.hostname}` : null,
|
|
118
118
|
missingKeys,
|
|
119
119
|
...(noKeyReader ? { noKeyReader } : {}),
|
|
120
120
|
};
|