@wtfalch/foundry 0.61.0 → 0.63.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 +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +3 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/modules/sign-in.js +1 -1
- package/dist/native-build.d.ts +50 -0
- package/dist/native-build.js +72 -0
- package/dist/org.d.ts +22 -3
- package/dist/org.js +25 -2
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- 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 +80 -9
- package/dist/plan/blue-green.js +115 -14
- 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/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +4 -1
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +184 -63
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +8 -0
- package/dist/plan/executor.d.ts +111 -19
- package/dist/plan/executor.js +578 -72
- 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/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/locked-hostnames.js +5 -2
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -1
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-limits.d.ts +12 -0
- package/dist/plan/own-limits.js +2 -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 +6 -0
- package/dist/plan/platform-diff.js +74 -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 +162 -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/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +5 -2
- package/dist/plan/providers-readers.js +20 -1
- 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/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- 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 +13 -1
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +11 -2
- package/dist/store/postgres.js +21 -0
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.js +10 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
|
@@ -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)');
|
|
@@ -3905,6 +4060,9 @@ async function waitForMailInstance(url, runtime, sleep) {
|
|
|
3905
4060
|
for (;;) {
|
|
3906
4061
|
try {
|
|
3907
4062
|
const response = await fetchImpl(url);
|
|
4063
|
+
// #760: an unread body left undici's parser paused when the socket
|
|
4064
|
+
// ended, throwing an uncaught assertion in the host.
|
|
4065
|
+
await response.body?.cancel();
|
|
3908
4066
|
if (response.ok)
|
|
3909
4067
|
return;
|
|
3910
4068
|
}
|
|
@@ -7035,6 +7193,9 @@ async function waitForInstance(url, runtime, sleep, label = 'platform:auth') {
|
|
|
7035
7193
|
const response = await fetchImpl(url, {
|
|
7036
7194
|
signal: AbortSignal.timeout(runtime.readyRequestTimeoutMs ?? 10_000),
|
|
7037
7195
|
});
|
|
7196
|
+
// #760: an unread body left undici's parser paused when the socket
|
|
7197
|
+
// ended, throwing an uncaught assertion in the host.
|
|
7198
|
+
await response.body?.cancel();
|
|
7038
7199
|
if (response.ok)
|
|
7039
7200
|
return;
|
|
7040
7201
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { DeclaredPlatform } from '../platform.js';
|
|
2
|
+
import type { CoolifyPort } from './executor.js';
|
|
3
|
+
import type { PlatformStepExecutionContext } from './plan-apply.js';
|
|
4
|
+
import { type PlatformDnsFactory, type PlatformTunnelsFactory } from './platform-providers.js';
|
|
5
|
+
export interface PlatformRegistryRuntime {
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
sleep?: (ms: number) => Promise<void>;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
readyTimeoutMs?: number;
|
|
10
|
+
readyIntervalMs?: number;
|
|
11
|
+
/** Tries at handing each password to the host (default 3). */
|
|
12
|
+
deliverAttempts?: number;
|
|
13
|
+
/** Told of every password this step makes or reads, so the host can scrub it from logs and audit rows. */
|
|
14
|
+
onSecret?: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Everything the registry steps need beyond {@link PlatformStepExecutionContext}. `deliverSecret`
|
|
18
|
+
* and `heldKeys` are the platform's held-key writes and reads (edge's pattern); `listServers` and
|
|
19
|
+
* `raw` reach every server Coolify manages, not only the platform's.
|
|
20
|
+
*/
|
|
21
|
+
export interface PlatformRegistryDeps {
|
|
22
|
+
coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'updateFileStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
|
|
23
|
+
/** F4.dns: every DNS record call, through edge at its recorded address. */
|
|
24
|
+
dns: PlatformDnsFactory;
|
|
25
|
+
/** F4.tunnels: every tunnel call, through edge at its recorded address. */
|
|
26
|
+
tunnels: PlatformTunnelsFactory;
|
|
27
|
+
deliverSecret: (ref: string, secret: string) => Promise<void>;
|
|
28
|
+
/** Reads a held key back. The login step reads the `pull` password this way. */
|
|
29
|
+
heldKeys?: {
|
|
30
|
+
resolve(ref: string): Promise<string | null>;
|
|
31
|
+
};
|
|
32
|
+
runtime?: PlatformRegistryRuntime;
|
|
33
|
+
}
|
|
34
|
+
/** One htpasswd line (`user:bcrypt hash`). Docker's `distribution` accepts bcrypt only. */
|
|
35
|
+
export declare function htpasswdLine(user: string, password: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* The registry app: `registry:3` (by digest), the data volume, the htpasswd file, the tunnel and
|
|
38
|
+
* the DNS record. Safe to run again at any point:
|
|
39
|
+
* - The two passwords are made once. The htpasswd file is written before they are recorded as
|
|
40
|
+
* undelivered, so the only window that blocks a re-run is the one the other platform steps
|
|
41
|
+
* have (CLAUDE.md, rule 1).
|
|
42
|
+
* - The app is deployed when it was created, is not running, its env changed, the declared
|
|
43
|
+
* hostname or tag changed, or the passwords were made in this run.
|
|
44
|
+
* - The applied hostname and tag are recorded last.
|
|
45
|
+
*/
|
|
46
|
+
export declare function provisionPlatformRegistry(context: PlatformStepExecutionContext, deps: PlatformRegistryDeps): Promise<{
|
|
47
|
+
resourceId?: string;
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Logs every Coolify server in to the registry as `pull` (`POST /servers/{uuid}/registries`,
|
|
51
|
+
* which runs `docker login` on the server and replaces an earlier login to the same host).
|
|
52
|
+
* - A server already recorded as logged in to this host with these passwords is left alone.
|
|
53
|
+
* - A server Coolify marks unusable or unreachable is skipped and named under
|
|
54
|
+
* {@link PLATFORM_REGISTRY_LOGINS_SKIPPED_SCOPE_KEY}; `driftedSteps` plans the step again
|
|
55
|
+
* once it is reachable.
|
|
56
|
+
* - A login that fails is counted; the others still run, each success is recorded at once, and
|
|
57
|
+
* the step then throws a text naming the server ids, so the next apply tries only those again.
|
|
58
|
+
* - Every Coolify call has a timeout. The password is read from the held keys and goes only
|
|
59
|
+
* into the request body.
|
|
60
|
+
*/
|
|
61
|
+
export declare function ensureRegistryLogins(context: PlatformStepExecutionContext, deps: PlatformRegistryDeps): Promise<Record<string, never>>;
|
|
62
|
+
/**
|
|
63
|
+
* True when a usable, reachable server has no login recorded for the current registry host and
|
|
64
|
+
* passwords: a server added, or back up, since the last login run. A read that fails counts as
|
|
65
|
+
* not drifted, like the Archon drift reads.
|
|
66
|
+
*/
|
|
67
|
+
export declare function registryLoginsDrifted(deps: Pick<PlatformRegistryDeps, 'coolify'>, platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<boolean>;
|