@wtfalch/foundry 0.62.0 → 0.64.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/apps/index.d.ts +1 -0
- package/dist/apps/index.js +1 -0
- package/dist/apps/server-entry.d.ts +43 -0
- package/dist/apps/server-entry.js +99 -0
- package/dist/bootstrap/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/build-json.d.ts +25 -0
- package/dist/build-json.js +61 -0
- package/dist/catalogue/copy-en.js +10 -0
- package/dist/catalogue/integrations.js +10 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/customer-grants.d.ts +3 -0
- package/dist/modules/customer-grants.js +28 -0
- package/dist/modules/index.js +49 -1
- package/dist/modules/repository.d.ts +6 -0
- package/dist/modules/repository.js +40 -0
- package/dist/modules/types.d.ts +8 -0
- package/dist/native-build.d.ts +7 -27
- package/dist/native-build.js +7 -27
- package/dist/org.d.ts +19 -1
- package/dist/org.js +45 -3
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +61 -2
- package/dist/plan/blue-green.js +80 -6
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +21 -2
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +7 -0
- package/dist/plan/executor.d.ts +71 -1
- package/dist/plan/executor.js +491 -56
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +23 -1
- package/dist/plan/platform-diff.js +96 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +156 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
|
@@ -11,6 +11,7 @@ import { legacyAppHostname } from './hostnames.js';
|
|
|
11
11
|
import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
|
|
12
12
|
import { MAIL_DATABASE_SCOPE_KEY, mailStoreIsLegacy, mailStoreSwitchChange, mailStoreSwitchDescription, } from './mail-store.js';
|
|
13
13
|
import { planOikosIngress } from './oikos-ingress.js';
|
|
14
|
+
import { PLATFORM_REGISTRY_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_REGISTRY_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_REGISTRY_KEY, PLATFORM_REGISTRY_LOGINS_APPLIED_SCOPE_KEY, PLATFORM_REGISTRY_LOGINS_KEY, PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY, PLATFORM_REGISTRY_PASSWORDS_UNDELIVERED_SCOPE_KEY, registryLoginVersion, } from './platform-registry-keys.js';
|
|
14
15
|
import { callsSfuAppliedFingerprint } from './platform-sfu.js';
|
|
15
16
|
import { postLogoutUpdateOwed, postLogoutUrisFor } from './post-logout-uris.js';
|
|
16
17
|
import { archonInAppHost } from './sign-in-archon.js';
|
|
@@ -811,6 +812,28 @@ export function appliedAppSettings(recorded, entryKey) {
|
|
|
811
812
|
const marker = recorded[appSettingsAppliedScopeKey(entryKey)];
|
|
812
813
|
return marker ? JSON.parse(marker) : DEFAULT_APP_SETTINGS;
|
|
813
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* The image an entry's app last ran, as `repository@digest` (no secret). `planApp` plans the step again, with a
|
|
817
|
+
* redeploy, when the entry's `image` differs from it.
|
|
818
|
+
*/
|
|
819
|
+
export function appImageAppliedScopeKey(entryKey) {
|
|
820
|
+
return `app_image_applied.${entryKey}`;
|
|
821
|
+
}
|
|
822
|
+
/** An entry image as one pull reference. */
|
|
823
|
+
export function entryImageRef(image) {
|
|
824
|
+
return `${image.repository}@${image.digest}`;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* The two image fields Coolify accepts for an entry image. Its tag rule has no `@` or `:`, so the digest goes in
|
|
828
|
+
* the name as `<repository>@sha256` and the tag holds the bare 64 hex digits (Coolify pulls `<name>:<tag>`),
|
|
829
|
+
* the same way `coolifyImageFor` writes a pinned platform image.
|
|
830
|
+
*/
|
|
831
|
+
export function entryImageFields(image) {
|
|
832
|
+
return {
|
|
833
|
+
docker_registry_image_name: `${image.repository}@sha256`,
|
|
834
|
+
docker_registry_image_tag: image.digest.replace(/^sha256:/, ''),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
814
837
|
/**
|
|
815
838
|
* The URLs `coolifyApp` last put in the entry's Coolify `domains`
|
|
816
839
|
* (`tunnelDomainList`), as a JSON array (`[]` for an internal entry). `planApp`
|
|
@@ -1418,6 +1441,12 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1418
1441
|
const hc = planHc(platform, recordedIds, blocked.hc ?? dnsBlocked?.('heartbeat server'));
|
|
1419
1442
|
if (hc.step)
|
|
1420
1443
|
plannedSteps.push(hc.step);
|
|
1444
|
+
// The image registry: edge's DNS and tunnel like hc. The login step follows the app step.
|
|
1445
|
+
const registry = planRegistry(platform, recordedIds, blocked.registry ?? dnsBlocked?.('image registry'), drifted.registryLogins ?? false);
|
|
1446
|
+
if (registry.step)
|
|
1447
|
+
plannedSteps.push(registry.step);
|
|
1448
|
+
if (registry.loginsStep)
|
|
1449
|
+
plannedSteps.push(registry.loginsStep);
|
|
1421
1450
|
// #540: the `authReady` pattern, for hc -- true once hc is either already
|
|
1422
1451
|
// applied or about to run earlier in this very plan, so the heartbeats
|
|
1423
1452
|
// reconcile below can be planned in the *same* round hc first comes up,
|
|
@@ -1494,6 +1523,7 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1494
1523
|
recorded: recordedIds,
|
|
1495
1524
|
servers: archonBlueGreen.servers,
|
|
1496
1525
|
executorBlocked: archonBlueGreen.blocked,
|
|
1526
|
+
...(archonBlueGreen.guards ? { guards: archonBlueGreen.guards } : {}),
|
|
1497
1527
|
})
|
|
1498
1528
|
: { step: null, unmanaged: [] };
|
|
1499
1529
|
if (archonBg.step) {
|
|
@@ -1637,6 +1667,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
1637
1667
|
}
|
|
1638
1668
|
if (hc.blocked)
|
|
1639
1669
|
unmanaged.push({ description: hc.blocked, key: 'hc' });
|
|
1670
|
+
if (registry.blocked)
|
|
1671
|
+
unmanaged.push({ description: registry.blocked, key: 'registry' });
|
|
1640
1672
|
if (archonOrg.blocked)
|
|
1641
1673
|
unmanaged.push({ description: archonOrg.blocked, key: 'archon-org' });
|
|
1642
1674
|
if (archon.blocked)
|
|
@@ -2887,6 +2919,70 @@ function planAccessProvider(platform, recordedIds, executorBlocked, ownerSignIn)
|
|
|
2887
2919
|
* Blocked, not planned: `executorBlocked`, from `dnsBlocked` (edge neither
|
|
2888
2920
|
* applied nor planned) or the host building no `hc` dependencies.
|
|
2889
2921
|
*/
|
|
2922
|
+
/**
|
|
2923
|
+
* The platform's image registry (C5): `coolify:app` keyed {@link PLATFORM_REGISTRY_KEY}, then
|
|
2924
|
+
* `coolify:registry-logins` keyed {@link PLATFORM_REGISTRY_LOGINS_KEY}, which logs every Coolify
|
|
2925
|
+
* server in as `pull`. The logins are planned while the host and passwords they last ran with
|
|
2926
|
+
* differ from the current ones, and again when a server has no login (`drifted`).
|
|
2927
|
+
*/
|
|
2928
|
+
function planRegistry(platform, recordedIds, executorBlocked, loginsDrifted) {
|
|
2929
|
+
const none = { step: null, loginsStep: null, blocked: null };
|
|
2930
|
+
if (!platform.registry.enabled)
|
|
2931
|
+
return none;
|
|
2932
|
+
const { hostname, gitRef } = platform.registry;
|
|
2933
|
+
const undelivered = recordedIds[PLATFORM_REGISTRY_PASSWORDS_UNDELIVERED_SCOPE_KEY];
|
|
2934
|
+
if (undelivered && !recordedIds[PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY]) {
|
|
2935
|
+
return {
|
|
2936
|
+
...none,
|
|
2937
|
+
blocked: `the image registry is blocked, not planned: its passwords (${undelivered}) were made but never confirmed delivered and cannot be shown again; remove the recorded id ${PLATFORM_REGISTRY_PASSWORDS_UNDELIVERED_SCOPE_KEY} to make new ones`,
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
const applied = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_REGISTRY_KEY)]) &&
|
|
2941
|
+
Boolean(recordedIds[PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY]) &&
|
|
2942
|
+
recordedIds[PLATFORM_REGISTRY_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
|
|
2943
|
+
recordedIds[PLATFORM_REGISTRY_APPLIED_GIT_REF_SCOPE_KEY] === gitRef;
|
|
2944
|
+
if (executorBlocked && !applied)
|
|
2945
|
+
return { ...none, blocked: executorBlocked };
|
|
2946
|
+
const step = applied
|
|
2947
|
+
? null
|
|
2948
|
+
: {
|
|
2949
|
+
action: {
|
|
2950
|
+
system: 'coolify',
|
|
2951
|
+
kind: 'app',
|
|
2952
|
+
key: PLATFORM_REGISTRY_KEY,
|
|
2953
|
+
desired: { hostname, gitRef },
|
|
2954
|
+
},
|
|
2955
|
+
step: {
|
|
2956
|
+
description: `provision the image registry at ${hostname}: the build workflow pushes company app images there and the company servers pull them`,
|
|
2957
|
+
destroys: false,
|
|
2958
|
+
migrates: false,
|
|
2959
|
+
},
|
|
2960
|
+
};
|
|
2961
|
+
const loginsCurrent = recordedIds[PLATFORM_REGISTRY_LOGINS_APPLIED_SCOPE_KEY] ===
|
|
2962
|
+
registryLoginVersion(hostname, recordedIds[PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY]) &&
|
|
2963
|
+
Boolean(recordedIds[PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY]);
|
|
2964
|
+
const loginsStep = (loginsCurrent && !loginsDrifted) || executorBlocked
|
|
2965
|
+
? null
|
|
2966
|
+
: {
|
|
2967
|
+
action: {
|
|
2968
|
+
system: 'coolify',
|
|
2969
|
+
kind: 'registry-logins',
|
|
2970
|
+
key: PLATFORM_REGISTRY_LOGINS_KEY,
|
|
2971
|
+
desired: { hostname },
|
|
2972
|
+
},
|
|
2973
|
+
step: {
|
|
2974
|
+
description: `log every Coolify server in to the image registry at ${hostname} so it can pull company app images`,
|
|
2975
|
+
destroys: false,
|
|
2976
|
+
migrates: false,
|
|
2977
|
+
},
|
|
2978
|
+
};
|
|
2979
|
+
const loginsNeeded = !loginsCurrent || loginsDrifted;
|
|
2980
|
+
return {
|
|
2981
|
+
step,
|
|
2982
|
+
loginsStep,
|
|
2983
|
+
blocked: executorBlocked && !step && loginsNeeded ? executorBlocked : null,
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2890
2986
|
function planHc(platform, recordedIds, executorBlocked) {
|
|
2891
2987
|
if (!platform.hc.enabled)
|
|
2892
2988
|
return { step: null, blocked: null };
|
|
@@ -373,6 +373,8 @@ export interface PlatformOwnerInput {
|
|
|
373
373
|
signIn?: OwnerSignIn | null;
|
|
374
374
|
files?: OwnerFiles | null;
|
|
375
375
|
chat?: OwnerChat | null;
|
|
376
|
+
/** The owner company's owners' emails: who passes the Access gate over Archon's stage. */
|
|
377
|
+
emails?: readonly string[];
|
|
376
378
|
}
|
|
377
379
|
/**
|
|
378
380
|
* The owner's sign-in from its declared org and the ids its own steps recorded.
|
|
@@ -12,6 +12,8 @@ import type { TunnelPort } from './edge-tunnels.js';
|
|
|
12
12
|
import type { ZonePort } from './edge-zones.js';
|
|
13
13
|
import type { CloudflarePort, CoolifyPort, FoundryStepExecutorPorts, ZitadelPort } from './executor.js';
|
|
14
14
|
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
15
|
+
import { type StageCopyHostConfig } from './stage-copy-host.js';
|
|
16
|
+
import { type ReleasePreviewMembers, type StageProbePort } from './stage-guards.js';
|
|
15
17
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
16
18
|
import type { LiveResource, PlatformCloudflareReader, PlatformCoolifyReader } from './live.js';
|
|
17
19
|
import { type OikosIngressPorts } from './oikos-ingress.js';
|
|
@@ -19,6 +21,7 @@ import { type FilesKeysMinted, type ProjectSource, type ServiceNetwork, type Ser
|
|
|
19
21
|
import type { PlatformDriftedSteps, PlatformStepExecutionContext, PlatformStepExecutor } from './plan-apply.js';
|
|
20
22
|
import { type BrokerDeployRecord, PLATFORM_ZITADEL_ORG_SCOPE_KEY } from './platform-diff.js';
|
|
21
23
|
import type { PlatformOwnerInput } from './platform-org.js';
|
|
24
|
+
import { type PlatformRegistryDeps } from './platform-registry.js';
|
|
22
25
|
import type { SigninDbBackupPort } from './signin-db-backup.js';
|
|
23
26
|
import { type PlatformZitadelMailBranding } from './signin-mail.js';
|
|
24
27
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
@@ -100,7 +103,7 @@ export declare function platformRequestHeaders(deps: {
|
|
|
100
103
|
*/
|
|
101
104
|
export type PlatformAccessFactory = (edgeUrl: string) => PlatformAccessPort | Promise<PlatformAccessPort>;
|
|
102
105
|
/** The Access calls a platform step makes. */
|
|
103
|
-
export type PlatformAccessPort = Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'ensureOneTimePinProvider' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'
|
|
106
|
+
export type PlatformAccessPort = Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'ensureOneTimePinProvider' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'> & Partial<Pick<AccessPort, 'setGatePeople'>>;
|
|
104
107
|
/**
|
|
105
108
|
* The SMTP call `provisionPlatformAuth` makes on the instance's ZITADEL.
|
|
106
109
|
* `@wtfalch/providers` gains it in 0.7.2; stated here so foundry compiles
|
|
@@ -309,6 +312,8 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
309
312
|
* `platform.hc.enabled` without it gets the step reported as blocked.
|
|
310
313
|
*/
|
|
311
314
|
hc?: PlatformHcDeps;
|
|
315
|
+
/** C5: dependencies for the image registry steps; a host that declares `platform.registry.enabled` without it gets them reported as blocked. */
|
|
316
|
+
registry?: PlatformRegistryDeps;
|
|
312
317
|
/**
|
|
313
318
|
* #540: dependencies for the `heartbeats:reconcile` step -- optional for
|
|
314
319
|
* the same reason as `auth`; `hc` applied with this unset gets the
|
|
@@ -354,6 +359,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
354
359
|
private readonly archon?;
|
|
355
360
|
private readonly backup?;
|
|
356
361
|
private readonly hc?;
|
|
362
|
+
private readonly registry?;
|
|
357
363
|
private readonly heartbeats?;
|
|
358
364
|
private readonly uptime?;
|
|
359
365
|
private readonly accessProvider?;
|
|
@@ -365,14 +371,32 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
365
371
|
private onPlatform;
|
|
366
372
|
/** `deps` with its Coolify port placed on the platform destination, then limited by `profile`. */
|
|
367
373
|
private limitedOnPlatform;
|
|
374
|
+
/**
|
|
375
|
+
* Why the image registry cannot be planned, as fixed text, or `undefined`. The held `pull`
|
|
376
|
+
* password is read only to know it is there; its value goes nowhere.
|
|
377
|
+
*/
|
|
378
|
+
private registryBlockedReason;
|
|
368
379
|
/** Every Coolify server and why platform-archon's blue/green cannot run here, for the planner. */
|
|
369
380
|
archonBlueGreen(): Promise<{
|
|
370
381
|
servers?: BlueGreenServer[];
|
|
371
382
|
blocked?: string;
|
|
372
383
|
}>;
|
|
384
|
+
/**
|
|
385
|
+
* The stage guards that can be made for platform-archon now. The planner puts the names in the applied hash
|
|
386
|
+
* and the step builds the same guards from the same facts, so a guard that becomes possible later plans the
|
|
387
|
+
* structure step once more.
|
|
388
|
+
*/
|
|
389
|
+
archonStageGuardNames(platform: DeclaredPlatform, recorded: Readonly<Record<string, string>>, owner: PlatformOwnerInput): string[];
|
|
373
390
|
/** The release runner's outside calls for platform-archon (`plan/release-host.ts`). The token is only read. */
|
|
374
391
|
releaseHost(platform: DeclaredPlatform, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
392
|
+
/** Archon's stage gate follows the people holding `releases:preview` (orgId `platform`); the platform's owners stay. */
|
|
393
|
+
syncStageGateMembers(_platform: DeclaredPlatform, subject: BlueGreenSubject, context: ReleaseContext): Promise<void>;
|
|
375
394
|
private archonBlueGreenStep;
|
|
395
|
+
/**
|
|
396
|
+
* Archon's stage guards: the probe port, and the Access gate over the stage host, offered only when the
|
|
397
|
+
* same facts {@link archonStageGuardNames} counts are all in hand.
|
|
398
|
+
*/
|
|
399
|
+
private archonStageGuards;
|
|
376
400
|
blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>, owner?: PlatformOwnerInput): Promise<{
|
|
377
401
|
auth?: string;
|
|
378
402
|
authExecutorMissing?: boolean;
|
|
@@ -389,6 +413,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
389
413
|
oikosIngress?: string;
|
|
390
414
|
backupExecutorMissing?: boolean;
|
|
391
415
|
hc?: string;
|
|
416
|
+
registry?: string;
|
|
392
417
|
heartbeatsExecutorMissing?: boolean;
|
|
393
418
|
heartbeatsApiKeyMissing?: boolean;
|
|
394
419
|
uptime?: string;
|
|
@@ -955,14 +980,24 @@ export declare function ownerInviteBlockedReason(ownerCompany: DeclaredOrg | nul
|
|
|
955
980
|
*/
|
|
956
981
|
export interface PlatformArchonDeps {
|
|
957
982
|
coolify: Pick<CoolifyPort, 'findProjectByName' | 'listEnvironments' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'getDeployment' | 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'updateFileStorage'> & {
|
|
983
|
+
deleteDatabase?: CoolifyPort['deleteDatabase'];
|
|
984
|
+
listDatabaseBackups?: CoolifyPort['listDatabaseBackups'];
|
|
958
985
|
listApplicationDeployments?: CoolifyPort['listApplicationDeployments'];
|
|
986
|
+
listDatabaseBackupExecutions?: CoolifyPort['listDatabaseBackupExecutions'];
|
|
959
987
|
};
|
|
988
|
+
/**
|
|
989
|
+
* The stage copy of platform-archon (run by the release tick). Same shape as `FoundryStepExecutorPorts.stageCopy`.
|
|
990
|
+
* Without it a copy request is refused with `no-copy-port`.
|
|
991
|
+
*/
|
|
992
|
+
stageCopy?: StageCopyHostConfig;
|
|
960
993
|
/** F4.dns: every DNS record call, through edge at its recorded address (see {@link PlatformDnsFactory}). */
|
|
961
994
|
dns: PlatformDnsFactory;
|
|
962
995
|
/** F4.tunnels: every tunnel call, through edge at its recorded address (see {@link PlatformTunnelsFactory}). */
|
|
963
996
|
tunnels: PlatformTunnelsFactory;
|
|
964
997
|
/** F4.access: every Access app call, through edge at its recorded address (see {@link PlatformAccessFactory}). */
|
|
965
998
|
access: PlatformAccessFactory;
|
|
999
|
+
/** The people holding `releases:preview` for Archon's own stage (orgId `platform`); see FoundryStepExecutorPorts.releasePreviewMembers. */
|
|
1000
|
+
releasePreviewMembers?: ReleasePreviewMembers;
|
|
966
1001
|
cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
|
|
967
1002
|
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
968
1003
|
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri' | 'updateOidcAppPostLogoutRedirectUris' | 'getOidcAppRedirectUris' | 'updateOidcAppRedirectUris'>>;
|
|
@@ -979,6 +1014,16 @@ export interface PlatformArchonDeps {
|
|
|
979
1014
|
* Access app nobody can pass.
|
|
980
1015
|
*/
|
|
981
1016
|
accessEmails: readonly string[];
|
|
1017
|
+
/**
|
|
1018
|
+
* Archon's stage guards, the same two an entry's stage gets (`plan/stage-guards.ts`): the probe port, and the
|
|
1019
|
+
* Access calls that make the gate over `stage-platform-archon.<apex>`. A missing part leaves that guard
|
|
1020
|
+
* unrecorded ("not run"), so the stage copy stays refused.
|
|
1021
|
+
*/
|
|
1022
|
+
stageGuards?: {
|
|
1023
|
+
probes?: StageProbePort;
|
|
1024
|
+
/** Edge's Access calls at its recorded address, like `access`. */
|
|
1025
|
+
gate?: (edgeUrl: string) => Pick<AccessPort, 'createGate'> | Promise<Pick<AccessPort, 'createGate'>>;
|
|
1026
|
+
};
|
|
982
1027
|
/**
|
|
983
1028
|
* True when the host process runs inside the Coolify app this step
|
|
984
1029
|
* redeploys (Archon's console is `platform:archon`). A deploy that waits
|
|
@@ -5,7 +5,7 @@ import { VERSIONS, coolifyImageFor, floatingTagBlocked, pinnedRef } from '../boo
|
|
|
5
5
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
6
6
|
import { ownerMailboxes, ownsServices, sharedSecretRef, } from '../org.js';
|
|
7
7
|
import { applyArchonBlueGreen } from './archon-blue-green.js';
|
|
8
|
-
import { ARCHON_REPO, archonSubject, healthTokenName, pinKeyName, tunnelOriginFor, } from './blue-green.js';
|
|
8
|
+
import { ARCHON_REPO, archonSubject, healthTokenName, parseGuards, pinKeyName, stageGuardInputs, tunnelOriginFor, } from './blue-green.js';
|
|
9
9
|
import { BOULE_BRIDGE_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_PEER_ROLE, bouleHostnameFor, } from './boule.js';
|
|
10
10
|
import { recordBuiltCommit } from './build-record.js';
|
|
11
11
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
@@ -20,8 +20,11 @@ import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_
|
|
|
20
20
|
import { LIMITS, limited } from './own-limits.js';
|
|
21
21
|
import { postLogoutAppliedValue, postLogoutUrisFor } from './post-logout-uris.js';
|
|
22
22
|
import { createReleaseHost } from './release-host.js';
|
|
23
|
+
import { buildStageCopyHostPorts } from './stage-copy-host.js';
|
|
24
|
+
import { stageGateKey, stageHostname, syncStageGateMembers, } from './stage-guards.js';
|
|
23
25
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
24
26
|
import { BROKER_DB_NAME, BROKER_DB_USER, ensureBrokerMcpEnv } from './broker-mcp.js';
|
|
27
|
+
import { withTimeout } from './call-timeout.js';
|
|
25
28
|
import { SIGN_IN_BIND_KEY_ENV, SIGN_IN_BIND_KEY_MIN_LENGTH, SIGN_IN_BIND_KEY_SCOPE_KEY, } from './cookie-domain.js';
|
|
26
29
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
27
30
|
import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
|
|
@@ -30,6 +33,8 @@ import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied,
|
|
|
30
33
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
31
34
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
32
35
|
import { AUTH_TUNNEL_ROUTES_VERSION, AUTH_ZITADEL_RUN_OPTIONS, GITHUB_RELEASE_HOOK_DEPS_MISSING, GITHUB_RELEASE_WEBHOOK_SECRET_ENV, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_DESKTOP_CALLBACK_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_POST_LOGOUT_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_WEBHOOK_SECRET_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_MCP_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_TUNNEL_ROUTES_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_GITHUB_RELEASE_HOOK_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROLE_CHECK_SCOPE_KEY, PLATFORM_MAIL_ROLE_ENSURED_SCOPE_KEY, PLATFORM_MAIL_ROLE_GRANTED_SCOPE_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_USER_ROLE, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, orgDeclaresMcp, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
36
|
+
import { PLATFORM_REGISTRY_KEY, PLATFORM_REGISTRY_LOGINS_KEY, PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY, PLATFORM_REGISTRY_PULL_PASSWORD_REF, registryImageTagBlocked, } from './platform-registry-keys.js';
|
|
37
|
+
import { ensureRegistryLogins, provisionPlatformRegistry, registryLoginsDrifted, } from './platform-registry.js';
|
|
33
38
|
import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
|
|
34
39
|
import { LOGIN_HOST_BINDINGS_ENV } from './sign-in-bindings.js';
|
|
35
40
|
import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
|
|
@@ -143,6 +148,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
143
148
|
archon;
|
|
144
149
|
backup;
|
|
145
150
|
hc;
|
|
151
|
+
registry;
|
|
146
152
|
heartbeats;
|
|
147
153
|
uptime;
|
|
148
154
|
accessProvider;
|
|
@@ -164,6 +170,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
164
170
|
this.archon = options.archon;
|
|
165
171
|
this.backup = options.backup;
|
|
166
172
|
this.hc = options.hc;
|
|
173
|
+
this.registry = options.registry;
|
|
167
174
|
this.heartbeats = options.heartbeats;
|
|
168
175
|
this.uptime = options.uptime;
|
|
169
176
|
this.accessProvider = options.accessProvider;
|
|
@@ -184,6 +191,35 @@ export class FoundryPlatformStepExecutor {
|
|
|
184
191
|
limitedOnPlatform(deps, profile) {
|
|
185
192
|
return { ...deps, coolify: limited(this.onPlatform(deps.coolify), profile) };
|
|
186
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Why the image registry cannot be planned, as fixed text, or `undefined`. The held `pull`
|
|
196
|
+
* password is read only to know it is there; its value goes nowhere.
|
|
197
|
+
*/
|
|
198
|
+
async registryBlockedReason(platform, recordedIds) {
|
|
199
|
+
if (!this.registry) {
|
|
200
|
+
return 'the image registry is blocked, not planned: the host built the platform executor without its registry dependencies';
|
|
201
|
+
}
|
|
202
|
+
const tagBlocked = registryImageTagBlocked(platform.registry.gitRef);
|
|
203
|
+
if (tagBlocked)
|
|
204
|
+
return tagBlocked;
|
|
205
|
+
const heldKeys = this.registry.heldKeys;
|
|
206
|
+
if (!heldKeys) {
|
|
207
|
+
return `the image registry is blocked, not planned: the host cannot read held keys, so the pull password at ${PLATFORM_REGISTRY_PULL_PASSWORD_REF} cannot be read`;
|
|
208
|
+
}
|
|
209
|
+
if (recordedIds[PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY]) {
|
|
210
|
+
let held = null;
|
|
211
|
+
try {
|
|
212
|
+
held = await withTimeout('the registry pull password read', 30_000, () => heldKeys.resolve(PLATFORM_REGISTRY_PULL_PASSWORD_REF));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return `the image registry logins are blocked, not planned: the pull password at ${PLATFORM_REGISTRY_PULL_PASSWORD_REF} could not be read`;
|
|
216
|
+
}
|
|
217
|
+
if (!held) {
|
|
218
|
+
return `the image registry logins are blocked, not planned: no password is held at ${PLATFORM_REGISTRY_PULL_PASSWORD_REF}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
187
223
|
/** Every Coolify server and why platform-archon's blue/green cannot run here, for the planner. */
|
|
188
224
|
async archonBlueGreen() {
|
|
189
225
|
const archon = this.archon;
|
|
@@ -201,6 +237,23 @@ export class FoundryPlatformStepExecutor {
|
|
|
201
237
|
})),
|
|
202
238
|
};
|
|
203
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* The stage guards that can be made for platform-archon now. The planner puts the names in the applied hash
|
|
242
|
+
* and the step builds the same guards from the same facts, so a guard that becomes possible later plans the
|
|
243
|
+
* structure step once more.
|
|
244
|
+
*/
|
|
245
|
+
archonStageGuardNames(platform, recorded, owner) {
|
|
246
|
+
const guards = this.archon?.stageGuards;
|
|
247
|
+
return stageGuardInputs({
|
|
248
|
+
probesPort: Boolean(guards?.probes),
|
|
249
|
+
accessPort: Boolean(guards?.gate),
|
|
250
|
+
providerId: recorded[ACCESS_PROVIDER_RECORD_KEY],
|
|
251
|
+
domain: platform.apex,
|
|
252
|
+
claimValue: owner.signIn?.organisationId,
|
|
253
|
+
emails: owner.emails?.length ?? 0,
|
|
254
|
+
hostRuled: false,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
204
257
|
/** The release runner's outside calls for platform-archon (`plan/release-host.ts`). The token is only read. */
|
|
205
258
|
releaseHost(platform, subject, context, renew) {
|
|
206
259
|
const archon = this.archon;
|
|
@@ -223,6 +276,48 @@ export class FoundryPlatformStepExecutor {
|
|
|
223
276
|
...(archon.runtime?.fetch ? { fetch: archon.runtime.fetch } : {}),
|
|
224
277
|
renew,
|
|
225
278
|
label: `coolify:release:${subject.id}`,
|
|
279
|
+
...(() => {
|
|
280
|
+
// The copy needs the two database calls the other release calls do not; a fixture without them has no copy.
|
|
281
|
+
const ready = archon.coolify.deleteDatabase && archon.coolify.listDatabaseBackups;
|
|
282
|
+
const ports = buildStageCopyHostPorts({
|
|
283
|
+
host: ready ? archon.stageCopy : undefined,
|
|
284
|
+
coolify: archon.coolify,
|
|
285
|
+
context,
|
|
286
|
+
org: { slug: 'platform' },
|
|
287
|
+
secret: (value) => archon.runtime?.onSecret?.(value),
|
|
288
|
+
databaseKey: 'archon',
|
|
289
|
+
databaseScopeKey: PLATFORM_ARCHON_DB_SCOPE_KEY,
|
|
290
|
+
backupScopeKey: null,
|
|
291
|
+
projectScopeKey: PLATFORM_ARCHON_PROJECT_SCOPE_KEY,
|
|
292
|
+
serverScopeKey: 'coolify_server',
|
|
293
|
+
});
|
|
294
|
+
return ports ? { stageCopy: ports } : {};
|
|
295
|
+
})(),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/** Archon's stage gate follows the people holding `releases:preview` (orgId `platform`); the platform's owners stay. */
|
|
299
|
+
async syncStageGateMembers(_platform, subject, context) {
|
|
300
|
+
const archon = this.archon;
|
|
301
|
+
if (!archon)
|
|
302
|
+
return;
|
|
303
|
+
await syncStageGateMembers({
|
|
304
|
+
label: `coolify:stage-gate:${subject.id}`,
|
|
305
|
+
subjectId: subject.id,
|
|
306
|
+
orgId: 'platform',
|
|
307
|
+
owners: archon.accessEmails,
|
|
308
|
+
read: archon.releasePreviewMembers,
|
|
309
|
+
recorded: context.recordedIds(),
|
|
310
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
311
|
+
persist: () => context.persist(),
|
|
312
|
+
setPeople: async (gateId, list) => {
|
|
313
|
+
const access = await platformAccess(archon, context.recordedIds(), 'platform:archon');
|
|
314
|
+
if (!access.setGatePeople) {
|
|
315
|
+
const error = new Error('the Access port cannot change a gate');
|
|
316
|
+
error.name = 'NoGatePeoplePort';
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
await access.setGatePeople(gateId, list);
|
|
320
|
+
},
|
|
226
321
|
});
|
|
227
322
|
}
|
|
228
323
|
async archonBlueGreenStep(action, context) {
|
|
@@ -263,9 +358,43 @@ export class FoundryPlatformStepExecutor {
|
|
|
263
358
|
sleep: deps.runtime?.sleep ?? defaultSleep,
|
|
264
359
|
logDir: LOG_DIR,
|
|
265
360
|
isRunning: isRunningOrStarting,
|
|
361
|
+
stageGuards: this.archonStageGuards(platform, subject.id, recorded, context.owner ?? {}, deps),
|
|
362
|
+
guardInputs: parseGuards(action.desired.guards),
|
|
266
363
|
});
|
|
267
364
|
return undefined;
|
|
268
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Archon's stage guards: the probe port, and the Access gate over the stage host, offered only when the
|
|
368
|
+
* same facts {@link archonStageGuardNames} counts are all in hand.
|
|
369
|
+
*/
|
|
370
|
+
archonStageGuards(platform, id, recorded, owner, deps) {
|
|
371
|
+
const configured = deps.stageGuards;
|
|
372
|
+
const guards = configured?.probes ? { probes: configured.probes } : {};
|
|
373
|
+
const gateFor = configured?.gate;
|
|
374
|
+
const providerId = recorded[ACCESS_PROVIDER_RECORD_KEY];
|
|
375
|
+
const claimValue = owner.signIn?.organisationId;
|
|
376
|
+
const emails = owner.emails ?? [];
|
|
377
|
+
if (!gateFor || !providerId || !claimValue || emails.length === 0)
|
|
378
|
+
return guards;
|
|
379
|
+
const hostname = stageHostname(platform.apex, id);
|
|
380
|
+
guards.gate = {
|
|
381
|
+
hostname,
|
|
382
|
+
create: async () => {
|
|
383
|
+
const access = await platformAccess({ access: gateFor }, recorded, 'coolify:archon-bluegreen');
|
|
384
|
+
return access.createGate({
|
|
385
|
+
organisationId: platform.ownerOrgId,
|
|
386
|
+
key: stageGateKey(id),
|
|
387
|
+
entry: id,
|
|
388
|
+
hostnames: [hostname],
|
|
389
|
+
providerId,
|
|
390
|
+
claimName: GATE_CLAIM_NAME,
|
|
391
|
+
claimValue,
|
|
392
|
+
emails: [...emails],
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
return guards;
|
|
397
|
+
}
|
|
269
398
|
async blockedSteps(platform, recordedIds = {}, owner = {}) {
|
|
270
399
|
const blocked = {};
|
|
271
400
|
if (platform.auth.enabled && !this.auth) {
|
|
@@ -449,6 +578,11 @@ export class FoundryPlatformStepExecutor {
|
|
|
449
578
|
'the heartbeat server is blocked, not planned: the host names no email the Cloudflare Access app in front of it may let through';
|
|
450
579
|
}
|
|
451
580
|
}
|
|
581
|
+
if (platform.registry.enabled) {
|
|
582
|
+
const registryBlocked = await this.registryBlockedReason(platform, recordedIds);
|
|
583
|
+
if (registryBlocked)
|
|
584
|
+
blocked.registry = registryBlocked;
|
|
585
|
+
}
|
|
452
586
|
// foundry#544: `'hosted'` needs none of `hc`'s own applied state -- only
|
|
453
587
|
// the self-hosted provider's standing note depends on `hc` being
|
|
454
588
|
// recorded applied at all. `planHeartbeats` itself already withholds the
|
|
@@ -668,6 +802,11 @@ export class FoundryPlatformStepExecutor {
|
|
|
668
802
|
*/
|
|
669
803
|
async driftedSteps(platform, recordedIds = {}, owner = {}) {
|
|
670
804
|
const result = {};
|
|
805
|
+
if (platform.registry.enabled && this.registry) {
|
|
806
|
+
if (await registryLoginsDrifted(this.registry, platform, recordedIds)) {
|
|
807
|
+
result.registryLogins = true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
671
810
|
if (platform.archon.enabled &&
|
|
672
811
|
this.archon &&
|
|
673
812
|
recordedIds[`coolify:app:${PLATFORM_ARCHON_KEY}`]) {
|
|
@@ -899,6 +1038,22 @@ export class FoundryPlatformStepExecutor {
|
|
|
899
1038
|
}
|
|
900
1039
|
return provisionPlatformMailRelay(action, context, this.limitedOnPlatform(this.mail, LIMITS.platformMailRelay));
|
|
901
1040
|
}
|
|
1041
|
+
if (action.system === 'coolify' &&
|
|
1042
|
+
action.kind === 'app' &&
|
|
1043
|
+
action.key === PLATFORM_REGISTRY_KEY) {
|
|
1044
|
+
if (!this.registry) {
|
|
1045
|
+
throw new Error('platform:registry needs FoundryPlatformStepExecutor to be constructed with its registry dependencies');
|
|
1046
|
+
}
|
|
1047
|
+
return provisionPlatformRegistry(context, this.limitedOnPlatform(this.registry, LIMITS.platformRegistry));
|
|
1048
|
+
}
|
|
1049
|
+
if (action.system === 'coolify' &&
|
|
1050
|
+
action.kind === 'registry-logins' &&
|
|
1051
|
+
action.key === PLATFORM_REGISTRY_LOGINS_KEY) {
|
|
1052
|
+
if (!this.registry) {
|
|
1053
|
+
throw new Error('platform:registry-logins needs FoundryPlatformStepExecutor to be constructed with its registry dependencies');
|
|
1054
|
+
}
|
|
1055
|
+
return ensureRegistryLogins(context, this.registry);
|
|
1056
|
+
}
|
|
902
1057
|
if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_HC_KEY) {
|
|
903
1058
|
if (!this.hc) {
|
|
904
1059
|
throw new Error('platform:hc needs FoundryPlatformStepExecutor to be constructed with its hc dependencies (#540)');
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DeclaredPlatform } from '../platform.js';
|
|
2
|
+
/**
|
|
3
|
+
* C5 of the Lokes Smie through Boule campaign: the platform's own image registry. The build
|
|
4
|
+
* workflow (`wtfalch/build`) pushes company app images here; the Coolify servers pull them.
|
|
5
|
+
* Docker's `registry:3` (`distribution`) on the platform server, behind the platform tunnel at
|
|
6
|
+
* `platform.registry.hostname`, with htpasswd sign-in and two users, `push` and `pull`. Their
|
|
7
|
+
* passwords are made here, held through `deliverSecret` and never leave it (CLAUDE.md, platform
|
|
8
|
+
* steps, rule 6).
|
|
9
|
+
*
|
|
10
|
+
* htpasswd tells users apart only by who they are, not by what they may do: `pull` can push too.
|
|
11
|
+
* What keeps one company's server from putting an image under another company's name is that
|
|
12
|
+
* the app is deployed by digest (`build.json`), and `REGISTRY_STORAGE_DELETE_ENABLED` is off, so a
|
|
13
|
+
* pushed digest never changes. Read-only `pull` needs token auth and is not part of C5.
|
|
14
|
+
*/
|
|
15
|
+
/** The step keys `planRegistry` plans under and the executor dispatches on. */
|
|
16
|
+
export declare const PLATFORM_REGISTRY_KEY = "platform:registry";
|
|
17
|
+
export declare const PLATFORM_REGISTRY_LOGINS_KEY = "platform:registry-logins";
|
|
18
|
+
/** The two htpasswd users. */
|
|
19
|
+
export declare const PLATFORM_REGISTRY_PUSH_USER = "push";
|
|
20
|
+
export declare const PLATFORM_REGISTRY_PULL_USER = "pull";
|
|
21
|
+
/**
|
|
22
|
+
* The held-key refs (`deliverSecret`'s `<service>:<scope>:<name>` convention) of the two
|
|
23
|
+
* passwords. Archon reads `PLATFORM_REGISTRY_PUSH_PASSWORD_REF` to put the push credential into
|
|
24
|
+
* `wtfalch/build`'s repo secrets; foundry never writes to GitHub.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PLATFORM_REGISTRY_PUSH_PASSWORD_REF = "registry:platform:pushPassword";
|
|
27
|
+
export declare const PLATFORM_REGISTRY_PULL_PASSWORD_REF = "registry:platform:pullPassword";
|
|
28
|
+
/**
|
|
29
|
+
* What a consumer needs to push to the registry: the host (the `REGISTRY_HOST` secret), the user
|
|
30
|
+
* (`REGISTRY_PUSH_USER`) and the held ref the password (`REGISTRY_PUSH_PASSWORD`) is read from.
|
|
31
|
+
* Names only, never the password.
|
|
32
|
+
*/
|
|
33
|
+
export declare function platformRegistryPushCredential(platform: DeclaredPlatform): {
|
|
34
|
+
host: string;
|
|
35
|
+
user: string;
|
|
36
|
+
passwordRef: string;
|
|
37
|
+
};
|
|
38
|
+
/** Scope ids (colon-free, CLAUDE.md platform steps rule 1). */
|
|
39
|
+
export declare const PLATFORM_REGISTRY_PASSWORDS_UNDELIVERED_SCOPE_KEY = "registry_passwords_undelivered";
|
|
40
|
+
/** sha256 of both passwords, recorded after delivery. Never the passwords. Also the login version. */
|
|
41
|
+
export declare const PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY = "registry_passwords_set";
|
|
42
|
+
export declare const PLATFORM_REGISTRY_APPLIED_HOSTNAME_SCOPE_KEY = "registry_applied_hostname";
|
|
43
|
+
export declare const PLATFORM_REGISTRY_APPLIED_GIT_REF_SCOPE_KEY = "registry_applied_git_ref";
|
|
44
|
+
export declare const PLATFORM_REGISTRY_INTERNAL_URL_SCOPE_KEY = "platform_registry_internal_url";
|
|
45
|
+
/** `<hostname>|<passwords set>` once every reachable server is logged in to that host with that `pull` password. */
|
|
46
|
+
export declare const PLATFORM_REGISTRY_LOGINS_APPLIED_SCOPE_KEY = "registry_logins_applied";
|
|
47
|
+
/** The servers the last login run left out because Coolify could not reach them. */
|
|
48
|
+
export declare const PLATFORM_REGISTRY_LOGINS_SKIPPED_SCOPE_KEY = "registry_logins_skipped";
|
|
49
|
+
export declare const PLATFORM_REGISTRY_PROJECT_SCOPE_KEY = "coolify_project_platform_registry";
|
|
50
|
+
export declare const PLATFORM_REGISTRY_APP_SCOPE_KEY = "coolify_platform_registry_app";
|
|
51
|
+
export declare const PLATFORM_REGISTRY_TUNNEL_SCOPE_KEY = "cloudflare_platform_registry_tunnel";
|
|
52
|
+
export declare const PLATFORM_REGISTRY_CONNECTOR_APP_SCOPE_KEY = "coolify_platform_registry_connector_app";
|
|
53
|
+
/** The scope id that records one server's login. Its value is the same as {@link registryLoginVersion}. */
|
|
54
|
+
export declare function registryServerLoginScopeKey(serverUuid: string): string;
|
|
55
|
+
/** What a finished login to `hostname` with the passwords in `set` is recorded as. */
|
|
56
|
+
export declare function registryLoginVersion(hostname: string, set: string | undefined): string;
|
|
57
|
+
/** Why the registry image tag cannot be used, as plan text, or `null`. */
|
|
58
|
+
export declare function registryImageTagBlocked(tag: string): string | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
|
+
/**
|
|
3
|
+
* C5 of the Lokes Smie through Boule campaign: the platform's own image registry. The build
|
|
4
|
+
* workflow (`wtfalch/build`) pushes company app images here; the Coolify servers pull them.
|
|
5
|
+
* Docker's `registry:3` (`distribution`) on the platform server, behind the platform tunnel at
|
|
6
|
+
* `platform.registry.hostname`, with htpasswd sign-in and two users, `push` and `pull`. Their
|
|
7
|
+
* passwords are made here, held through `deliverSecret` and never leave it (CLAUDE.md, platform
|
|
8
|
+
* steps, rule 6).
|
|
9
|
+
*
|
|
10
|
+
* htpasswd tells users apart only by who they are, not by what they may do: `pull` can push too.
|
|
11
|
+
* What keeps one company's server from putting an image under another company's name is that
|
|
12
|
+
* the app is deployed by digest (`build.json`), and `REGISTRY_STORAGE_DELETE_ENABLED` is off, so a
|
|
13
|
+
* pushed digest never changes. Read-only `pull` needs token auth and is not part of C5.
|
|
14
|
+
*/
|
|
15
|
+
/** The step keys `planRegistry` plans under and the executor dispatches on. */
|
|
16
|
+
export const PLATFORM_REGISTRY_KEY = 'platform:registry';
|
|
17
|
+
export const PLATFORM_REGISTRY_LOGINS_KEY = 'platform:registry-logins';
|
|
18
|
+
/** The two htpasswd users. */
|
|
19
|
+
export const PLATFORM_REGISTRY_PUSH_USER = 'push';
|
|
20
|
+
export const PLATFORM_REGISTRY_PULL_USER = 'pull';
|
|
21
|
+
/**
|
|
22
|
+
* The held-key refs (`deliverSecret`'s `<service>:<scope>:<name>` convention) of the two
|
|
23
|
+
* passwords. Archon reads `PLATFORM_REGISTRY_PUSH_PASSWORD_REF` to put the push credential into
|
|
24
|
+
* `wtfalch/build`'s repo secrets; foundry never writes to GitHub.
|
|
25
|
+
*/
|
|
26
|
+
export const PLATFORM_REGISTRY_PUSH_PASSWORD_REF = 'registry:platform:pushPassword';
|
|
27
|
+
export const PLATFORM_REGISTRY_PULL_PASSWORD_REF = 'registry:platform:pullPassword';
|
|
28
|
+
/**
|
|
29
|
+
* What a consumer needs to push to the registry: the host (the `REGISTRY_HOST` secret), the user
|
|
30
|
+
* (`REGISTRY_PUSH_USER`) and the held ref the password (`REGISTRY_PUSH_PASSWORD`) is read from.
|
|
31
|
+
* Names only, never the password.
|
|
32
|
+
*/
|
|
33
|
+
export function platformRegistryPushCredential(platform) {
|
|
34
|
+
return {
|
|
35
|
+
host: platform.registry.hostname,
|
|
36
|
+
user: PLATFORM_REGISTRY_PUSH_USER,
|
|
37
|
+
passwordRef: PLATFORM_REGISTRY_PUSH_PASSWORD_REF,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Scope ids (colon-free, CLAUDE.md platform steps rule 1). */
|
|
41
|
+
export const PLATFORM_REGISTRY_PASSWORDS_UNDELIVERED_SCOPE_KEY = 'registry_passwords_undelivered';
|
|
42
|
+
/** sha256 of both passwords, recorded after delivery. Never the passwords. Also the login version. */
|
|
43
|
+
export const PLATFORM_REGISTRY_PASSWORDS_SET_SCOPE_KEY = 'registry_passwords_set';
|
|
44
|
+
export const PLATFORM_REGISTRY_APPLIED_HOSTNAME_SCOPE_KEY = 'registry_applied_hostname';
|
|
45
|
+
export const PLATFORM_REGISTRY_APPLIED_GIT_REF_SCOPE_KEY = 'registry_applied_git_ref';
|
|
46
|
+
export const PLATFORM_REGISTRY_INTERNAL_URL_SCOPE_KEY = 'platform_registry_internal_url';
|
|
47
|
+
/** `<hostname>|<passwords set>` once every reachable server is logged in to that host with that `pull` password. */
|
|
48
|
+
export const PLATFORM_REGISTRY_LOGINS_APPLIED_SCOPE_KEY = 'registry_logins_applied';
|
|
49
|
+
/** The servers the last login run left out because Coolify could not reach them. */
|
|
50
|
+
export const PLATFORM_REGISTRY_LOGINS_SKIPPED_SCOPE_KEY = 'registry_logins_skipped';
|
|
51
|
+
export const PLATFORM_REGISTRY_PROJECT_SCOPE_KEY = 'coolify_project_platform_registry';
|
|
52
|
+
export const PLATFORM_REGISTRY_APP_SCOPE_KEY = 'coolify_platform_registry_app';
|
|
53
|
+
export const PLATFORM_REGISTRY_TUNNEL_SCOPE_KEY = 'cloudflare_platform_registry_tunnel';
|
|
54
|
+
export const PLATFORM_REGISTRY_CONNECTOR_APP_SCOPE_KEY = 'coolify_platform_registry_connector_app';
|
|
55
|
+
/** The scope id that records one server's login. Its value is the same as {@link registryLoginVersion}. */
|
|
56
|
+
export function registryServerLoginScopeKey(serverUuid) {
|
|
57
|
+
return `registry_login_${serverUuid}`;
|
|
58
|
+
}
|
|
59
|
+
/** What a finished login to `hostname` with the passwords in `set` is recorded as. */
|
|
60
|
+
export function registryLoginVersion(hostname, set) {
|
|
61
|
+
return `${hostname}|${set ?? ''}`;
|
|
62
|
+
}
|
|
63
|
+
/** Why the registry image tag cannot be used, as plan text, or `null`. */
|
|
64
|
+
export function registryImageTagBlocked(tag) {
|
|
65
|
+
const reason = floatingTagBlocked('registry', tag, VERSIONS.registry);
|
|
66
|
+
return reason
|
|
67
|
+
? `the image registry is blocked, not planned: ${reason} as the registry gitRef`
|
|
68
|
+
: null;
|
|
69
|
+
}
|