@wtfalch/foundry 0.17.0 → 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 +6 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0043_platform_names.sql +25 -0
- package/dist/migrations/0044_platform_mail_lettermint_project.sql +11 -0
- package/dist/migrations/0045_names_to_edge.sql +107 -0
- package/dist/org.d.ts +74 -3
- package/dist/org.js +133 -0
- package/dist/plan/chat-matrix.d.ts +0 -1
- package/dist/plan/chat-matrix.js +4 -1
- package/dist/plan/diff.d.ts +18 -0
- package/dist/plan/diff.js +239 -26
- package/dist/plan/entry-edge-key.d.ts +79 -0
- package/dist/plan/entry-edge-key.js +260 -0
- package/dist/plan/executor.d.ts +246 -1
- package/dist/plan/executor.js +684 -32
- package/dist/plan/index.d.ts +2 -2
- package/dist/plan/index.js +1 -1
- package/dist/plan/live.d.ts +4 -2
- package/dist/plan/login-host.d.ts +12 -1
- package/dist/plan/login-host.js +20 -2
- package/dist/plan/plan-apply.d.ts +15 -0
- package/dist/plan/plan-apply.js +102 -9
- package/dist/plan/platform-diff.d.ts +92 -1
- package/dist/plan/platform-diff.js +166 -1
- package/dist/plan/platform-org.d.ts +29 -1
- package/dist/plan/platform-org.js +17 -2
- package/dist/plan/platform-providers.d.ts +45 -1
- package/dist/plan/platform-providers.js +514 -7
- package/dist/plan/step-system.js +1 -0
- package/dist/plan/worker.d.ts +134 -0
- package/dist/plan/worker.js +239 -0
- package/dist/platform.d.ts +22 -3
- package/dist/platform.js +6 -0
- package/dist/store/postgres.js +15 -1
- package/dist/store/schema.d.ts +70 -2
- package/dist/store/schema.js +6 -0
- package/package.json +1 -1
|
@@ -139,6 +139,23 @@ export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref
|
|
|
139
139
|
* unrecorded and the old key stays pinned.
|
|
140
140
|
*/
|
|
141
141
|
export const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = 'archon_integrations_key_synced';
|
|
142
|
+
/**
|
|
143
|
+
* #467: sha256 of the exact env set the *last confirmed* Archon deploy ran
|
|
144
|
+
* with, recorded only once that deploy's `waitForHealthyVersion` passed --
|
|
145
|
+
* mirroring {@link PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY}'s own
|
|
146
|
+
* confirm-after-health-check timing, generalized from one env to all of
|
|
147
|
+
* them. `provisionPlatformArchon` clears this to `''` the moment it decides
|
|
148
|
+
* a redeploy is needed, before attempting it: a deploy that then fails (or
|
|
149
|
+
* a crash before the health check passes) must leave the attempt
|
|
150
|
+
* unconfirmed, and the env hash alone cannot tell a failed retry's intended
|
|
151
|
+
* content apart from a successful one's, since both intend the same bytes.
|
|
152
|
+
* Without this, a run that writes new envs and then fails before Archon
|
|
153
|
+
* redeploys -- `envsDiffer` comparing desired envs against the *live*
|
|
154
|
+
* store, which this run's own write already updated -- leaves a retry
|
|
155
|
+
* reading no difference at all, and the container stays on the stale
|
|
156
|
+
* values until someone redeploys by hand (#467).
|
|
157
|
+
*/
|
|
158
|
+
export const PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY = 'archon_envs_deployed_hash';
|
|
142
159
|
/**
|
|
143
160
|
* #211 round 4: the broker app id Archon last registered its entry with.
|
|
144
161
|
* `provisionPlatformArchon` reads the broker to register with from
|
|
@@ -157,6 +174,21 @@ export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_a
|
|
|
157
174
|
* has recorded it.
|
|
158
175
|
*/
|
|
159
176
|
export const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = 'archon_applied_bootstrap_operator';
|
|
177
|
+
/**
|
|
178
|
+
* #475: whether the last successful Archon apply patched the app with
|
|
179
|
+
* `custom_network_aliases` and `is_consistent_container_name_enabled: false`
|
|
180
|
+
* instead of the old fixed `custom_internal_name` alone. Coolify only runs a
|
|
181
|
+
* rolling, health-checked deploy -- start the new container, wait, then stop
|
|
182
|
+
* the old one -- when the consistent-name flag is off; with it on (set by
|
|
183
|
+
* foundry's own pre-#475 applies, or by hand in the Coolify UI) every deploy
|
|
184
|
+
* stops the old container before the new one exists, which is what took
|
|
185
|
+
* Archon down for #475's incident. Absent on every platform applied before
|
|
186
|
+
* this fix landed; `planArchon` plans the step again until
|
|
187
|
+
* `provisionPlatformArchon` records it, which only happens once that run's
|
|
188
|
+
* redeploy has passed its health check -- the same confirm-after-health-check
|
|
189
|
+
* timing {@link PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY} uses.
|
|
190
|
+
*/
|
|
191
|
+
export const PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY = 'archon_applied_rolling_update';
|
|
160
192
|
/**
|
|
161
193
|
* The owner's ZITADEL user id Archon takes as its first operator, from
|
|
162
194
|
* {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
|
|
@@ -454,6 +486,20 @@ export function appEnvAppliedScopeKey(entryKey) {
|
|
|
454
486
|
export function dbEnvAppliedScopeKey(entryKey) {
|
|
455
487
|
return `db_env_applied.${entryKey}`;
|
|
456
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* #456: the same marker as {@link dbEnvAppliedScopeKey}, for the chat and
|
|
491
|
+
* calls-service sibling platform services' own owner/runtime database split
|
|
492
|
+
* -- in its own `service_db_env_applied.` namespace, never
|
|
493
|
+
* `dbEnvAppliedScopeKey`'s `db_env_applied.`. Those two services are not
|
|
494
|
+
* hosting entries, but `dbEnvAppliedScopeKey`'s `entryKey` is an arbitrary
|
|
495
|
+
* caller-chosen string with no reserved words (`validateHostingEntry` only
|
|
496
|
+
* requires a DNS label), so a hosting entry a company declares with the key
|
|
497
|
+
* `chat` or `calls-service` would otherwise read or clear the sibling
|
|
498
|
+
* service's own marker, and vice versa.
|
|
499
|
+
*/
|
|
500
|
+
export function serviceDbEnvAppliedScopeKey(service) {
|
|
501
|
+
return `service_db_env_applied.${service}`;
|
|
502
|
+
}
|
|
457
503
|
export function appSettingsOf(entry) {
|
|
458
504
|
return { folder: entry.folder, port: entry.port, autoDeploy: entry.autoDeploy };
|
|
459
505
|
}
|
|
@@ -651,6 +697,50 @@ export const PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY = 'files_management
|
|
|
651
697
|
*/
|
|
652
698
|
export const PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY = 'files_applied_hostname';
|
|
653
699
|
export const PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY = 'files_applied_git_ref';
|
|
700
|
+
/**
|
|
701
|
+
* #478: the resource key `planEdge` plans under and
|
|
702
|
+
* `FoundryPlatformStepExecutor` dispatches on -- the {@link PLATFORM_FILES_KEY}
|
|
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
|
+
*/
|
|
707
|
+
export const PLATFORM_EDGE_KEY = 'platform:edge';
|
|
708
|
+
/**
|
|
709
|
+
* #478: the held-key ref the edge platform secret's full value is
|
|
710
|
+
* delivered under, once, by {@link FoundryStepExecutorPorts.deliverSecret} --
|
|
711
|
+
* never the host's app env, which gets only its SHA-256 hash
|
|
712
|
+
* (`EDGE_PLATFORM_KEY_HASH`). Archon's own `EdgePort` reads the full
|
|
713
|
+
* secret back from this ref to call edge' `mintKey`/`revokeKey`/
|
|
714
|
+
* `setReservedLabels`.
|
|
715
|
+
*/
|
|
716
|
+
export const PLATFORM_EDGE_KEY_REF = 'edge:platform:key';
|
|
717
|
+
/**
|
|
718
|
+
* #478: the held-key ref for edge' own Cloudflare API token (tunnel edit +
|
|
719
|
+
* DNS edit on one account) -- the issue's own "never Archon's token": this
|
|
720
|
+
* is a narrower, edge-only credential a human provisions and holds
|
|
721
|
+
* separately from whatever broader token `PlatformEdgeDeps.cloudflare`
|
|
722
|
+
* uses to stand the host itself up on Coolify.
|
|
723
|
+
*/
|
|
724
|
+
export const PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF = 'cloudflare:edge:apiToken';
|
|
725
|
+
/**
|
|
726
|
+
* #478: scope keys (colon-free) for the edge platform key, the
|
|
727
|
+
* {@link PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY} pattern once:
|
|
728
|
+
* the undelivered id is recorded and persisted the moment the secret is
|
|
729
|
+
* minted, before delivery; the guard is recorded only once it was delivered
|
|
730
|
+
* AND its hash written to the runner's env. An undelivered id with no guard
|
|
731
|
+
* means the secret is lost (never kept past that point): the planner
|
|
732
|
+
* reports it and never mints again. Neither key holds the secret itself.
|
|
733
|
+
*/
|
|
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
|
+
/**
|
|
737
|
+
* #478: the hostname and `gitRef` the last successful names run applied.
|
|
738
|
+
* `planEdge` plans the step again when either differs from the declared
|
|
739
|
+
* value; a re-run updates `git_branch` and the runner env and redeploys,
|
|
740
|
+
* and mints no new platform key (the guards above hold).
|
|
741
|
+
*/
|
|
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';
|
|
654
744
|
/**
|
|
655
745
|
* #167: the platform's own diff engine, mirroring `diff.ts`'s
|
|
656
746
|
* `computePlanDetail` for the one shared {@link DeclaredPlatform} record
|
|
@@ -665,7 +755,8 @@ export const PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY = 'files_applied_git_ref';
|
|
|
665
755
|
* Scope for #167: the frame plus one step, {@link planApexZone}. #173 adds
|
|
666
756
|
* {@link planJobs}, #170 adds {@link planSfu}, #168 adds {@link planAuth},
|
|
667
757
|
* #169 adds {@link planMail}, #174 adds {@link planIntegrations}, #171 adds
|
|
668
|
-
* {@link planFiles}, #172 adds {@link planAi}
|
|
758
|
+
* {@link planFiles}, #172 adds {@link planAi}, #478 adds {@link planEdge}
|
|
759
|
+
* and #175 adds
|
|
669
760
|
* {@link planArchon}, the same way #19 shipped org plan/apply against
|
|
670
761
|
* three singletons before #27, #71, #80 and the rest added the remaining
|
|
671
762
|
* twelve. Until then, a live resource under one of those eventual keys is
|
|
@@ -714,6 +805,10 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
714
805
|
const ai = planAi(platform, recordedIds, blocked.ai);
|
|
715
806
|
if (ai.step)
|
|
716
807
|
plannedSteps.push(ai.step);
|
|
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);
|
|
717
812
|
// #175: last, on purpose. Archon's sign-in client lives in the sign-in
|
|
718
813
|
// `planAuth` builds, and its env reads what every other service's step
|
|
719
814
|
// records or delivers, so in one apply it runs after all of them.
|
|
@@ -747,6 +842,9 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
747
842
|
integrations.blocked
|
|
748
843
|
? null
|
|
749
844
|
: versionChangeLine('integrations', recordedIds[PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY], platform.integrations.gitRef, 'git ref, redeploy'),
|
|
845
|
+
edge.blocked
|
|
846
|
+
? null
|
|
847
|
+
: versionChangeLine('edge', recordedIds[PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY], platform.edge.gitRef, 'git ref, redeploy'),
|
|
750
848
|
auth.blocked
|
|
751
849
|
? null
|
|
752
850
|
: versionNotAppliedLine('zitadel', Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_AUTH_KEY)]), recordedIds[PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.zitadel),
|
|
@@ -778,6 +876,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
778
876
|
unmanaged.push({ description: files.blocked, key: 'files' });
|
|
779
877
|
if (ai.blocked)
|
|
780
878
|
unmanaged.push({ description: ai.blocked, key: 'ai' });
|
|
879
|
+
if (edge.blocked)
|
|
880
|
+
unmanaged.push({ description: edge.blocked, key: 'edge' });
|
|
781
881
|
if (archon.blocked)
|
|
782
882
|
unmanaged.push({ description: archon.blocked, key: 'archon' });
|
|
783
883
|
// #398: the old integrations key stays pinned until Archon holds the new
|
|
@@ -1646,6 +1746,62 @@ function planFiles(platform, recordedIds, executorBlocked, authReady, drifted) {
|
|
|
1646
1746
|
blocked: null,
|
|
1647
1747
|
};
|
|
1648
1748
|
}
|
|
1749
|
+
/**
|
|
1750
|
+
* #478: the edge host (https://github.com/wtfalch/edge) -- its database
|
|
1751
|
+
* (owner and runtime roles), its one Coolify app (no separate migrate app:
|
|
1752
|
+
* the Dockerfile migrates inside the runner's own entrypoint, see
|
|
1753
|
+
* `provisionPlatformEdge`'s own doc comment), foundry's own platform key
|
|
1754
|
+
* (minted once, its hash alone written to the runner) and a Cloudflare
|
|
1755
|
+
* tunnel + DNS record at `platform.edge.hostname`. One `coolify:app` step
|
|
1756
|
+
* keyed {@link PLATFORM_EDGE_KEY}, the {@link planFiles} bundling shape,
|
|
1757
|
+
* public like {@link planMail}/{@link planAuth} rather than internal-only
|
|
1758
|
+
* like {@link planFiles}/{@link planAi}.
|
|
1759
|
+
*
|
|
1760
|
+
* Idempotent on the recorded app id plus the applied hostname and `gitRef`,
|
|
1761
|
+
* for {@link planAuth}'s own reason: no platform reader reports this
|
|
1762
|
+
* Coolify app. A changed hostname or `gitRef` plans the step again (a
|
|
1763
|
+
* redeploy, no new secret).
|
|
1764
|
+
*
|
|
1765
|
+
* Blocked, not planned: a platform key minted but never delivered (lost,
|
|
1766
|
+
* `ServiceError`-free the way {@link planFiles}'s own management-key check
|
|
1767
|
+
* is), or `executorBlocked` -- the host built no edge dependencies, or
|
|
1768
|
+
* (CLAUDE.md's "Human steps") no Cloudflare API token is held yet under
|
|
1769
|
+
* {@link PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF}.
|
|
1770
|
+
*/
|
|
1771
|
+
function planEdge(platform, recordedIds, executorBlocked) {
|
|
1772
|
+
if (!platform.edge.enabled)
|
|
1773
|
+
return { step: null, blocked: null };
|
|
1774
|
+
const undelivered = recordedIds[PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY];
|
|
1775
|
+
if (undelivered && !recordedIds[PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY]) {
|
|
1776
|
+
return {
|
|
1777
|
+
step: null,
|
|
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
|
+
};
|
|
1780
|
+
}
|
|
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
|
+
if (applied)
|
|
1785
|
+
return { step: null, blocked: null };
|
|
1786
|
+
if (executorBlocked)
|
|
1787
|
+
return { step: null, blocked: executorBlocked };
|
|
1788
|
+
return {
|
|
1789
|
+
step: {
|
|
1790
|
+
action: {
|
|
1791
|
+
system: 'coolify',
|
|
1792
|
+
kind: 'app',
|
|
1793
|
+
key: PLATFORM_EDGE_KEY,
|
|
1794
|
+
desired: { hostname: platform.edge.hostname, gitRef: platform.edge.gitRef },
|
|
1795
|
+
},
|
|
1796
|
+
step: {
|
|
1797
|
+
description: `provision the edge host at ${platform.edge.hostname}, with its own database and foundry's platform key`,
|
|
1798
|
+
destroys: false,
|
|
1799
|
+
migrates: false,
|
|
1800
|
+
},
|
|
1801
|
+
},
|
|
1802
|
+
blocked: null,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1649
1805
|
/**
|
|
1650
1806
|
* #172: the platform's AI host (https://github.com/wtfalch/ai) -- a Coolify
|
|
1651
1807
|
* Postgres plus three apps built from that repo's root Dockerfile, one per
|
|
@@ -1780,6 +1936,15 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted)
|
|
|
1780
1936
|
recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
|
|
1781
1937
|
(recordedIds[PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY] ?? 'none') ===
|
|
1782
1938
|
(archonBootstrapOperator(recordedIds) ?? 'none') &&
|
|
1939
|
+
// #475: a platform applied before the rolling-update fix keeps planning
|
|
1940
|
+
// this step until a confirmed redeploy has cleared it. #480: this marker
|
|
1941
|
+
// alone does not catch the live app's settings changing back afterward
|
|
1942
|
+
// (the consistent-name flag turned on, the alias removed, the health
|
|
1943
|
+
// check disabled) -- `driftedSteps`' `archonRollingUpdateDrifted` catches
|
|
1944
|
+
// that by re-reading the live app, and sets `drifted` below, which plans
|
|
1945
|
+
// the step again the same way an already-`applied` step does for any
|
|
1946
|
+
// other drift reason.
|
|
1947
|
+
recordedIds[PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY] === '1' &&
|
|
1783
1948
|
!recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY] &&
|
|
1784
1949
|
// #398: a rotated integrations key id Archon's env was not yet written
|
|
1785
1950
|
// with (see PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY). Absent
|
|
@@ -46,6 +46,21 @@ export interface OrgPlatformReadiness {
|
|
|
46
46
|
apex: string;
|
|
47
47
|
mailHost: string;
|
|
48
48
|
} | null;
|
|
49
|
+
/**
|
|
50
|
+
* #491 (review round 2): `DeclaredPlatform.apex` itself (not `mailHost.apex`
|
|
51
|
+
* above, which is the fixed domainless-org fallback, {@link
|
|
52
|
+
* NO_DOMAIN_MAIL_APEX}) -- `null` only when no platform is declared at all.
|
|
53
|
+
* Deliberately *not* folded into `mailHost` or gated on mail being enabled
|
|
54
|
+
* or applied: an org's `coolify:app` step needs to recognise the platform's
|
|
55
|
+
* own org (its mail domain equals this) even while the platform's mail
|
|
56
|
+
* server is off or not yet applied -- the first review round gated this on
|
|
57
|
+
* `mailHost` instead, which let `ensureAppLettermintMail` take the
|
|
58
|
+
* unprotected path the moment platform mail went from applied back to off
|
|
59
|
+
* while `MAIL_DOMAIN_SCOPE_KEY` was still recorded as the apex.
|
|
60
|
+
*/
|
|
61
|
+
platformApex: string | null;
|
|
62
|
+
/** `DeclaredPlatform.mail.lettermintProjectId`, `null` when undeclared or when no platform is declared. See {@link platformApex}'s own doc comment for why this is unconditional too. */
|
|
63
|
+
platformLettermintProjectId: string | null;
|
|
49
64
|
/** The files host (#171). Files is always on for every org (#80), so `false` always blocks rather than only mattering when a flag is set. */
|
|
50
65
|
filesReady: boolean;
|
|
51
66
|
/**
|
|
@@ -72,6 +87,17 @@ export interface OrgPlatformReadiness {
|
|
|
72
87
|
authReady: boolean;
|
|
73
88
|
/** The jobs scheduler (#173) is enabled, applied, and its admin token is held. */
|
|
74
89
|
jobsReady: boolean;
|
|
90
|
+
/**
|
|
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
|
+
* `filesReady`/`aiReady` "declared and applied" shape now that #478 built
|
|
95
|
+
* the deploy step #317 shipped ahead of (this field's own doc comment
|
|
96
|
+
* used to say there was none; N1 is no longer deployed by hand).
|
|
97
|
+
*/
|
|
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;
|
|
75
101
|
/**
|
|
76
102
|
* The ref of a platform key an applied service needs that is not held yet,
|
|
77
103
|
* `null` when it is held or when the service is not applied. A ref only,
|
|
@@ -139,7 +165,9 @@ export declare function orgPlatformReadinessFrom(platform: DeclaredPlatform, pla
|
|
|
139
165
|
*/
|
|
140
166
|
heldRefs?: ReadonlySet<string>,
|
|
141
167
|
/** True when the host wired no held-keys reader ({@link OrgPlatformReadiness.noKeyReader}). */
|
|
142
|
-
noKeyReader?: boolean
|
|
168
|
+
noKeyReader?: boolean,
|
|
169
|
+
/** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
|
|
170
|
+
edgePortPresent?: boolean): OrgPlatformReadiness;
|
|
143
171
|
/**
|
|
144
172
|
* #177: {@link AgoraHostValues} derived from the same platform state, rather
|
|
145
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, platformMailMxHost, } from './platform-diff.js';
|
|
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
|
|
@@ -25,11 +25,14 @@ export const NO_DOMAIN_MAIL_APEX = 'oikos.host';
|
|
|
25
25
|
*/
|
|
26
26
|
export const DEFAULT_ORG_PLATFORM_READINESS = {
|
|
27
27
|
mailHost: null,
|
|
28
|
+
platformApex: null,
|
|
29
|
+
platformLettermintProjectId: null,
|
|
28
30
|
filesReady: false,
|
|
29
31
|
aiReady: false,
|
|
30
32
|
callsReady: false,
|
|
31
33
|
authReady: false,
|
|
32
34
|
jobsReady: false,
|
|
35
|
+
edgeReady: false,
|
|
33
36
|
missingKeys: { files: null, ai: null, jobs: null },
|
|
34
37
|
};
|
|
35
38
|
/** The held-key refs an org step needs from the platform, one per service. */
|
|
@@ -76,11 +79,14 @@ export function orgPlatformReadinessFrom(platform, platformRecordedIds,
|
|
|
76
79
|
*/
|
|
77
80
|
heldRefs,
|
|
78
81
|
/** True when the host wired no held-keys reader ({@link OrgPlatformReadiness.noKeyReader}). */
|
|
79
|
-
noKeyReader = false
|
|
82
|
+
noKeyReader = false,
|
|
83
|
+
/** #317: true when the host's executor was built with an `EdgePort` ({@link OrgPlatformReadiness.edgeReady}). */
|
|
84
|
+
edgePortPresent = false) {
|
|
80
85
|
const mailApplied = platform.mail.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_MAIL_KEY);
|
|
81
86
|
const filesApplied = platform.files.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_FILES_KEY);
|
|
82
87
|
const aiApplied = platform.ai.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_AI_KEY);
|
|
83
88
|
const jobsApplied = platform.jobs.enabled && Boolean(platformRecordedIds[`coolify:jobs:${JOBS_SCHEDULER_KEY}`]);
|
|
89
|
+
const edgeApplied = platform.edge.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_EDGE_KEY);
|
|
84
90
|
const missing = (applied, ref) => applied && heldRefs !== undefined && !heldRefs.has(ref) ? ref : null;
|
|
85
91
|
const missingKeys = {
|
|
86
92
|
files: missing(filesApplied, ORG_PLATFORM_KEY_REFS.files),
|
|
@@ -91,6 +97,13 @@ noKeyReader = false) {
|
|
|
91
97
|
mailHost: mailApplied
|
|
92
98
|
? { apex: NO_DOMAIN_MAIL_APEX, mailHost: platformMailMxHost(platform.apex) }
|
|
93
99
|
: null,
|
|
100
|
+
// #491 (review round 2): unconditional -- `platform` here is always a
|
|
101
|
+
// real declared platform (this function's own doc comment: "the platform
|
|
102
|
+
// is either declared ... or this is never called"), so its apex and its
|
|
103
|
+
// mail.lettermintProjectId are facts regardless of whether mail itself is
|
|
104
|
+
// enabled or applied.
|
|
105
|
+
platformApex: platform.apex,
|
|
106
|
+
platformLettermintProjectId: platform.mail.lettermintProjectId ?? null,
|
|
94
107
|
filesReady: filesApplied && missingKeys.files === null,
|
|
95
108
|
filesUrl: filesApplied
|
|
96
109
|
? (platformRecordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY] ?? null)
|
|
@@ -100,6 +113,8 @@ noKeyReader = false) {
|
|
|
100
113
|
callsReady: platform.sfu.enabled,
|
|
101
114
|
authReady: platform.auth.enabled && coolifyAppApplied(platformRecordedIds, PLATFORM_AUTH_KEY),
|
|
102
115
|
jobsReady: jobsApplied && missingKeys.jobs === null,
|
|
116
|
+
edgeReady: edgeApplied && edgePortPresent,
|
|
117
|
+
edgeUrl: edgeApplied && edgePortPresent ? `https://${platform.edge.hostname}` : null,
|
|
103
118
|
missingKeys,
|
|
104
119
|
...(noKeyReader ? { noKeyReader } : {}),
|
|
105
120
|
};
|
|
@@ -131,6 +131,12 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
131
131
|
* `platform.ai.enabled` without it gets the step reported as blocked.
|
|
132
132
|
*/
|
|
133
133
|
ai?: PlatformAiDeps;
|
|
134
|
+
/**
|
|
135
|
+
* #478: dependencies for `coolify:app` keyed {@link PLATFORM_EDGE_KEY} --
|
|
136
|
+
* optional for the same reason as `auth`; a host that declares
|
|
137
|
+
* `platform.edge.enabled` without it gets the step reported as blocked.
|
|
138
|
+
*/
|
|
139
|
+
edge?: PlatformEdgeDeps;
|
|
134
140
|
/**
|
|
135
141
|
* #175: dependencies for `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}
|
|
136
142
|
* -- optional for the same reason as {@link auth}.
|
|
@@ -197,6 +203,9 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
197
203
|
* #172 adds `coolify:app` keyed {@link PLATFORM_AI_KEY}, the platform's AI
|
|
198
204
|
* host ({@link provisionPlatformAi}), once a host passes {@link PlatformAiDeps}.
|
|
199
205
|
*
|
|
206
|
+
* #478 adds `coolify:app` keyed {@link PLATFORM_EDGE_KEY}, the edge host
|
|
207
|
+
* ({@link provisionPlatformEdge}), once a host passes {@link PlatformEdgeDeps}.
|
|
208
|
+
*
|
|
200
209
|
* #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
|
|
201
210
|
* ({@link provisionPlatformArchon}), once a host passes
|
|
202
211
|
* {@link PlatformArchonDeps}.
|
|
@@ -216,6 +225,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
216
225
|
private readonly integrations?;
|
|
217
226
|
private readonly files?;
|
|
218
227
|
private readonly ai?;
|
|
228
|
+
private readonly edge?;
|
|
219
229
|
private readonly archon?;
|
|
220
230
|
private readonly backup?;
|
|
221
231
|
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
|
|
@@ -226,6 +236,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
226
236
|
files?: string;
|
|
227
237
|
integrations?: string;
|
|
228
238
|
ai?: string;
|
|
239
|
+
edge?: string;
|
|
229
240
|
archon?: string;
|
|
230
241
|
mail?: string;
|
|
231
242
|
backupExecutorMissing?: boolean;
|
|
@@ -417,7 +428,7 @@ export interface PlatformArchonDeps {
|
|
|
417
428
|
zitadelForInstance?: (issuerUrl: string) => PlatformArchonDeps['zitadel'];
|
|
418
429
|
/**
|
|
419
430
|
* Who the Cloudflare Access app in front of Archon lets through. Host
|
|
420
|
-
* config, not `DeclaredPlatform`: the platform record
|
|
431
|
+
* config, not `DeclaredPlatform`: the platform record edge services, not
|
|
421
432
|
* people. Empty blocks the step (`blockedSteps`) rather than building an
|
|
422
433
|
* Access app nobody can pass.
|
|
423
434
|
*/
|
|
@@ -811,6 +822,39 @@ export declare const FILES_MANAGEMENT_KEY_ID = "foundry-1";
|
|
|
811
822
|
export declare const FILES_MANAGEMENT_KEY_REF = "files:platform:managementKey";
|
|
812
823
|
/** The ref the files host's introspection client secret is delivered under. */
|
|
813
824
|
export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:introspection";
|
|
825
|
+
export interface PlatformEdgeRuntime {
|
|
826
|
+
fetch?: typeof fetch;
|
|
827
|
+
sleep?: (ms: number) => Promise<void>;
|
|
828
|
+
now?: () => number;
|
|
829
|
+
/** Tries at handing the platform key to the host (default 3). */
|
|
830
|
+
deliverAttempts?: number;
|
|
831
|
+
readyTimeoutMs?: number;
|
|
832
|
+
readyIntervalMs?: number;
|
|
833
|
+
}
|
|
834
|
+
export interface PlatformEdgeDeps {
|
|
835
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
836
|
+
cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
|
|
837
|
+
deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
|
|
838
|
+
/**
|
|
839
|
+
* Reads the edge service's own Cloudflare API token back, held under
|
|
840
|
+
* {@link PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF} -- a human step (the issue's
|
|
841
|
+
* own "Human steps"), never minted here: foundry has no Cloudflare
|
|
842
|
+
* account of its own to carve that token out of. Absent, or with nothing
|
|
843
|
+
* held under that ref yet, is reported blocked by `blockedSteps`
|
|
844
|
+
* ({@link edgeBlockedReason}), never planned to throw.
|
|
845
|
+
*/
|
|
846
|
+
heldKeys?: {
|
|
847
|
+
resolve(ref: string): Promise<string | null>;
|
|
848
|
+
};
|
|
849
|
+
/**
|
|
850
|
+
* The Cloudflare account id the token above is scoped to. Not a secret --
|
|
851
|
+
* a plain config value beside the credential it travels with, the same
|
|
852
|
+
* way {@link PlatformFilesDeps.r2}'s bucket name is.
|
|
853
|
+
*/
|
|
854
|
+
cloudflareAccountId?: string;
|
|
855
|
+
/** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
|
|
856
|
+
runtime?: PlatformEdgeRuntime;
|
|
857
|
+
}
|
|
814
858
|
/**
|
|
815
859
|
* #174: everything {@link provisionPlatformIntegrations} needs beyond
|
|
816
860
|
* {@link PlatformStepExecutionContext} -- a bundle, like
|