@wtfalch/foundry 0.41.2 → 0.43.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/versions.d.ts +2 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/migrations/0054_org_signin_in_app.sql +6 -0
- package/dist/migrations/0055_release_requests.sql +20 -0
- package/dist/migrations/0056_platform_archon_blue_green.sql +7 -0
- package/dist/org.d.ts +30 -0
- package/dist/org.js +52 -1
- package/dist/plan/agora.d.ts +4 -1
- package/dist/plan/agora.js +16 -2
- package/dist/plan/archon-blue-green.d.ts +54 -0
- package/dist/plan/archon-blue-green.js +72 -0
- package/dist/plan/blue-green-apply.d.ts +82 -0
- package/dist/plan/blue-green-apply.js +360 -0
- package/dist/plan/blue-green.d.ts +223 -0
- package/dist/plan/blue-green.js +444 -0
- package/dist/plan/boule.d.ts +5 -2
- package/dist/plan/boule.js +14 -2
- package/dist/plan/cloudflared-connector.d.ts +26 -0
- package/dist/plan/cloudflared-connector.js +37 -0
- package/dist/plan/coolify-previews.d.ts +10 -0
- package/dist/plan/coolify-previews.js +14 -0
- package/dist/plan/diff.d.ts +6 -0
- package/dist/plan/diff.js +123 -11
- package/dist/plan/edge-ratelimits.d.ts +5 -0
- package/dist/plan/edge-ratelimits.js +16 -6
- package/dist/plan/executor.d.ts +79 -0
- package/dist/plan/executor.js +527 -31
- package/dist/plan/login-host.d.ts +23 -2
- package/dist/plan/login-host.js +30 -5
- package/dist/plan/own-diff.js +6 -0
- package/dist/plan/plan-apply.d.ts +51 -0
- package/dist/plan/plan-apply.js +222 -0
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +28 -1
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-org.js +5 -0
- package/dist/plan/platform-providers.d.ts +31 -2
- package/dist/plan/platform-providers.js +345 -13
- package/dist/plan/providers-readers.js +6 -2
- package/dist/plan/proxy-labels.d.ts +2 -2
- package/dist/plan/proxy-labels.js +12 -4
- package/dist/plan/release-host.d.ts +59 -0
- package/dist/plan/release-host.js +239 -0
- package/dist/plan/remote-database.d.ts +124 -0
- package/dist/plan/remote-database.js +373 -0
- package/dist/plan/sign-in-archon.d.ts +46 -0
- package/dist/plan/sign-in-archon.js +71 -0
- package/dist/plan/sign-in-bindings.d.ts +16 -0
- package/dist/plan/sign-in-bindings.js +44 -0
- package/dist/plan/sign-in.d.ts +135 -0
- package/dist/plan/sign-in.js +172 -0
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +4 -1
- package/dist/release/decide.d.ts +95 -0
- package/dist/release/decide.js +242 -0
- package/dist/release/health.d.ts +48 -0
- package/dist/release/health.js +61 -0
- package/dist/release/run.d.ts +40 -0
- package/dist/release/run.js +56 -0
- package/dist/release/state.d.ts +64 -0
- package/dist/release/state.js +126 -0
- package/dist/release/types.d.ts +54 -0
- package/dist/release/types.js +4 -0
- package/dist/store/postgres.d.ts +8 -0
- package/dist/store/postgres.js +62 -3
- package/dist/store/schema.d.ts +171 -0
- package/dist/store/schema.js +14 -0
- package/package.json +1 -1
- package/versions.digests.json +2 -1
- package/versions.json +2 -1
|
@@ -3,17 +3,21 @@ import { isIPv4 } from 'node:net';
|
|
|
3
3
|
import { applyObjects, openInstance, platformMailSettingsObjects } from '@wtfalch/email/postmaster';
|
|
4
4
|
import { VERSIONS, coolifyImageFor, floatingTagBlocked, pinnedRef } from '../bootstrap/versions.js';
|
|
5
5
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
6
|
-
import { ownerMailboxes } from '../org.js';
|
|
6
|
+
import { ownerMailboxes, sharedSecretRef, } from '../org.js';
|
|
7
|
+
import { applyArchonBlueGreen } from './archon-blue-green.js';
|
|
8
|
+
import { ARCHON_REPO, archonSubject, healthTokenName, tunnelOriginFor, } from './blue-green.js';
|
|
7
9
|
import { BOULE_BRIDGE_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_PEER_ROLE, bouleHostnameFor, } from './boule.js';
|
|
8
10
|
import { recordBuiltCommit } from './build-record.js';
|
|
9
11
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
12
|
+
import { previewsOn } from './coolify-previews.js';
|
|
10
13
|
import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
11
14
|
import { placedOnPlatform } from './destinations.js';
|
|
12
|
-
import { ACCESS_POLICY_NAME, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
|
|
15
|
+
import { ACCESS_POLICY_NAME, LOG_DIR, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
|
|
13
16
|
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
14
17
|
import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, tcpProbe, } from './health-check-settings.js';
|
|
15
18
|
import { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
16
19
|
import { LIMITS, limited } from './own-limits.js';
|
|
20
|
+
import { createReleaseHost } from './release-host.js';
|
|
17
21
|
export { FILES_MANAGEMENT_KEY_REF, JOBS_ADMIN_TOKEN_REF, PLATFORM_AI_MANAGEMENT_KEY_REF, } from './held-key-refs.js';
|
|
18
22
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
19
23
|
import { MAIL_RELAY_BLOCKED, provisionMailRelay } from './mail-relay.js';
|
|
@@ -22,7 +26,10 @@ import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied,
|
|
|
22
26
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
23
27
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
24
28
|
import { AUTH_ZITADEL_RUN_OPTIONS, 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_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_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_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_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_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_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_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_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, 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, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
29
|
+
import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
|
|
30
|
+
import { LOGIN_HOST_BINDINGS_ENV } from './sign-in-bindings.js';
|
|
25
31
|
import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
|
|
32
|
+
import { SIGN_IN_BASE_PATH, SIGN_IN_BUILD_ARG_ENV, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_UPSTREAM_ENV, inAppOnSharedLogin, signInLoginNames, signInUpstream, } from './sign-in.js';
|
|
26
33
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
27
34
|
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
28
35
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
@@ -169,6 +176,86 @@ export class FoundryPlatformStepExecutor {
|
|
|
169
176
|
limitedOnPlatform(deps, profile) {
|
|
170
177
|
return { ...deps, coolify: limited(this.onPlatform(deps.coolify), profile) };
|
|
171
178
|
}
|
|
179
|
+
/** Every Coolify server and why platform-archon's blue/green cannot run here, for the planner. */
|
|
180
|
+
async archonBlueGreen() {
|
|
181
|
+
const archon = this.archon;
|
|
182
|
+
if (!archon)
|
|
183
|
+
return { blocked: 'the host built the platform executor without archon dependencies' };
|
|
184
|
+
if (typeof archon.heldKeys?.put !== 'function') {
|
|
185
|
+
return { blocked: 'the host cannot hold the health token (its held keys cannot put)' };
|
|
186
|
+
}
|
|
187
|
+
const servers = await archon.coolify.listServers();
|
|
188
|
+
return {
|
|
189
|
+
servers: servers.map((s) => ({
|
|
190
|
+
uuid: s.uuid,
|
|
191
|
+
usable: s.is_usable,
|
|
192
|
+
reachable: s.is_reachable,
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The release runner's outside calls for platform-archon (`plan/release-host.ts`). The token is only read. */
|
|
197
|
+
releaseHost(platform, subject, context, renew) {
|
|
198
|
+
const archon = this.archon;
|
|
199
|
+
const recorded = context.recordedIds();
|
|
200
|
+
const primaryBlueAppId = recorded[`coolify:app:${PLATFORM_ARCHON_KEY}`];
|
|
201
|
+
const primaryUuid = recorded.coolify_server;
|
|
202
|
+
if (!archon || !primaryBlueAppId || !primaryUuid)
|
|
203
|
+
return null;
|
|
204
|
+
return createReleaseHost({
|
|
205
|
+
subject,
|
|
206
|
+
context,
|
|
207
|
+
coolify: archon.coolify,
|
|
208
|
+
healthToken: async () => (await archon.heldKeys?.resolve(sharedSecretRef(platform.ownerOrgId, healthTokenName(subject.id)))) ?? null,
|
|
209
|
+
secret: (value) => archon.runtime?.onSecret?.(value),
|
|
210
|
+
primaryUuid,
|
|
211
|
+
primaryBlueAppId,
|
|
212
|
+
runtime: this.runtime,
|
|
213
|
+
...(archon.runtime?.fetch ? { fetch: archon.runtime.fetch } : {}),
|
|
214
|
+
renew,
|
|
215
|
+
label: `coolify:release:${subject.id}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async archonBlueGreenStep(action, context) {
|
|
219
|
+
if (!this.archon) {
|
|
220
|
+
throw new Error('platform:archon needs FoundryPlatformStepExecutor to be constructed with its archon dependencies (#175)');
|
|
221
|
+
}
|
|
222
|
+
const deps = this.limitedOnPlatform(this.archon, LIMITS.platformArchon);
|
|
223
|
+
const platform = context.platform;
|
|
224
|
+
const subject = archonSubject(platform, ARCHON_REPO);
|
|
225
|
+
const recorded = context.recordedIds();
|
|
226
|
+
const blueAppId = recorded[`coolify:app:${PLATFORM_ARCHON_KEY}`];
|
|
227
|
+
const projectUuid = recorded[PLATFORM_ARCHON_PROJECT_SCOPE_KEY];
|
|
228
|
+
const tunnelId = recorded[PLATFORM_ARCHON_TUNNEL_SCOPE_KEY];
|
|
229
|
+
if (!subject || !blueAppId || !projectUuid || !tunnelId) {
|
|
230
|
+
throw new Error('coolify:archon-bluegreen: platform-archon is not applied yet');
|
|
231
|
+
}
|
|
232
|
+
if (typeof deps.heldKeys?.put !== 'function') {
|
|
233
|
+
throw new Error('coolify:archon-bluegreen: the host cannot hold the health token');
|
|
234
|
+
}
|
|
235
|
+
await applyArchonBlueGreen({
|
|
236
|
+
subject,
|
|
237
|
+
ownerOrgId: platform.ownerOrgId,
|
|
238
|
+
desired: { primary: String(action.desired.primary ?? '') },
|
|
239
|
+
recorded: () => context.recordedIds(),
|
|
240
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
241
|
+
persist: async () => {
|
|
242
|
+
await context.persist?.();
|
|
243
|
+
},
|
|
244
|
+
coolify: deps.coolify,
|
|
245
|
+
heldKeys: { resolve: deps.heldKeys.resolve, put: deps.heldKeys.put },
|
|
246
|
+
secret: (value) => deps.runtime?.onSecret?.(value),
|
|
247
|
+
projectUuid,
|
|
248
|
+
blueAppId,
|
|
249
|
+
tunnelId,
|
|
250
|
+
hostname: platform.archon.hostname,
|
|
251
|
+
tunnels: await platformTunnels(deps, context.recordedIds(), 'coolify:archon-bluegreen'),
|
|
252
|
+
runtime: deps.runtime ?? {},
|
|
253
|
+
sleep: deps.runtime?.sleep ?? defaultSleep,
|
|
254
|
+
logDir: LOG_DIR,
|
|
255
|
+
isRunning: isRunningOrStarting,
|
|
256
|
+
});
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
172
259
|
async blockedSteps(platform, recordedIds = {}, owner = {}) {
|
|
173
260
|
const blocked = {};
|
|
174
261
|
if (platform.auth.enabled && !this.auth) {
|
|
@@ -594,8 +681,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
594
681
|
}
|
|
595
682
|
if (platform.auth.enabled && this.auth) {
|
|
596
683
|
const loginClientDrifted = await authLoginClientDrifted(platform, this.auth, recordedIds);
|
|
597
|
-
if (loginClientDrifted)
|
|
684
|
+
if (loginClientDrifted || (await this.signInLoginDrifted(this.auth, recordedIds))) {
|
|
598
685
|
result.auth = true;
|
|
686
|
+
}
|
|
599
687
|
}
|
|
600
688
|
// #380: once the runner app is recorded, re-check its live env against
|
|
601
689
|
// every setting `ensureStarted()` requires -- the gap that left the
|
|
@@ -653,13 +741,45 @@ export class FoundryPlatformStepExecutor {
|
|
|
653
741
|
await coolify.deploy(runnerAppId);
|
|
654
742
|
return { [PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY]: normalHosts(wanted).join(',') };
|
|
655
743
|
}
|
|
744
|
+
/** True while a company signs in on its apps' own addresses through the platform's login. */
|
|
745
|
+
async signInLoginWanted() {
|
|
746
|
+
return (await this.declaredOrgStore.list()).some(inAppOnSharedLogin);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* In-app sign-in: the login app that serves `/sign-in` is wanted and either not recorded, gone from
|
|
750
|
+
* Coolify, or holding a host table other than the login's. The step then plans again and repairs it.
|
|
751
|
+
* A Coolify read that fails counts as not drifted, with one fixed log line (the error can carry values).
|
|
752
|
+
*/
|
|
753
|
+
async signInLoginDrifted(auth, recordedIds) {
|
|
754
|
+
if (!(await this.signInLoginWanted()))
|
|
755
|
+
return false;
|
|
756
|
+
const signInId = recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY];
|
|
757
|
+
const loginId = recordedIds[PLATFORM_AUTH_LOGIN_SCOPE_KEY];
|
|
758
|
+
if (!signInId)
|
|
759
|
+
return true;
|
|
760
|
+
if (!loginId)
|
|
761
|
+
return false;
|
|
762
|
+
try {
|
|
763
|
+
const [login, signIn] = await Promise.all([
|
|
764
|
+
currentEnvValue(auth.coolify, loginId, LOGIN_HOST_BINDINGS_ENV),
|
|
765
|
+
currentEnvValue(auth.coolify, signInId, LOGIN_HOST_BINDINGS_ENV),
|
|
766
|
+
]);
|
|
767
|
+
return (login ?? '') !== (signIn ?? '');
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
if (isNotFoundError(error))
|
|
771
|
+
return true;
|
|
772
|
+
console.error('platform:auth: could not read the login apps to compare their host tables; treating the in-app login as in step');
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
656
776
|
async execute(action, context) {
|
|
657
777
|
if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_AUTH_KEY) {
|
|
658
778
|
if (!this.auth) {
|
|
659
779
|
throw new Error('platform:auth needs FoundryPlatformStepExecutor to be constructed with its auth dependencies (#168)');
|
|
660
780
|
}
|
|
661
781
|
const company = await this.declaredOrgStore.get(context.platform.ownerOrgId);
|
|
662
|
-
return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), company);
|
|
782
|
+
return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), company, await this.signInLoginWanted());
|
|
663
783
|
}
|
|
664
784
|
if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_FILES_KEY) {
|
|
665
785
|
if (!this.files) {
|
|
@@ -687,6 +807,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
687
807
|
}
|
|
688
808
|
return provisionPlatformEdge(context, this.limitedOnPlatform(this.edge, LIMITS.platformEdge));
|
|
689
809
|
}
|
|
810
|
+
if (action.system === 'coolify' && action.kind === 'archon-bluegreen') {
|
|
811
|
+
return this.archonBlueGreenStep(action, context);
|
|
812
|
+
}
|
|
690
813
|
if (action.system === 'coolify' &&
|
|
691
814
|
action.kind === 'app' &&
|
|
692
815
|
action.key === PLATFORM_ARCHON_KEY) {
|
|
@@ -919,6 +1042,7 @@ export async function provisionJobsHost(scope, deps, target, input) {
|
|
|
919
1042
|
const server = await resolvePlatformServer(coolify);
|
|
920
1043
|
const githubApp = await coolify.requireGithubApp();
|
|
921
1044
|
const created = await coolify.createGithubAppApplication({
|
|
1045
|
+
is_preview_deployments_enabled: false,
|
|
922
1046
|
project_uuid: projectUuid,
|
|
923
1047
|
server_uuid: server.uuid,
|
|
924
1048
|
environment_name: environment.name,
|
|
@@ -1552,6 +1676,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1552
1676
|
const server = await resolvePlatformServer(deps.coolify);
|
|
1553
1677
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
1554
1678
|
return deps.coolify.createGithubAppApplication({
|
|
1679
|
+
is_preview_deployments_enabled: false,
|
|
1555
1680
|
project_uuid: projectUuid,
|
|
1556
1681
|
server_uuid: server.uuid,
|
|
1557
1682
|
environment_name: environment.name,
|
|
@@ -1571,11 +1696,46 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1571
1696
|
custom_internal_name: target.internal.authLogin,
|
|
1572
1697
|
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
1573
1698
|
});
|
|
1699
|
+
// ---- the in-app login app (`/sign-in`) ------------------------------------
|
|
1700
|
+
// A Next.js base path is fixed at build, so the login that serves `/sign-in` is a second app built from
|
|
1701
|
+
// the same Dockerfile with the base path as a build-time env. It is recorded as applied only at the end,
|
|
1702
|
+
// once it is deployed and holds the login token.
|
|
1703
|
+
const signInNames = input.signInLogin ? signInLoginNames(target) : null;
|
|
1704
|
+
const signInApp = signInNames
|
|
1705
|
+
? await findOrCreateSignInLoginApp(deps.coolify, {
|
|
1706
|
+
name: signInNames.name,
|
|
1707
|
+
environment,
|
|
1708
|
+
label,
|
|
1709
|
+
create: async () => {
|
|
1710
|
+
const server = await resolvePlatformServer(deps.coolify);
|
|
1711
|
+
const githubApp = await deps.coolify.requireGithubApp();
|
|
1712
|
+
return deps.coolify.createGithubAppApplication({
|
|
1713
|
+
project_uuid: projectUuid,
|
|
1714
|
+
server_uuid: server.uuid,
|
|
1715
|
+
environment_name: environment.name,
|
|
1716
|
+
environment_uuid: environment.uuid,
|
|
1717
|
+
github_app_uuid: githubApp.uuid,
|
|
1718
|
+
git_repository: AUTH_REPO,
|
|
1719
|
+
git_branch: gitRef,
|
|
1720
|
+
build_pack: 'dockerfile',
|
|
1721
|
+
name: signInNames.name,
|
|
1722
|
+
dockerfile_location: AUTH_LOGIN_DOCKERFILE,
|
|
1723
|
+
});
|
|
1724
|
+
},
|
|
1725
|
+
})
|
|
1726
|
+
: null;
|
|
1727
|
+
if (signInApp && signInNames) {
|
|
1728
|
+
await deps.coolify.updateApplication(signInApp.appId, {
|
|
1729
|
+
custom_internal_name: signInNames.internal,
|
|
1730
|
+
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1574
1733
|
// ---- the broker app (#204) -------------------------------------------------
|
|
1575
1734
|
const brokerApp = await findOrCreatePlatformApp(deps.coolify, target.names.authBroker, environment, async () => {
|
|
1576
1735
|
const server = await resolvePlatformServer(deps.coolify);
|
|
1577
1736
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
1578
1737
|
return deps.coolify.createGithubAppApplication({
|
|
1738
|
+
is_preview_deployments_enabled: false,
|
|
1579
1739
|
project_uuid: projectUuid,
|
|
1580
1740
|
server_uuid: server.uuid,
|
|
1581
1741
|
environment_name: environment.name,
|
|
@@ -1709,6 +1869,19 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1709
1869
|
await deps.coolify.bulkUpsertEnvs(zitadelAppId, zitadelEnvs);
|
|
1710
1870
|
await deps.coolify.bulkUpsertEnvs(loginAppId, loginEnvs);
|
|
1711
1871
|
await deps.coolify.bulkUpsertEnvs(brokerAppId, brokerEnvs);
|
|
1872
|
+
// The in-app login: the login's env, the base path as a build argument, and the host table.
|
|
1873
|
+
let signInEnvsChanged = false;
|
|
1874
|
+
if (signInApp && input.signInLogin) {
|
|
1875
|
+
const bindings = input.signInLogin.bindings ??
|
|
1876
|
+
(await currentEnvValue(deps.coolify, loginAppId, LOGIN_HOST_BINDINGS_ENV));
|
|
1877
|
+
const signInEnvs = [
|
|
1878
|
+
...loginEnvs,
|
|
1879
|
+
{ key: SIGN_IN_BUILD_ARG_ENV, value: SIGN_IN_BASE_PATH, buildTime: true },
|
|
1880
|
+
...(bindings ? [{ key: LOGIN_HOST_BINDINGS_ENV, value: bindings }] : []),
|
|
1881
|
+
];
|
|
1882
|
+
signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, signInEnvs);
|
|
1883
|
+
await deps.coolify.bulkUpsertEnvs(signInApp.appId, signInEnvs);
|
|
1884
|
+
}
|
|
1712
1885
|
// Deploy an app that was just created, is not running (a throw after the
|
|
1713
1886
|
// create left it never deployed, or it stopped), or whose envs changed.
|
|
1714
1887
|
// A move that started ZITADEL on the TLS database and never saw it healthy there (#746): start it
|
|
@@ -1732,6 +1905,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1732
1905
|
if (loginApp.justCreated || !loginApp.running || loginEnvsChanged) {
|
|
1733
1906
|
await deps.coolify.deploy(loginAppId);
|
|
1734
1907
|
}
|
|
1908
|
+
if (signInApp && (signInApp.justCreated || !signInApp.running || signInEnvsChanged)) {
|
|
1909
|
+
await deps.coolify.deploy(signInApp.appId);
|
|
1910
|
+
}
|
|
1735
1911
|
// #211: the broker's own `server.ts` refuses to start without
|
|
1736
1912
|
// `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
|
|
1737
1913
|
// deploying it without them only produces a container that crash-loops
|
|
@@ -2010,8 +2186,33 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2010
2186
|
const loginClientOtherInputsMissing = brokerMissingInputs.filter((m) => m !== 'ZITADEL_SERVICE_USER_TOKEN');
|
|
2011
2187
|
const freshRecordedIds = scope.recordedIds();
|
|
2012
2188
|
const freshLoginClientRecord = parseLoginClientRecord(freshRecordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]);
|
|
2013
|
-
|
|
2189
|
+
let loginClientNeedsMint = !freshLoginClientRecord ||
|
|
2014
2190
|
(await authLoginClientDriftedAt({ hostname, bearerRef: target.refs.instanceBearer }, deps, freshRecordedIds));
|
|
2191
|
+
// The in-app login app needs the same token. When the login already holds a live one, it is copied
|
|
2192
|
+
// (no new mint, so the login is not restarted) if the in-app login holds none or another one -- a re-mint
|
|
2193
|
+
// while the app was not delivered to leaves it a revoked token. When the login's cannot be read, the mint
|
|
2194
|
+
// below delivers it. The tokens are compared, never logged.
|
|
2195
|
+
if (signInApp && !loginClientNeedsMint) {
|
|
2196
|
+
const signInHeld = await currentEnvValue(deps.coolify, signInApp.appId, SERVICE_USER_TOKEN_ENV);
|
|
2197
|
+
const held = await currentEnvValue(deps.coolify, loginAppId, SERVICE_USER_TOKEN_ENV);
|
|
2198
|
+
if (held && held !== signInHeld) {
|
|
2199
|
+
runtime.onSecret?.(held);
|
|
2200
|
+
const attempts = runtime.deliverAttempts ?? 3;
|
|
2201
|
+
try {
|
|
2202
|
+
await retryPlatformAuthCall(attempts, sleep, () => deps.coolify.bulkUpsertEnvs(signInApp.appId, [
|
|
2203
|
+
{ key: SERVICE_USER_TOKEN_ENV, value: held },
|
|
2204
|
+
]));
|
|
2205
|
+
await retryPlatformAuthCall(attempts, sleep, () => deps.coolify.deploy(signInApp.appId, true));
|
|
2206
|
+
}
|
|
2207
|
+
catch {
|
|
2208
|
+
// Never echo the cause: Coolify's validation error can include the rejected value (the token).
|
|
2209
|
+
throw new Error(`${label}: the login token could not be copied to ${signInNames?.name} after ${attempts} tries`);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
else if (!held && !signInHeld) {
|
|
2213
|
+
loginClientNeedsMint = true;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2015
2216
|
if (loginClientNeedsMint && loginClientOtherInputsMissing.length === 0) {
|
|
2016
2217
|
// An own org never mints a second time on top of a token that may be live:
|
|
2017
2218
|
// the platform's re-mint revokes the old one, which would take the org's
|
|
@@ -2040,6 +2241,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2040
2241
|
// comment above gives.
|
|
2041
2242
|
await record(PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, loginClientUserId);
|
|
2042
2243
|
const deliverAttempts = runtime.deliverAttempts ?? 3;
|
|
2244
|
+
const receivers = signInApp
|
|
2245
|
+
? `the login app, the in-app login app (${signInNames?.name}) and broker`
|
|
2246
|
+
: 'the login app and broker';
|
|
2043
2247
|
try {
|
|
2044
2248
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.bulkUpsertEnvs(loginAppId, [
|
|
2045
2249
|
{ key: 'ZITADEL_SERVICE_USER_TOKEN', value: loginClientToken },
|
|
@@ -2047,6 +2251,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2047
2251
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.bulkUpsertEnvs(brokerAppId, [
|
|
2048
2252
|
{ key: 'ZITADEL_SERVICE_USER_TOKEN', value: loginClientToken },
|
|
2049
2253
|
]));
|
|
2254
|
+
if (signInApp) {
|
|
2255
|
+
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.bulkUpsertEnvs(signInApp.appId, [
|
|
2256
|
+
{ key: SERVICE_USER_TOKEN_ENV, value: loginClientToken },
|
|
2257
|
+
]));
|
|
2258
|
+
}
|
|
2050
2259
|
// Forced: an unforced deploy answers 200 "skipped" when one for the
|
|
2051
2260
|
// same commit is already queued -- likely here, since this function's
|
|
2052
2261
|
// own deploys above may just have queued one for either app. An
|
|
@@ -2055,6 +2264,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2055
2264
|
// (#216 follow-up review, item 2).
|
|
2056
2265
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.deploy(loginAppId, true));
|
|
2057
2266
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.deploy(brokerAppId, true));
|
|
2267
|
+
if (signInApp) {
|
|
2268
|
+
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.deploy(signInApp.appId, true));
|
|
2269
|
+
}
|
|
2058
2270
|
}
|
|
2059
2271
|
catch {
|
|
2060
2272
|
// Never echo the underlying error: Coolify's validation error can
|
|
@@ -2074,8 +2286,8 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2074
2286
|
// saying "its prior token was already revoked" there would describe a
|
|
2075
2287
|
// revoke that never happened, so the message only says so when it did.
|
|
2076
2288
|
throw new Error(revokedPriorPat
|
|
2077
|
-
? `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; its prior token was already revoked, so
|
|
2078
|
-
: `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; this is its first token, so
|
|
2289
|
+
? `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; its prior token was already revoked, so ${receivers} are down until a later apply mints and delivers a fresh one`
|
|
2290
|
+
: `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; this is its first token, so ${receivers} still have none until a later apply mints and delivers one`);
|
|
2079
2291
|
}
|
|
2080
2292
|
// #216 round 4/5: records what was actually delivered -- the user id,
|
|
2081
2293
|
// the exact PAT id, and (round 5) a hash of the exact token -- so the
|
|
@@ -2085,6 +2297,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2085
2297
|
// own doc comment, and `authLoginClientDrifted`'s, for why.
|
|
2086
2298
|
await record(PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, loginClientRecordValue(loginClientUserId, loginClientPatId, loginClientToken));
|
|
2087
2299
|
}
|
|
2300
|
+
// The in-app login app is ready once it is created, deployed (above) and holds the token: only then is
|
|
2301
|
+
// it recorded, so the login-host step plans no app host for a login that cannot serve it yet.
|
|
2302
|
+
if (signInApp && (await currentEnvValue(deps.coolify, signInApp.appId, SERVICE_USER_TOKEN_ENV))) {
|
|
2303
|
+
await record(SIGN_IN_LOGIN_APP_SCOPE_KEY, signInApp.appId);
|
|
2304
|
+
}
|
|
2088
2305
|
// ---- the owner company's own sign-in org ----------------------------------
|
|
2089
2306
|
// Archon signs people in from this org; the `foundry` org holds none. The
|
|
2090
2307
|
// company executor finds the same org by name (its slug) and makes no second.
|
|
@@ -2206,7 +2423,7 @@ const platformAuthProject = {
|
|
|
2206
2423
|
},
|
|
2207
2424
|
};
|
|
2208
2425
|
/** The platform's sign-in: {@link provisionAuthStack} with {@link platformTarget} and the platform's own inputs. */
|
|
2209
|
-
async function provisionPlatformAuth(context, deps, ownerCompany = null) {
|
|
2426
|
+
async function provisionPlatformAuth(context, deps, ownerCompany = null, signInLogin = false) {
|
|
2210
2427
|
const { platform } = context;
|
|
2211
2428
|
const scope = scopeFromPlatform(context);
|
|
2212
2429
|
return provisionAuthStack(scope, deps, platformTarget(), {
|
|
@@ -2218,6 +2435,8 @@ async function provisionPlatformAuth(context, deps, ownerCompany = null) {
|
|
|
2218
2435
|
smtpSender: `noreply@${platform.apex}`,
|
|
2219
2436
|
defaultRedirectUri: platform.archon.enabled ? `https://${platform.archon.hostname}` : null,
|
|
2220
2437
|
ownerCompany,
|
|
2438
|
+
// In-app sign-in: the second login app copies the login's host table (the host's port writes that).
|
|
2439
|
+
...(signInLogin ? { signInLogin: { bindings: null } } : {}),
|
|
2221
2440
|
});
|
|
2222
2441
|
}
|
|
2223
2442
|
/**
|
|
@@ -3469,8 +3688,6 @@ const PLATFORM_ARCHON_INTERNAL_NAME = 'platform-archon';
|
|
|
3469
3688
|
const PLATFORM_ARCHON_TUNNEL_NAME = 'platform-archon-tunnel';
|
|
3470
3689
|
/** #197: the one Coolify app that runs `cloudflared` for {@link PLATFORM_ARCHON_TUNNEL_NAME}. */
|
|
3471
3690
|
const PLATFORM_ARCHON_CLOUDFLARED_APP_NAME = 'platform-archon-cloudflared';
|
|
3472
|
-
/** https://github.com/wtfalch/archon: one Dockerfile at its root, `runner` stage on port 3000. */
|
|
3473
|
-
const ARCHON_REPO = 'wtfalch/archon';
|
|
3474
3691
|
const ARCHON_PORT = '3000';
|
|
3475
3692
|
/**
|
|
3476
3693
|
* #480: Archon's own health route (`@wtfalch/reporting`'s `healthHandler`,
|
|
@@ -3814,6 +4031,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3814
4031
|
const server = await resolvePlatformServer(deps.coolify);
|
|
3815
4032
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
3816
4033
|
return deps.coolify.createGithubAppApplication({
|
|
4034
|
+
is_preview_deployments_enabled: false,
|
|
3817
4035
|
project_uuid: projectUuid,
|
|
3818
4036
|
server_uuid: server.uuid,
|
|
3819
4037
|
environment_name: environment.name,
|
|
@@ -3850,6 +4068,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3850
4068
|
await deps.coolify.updateApplication(appId, {
|
|
3851
4069
|
custom_network_aliases: PLATFORM_ARCHON_INTERNAL_NAME,
|
|
3852
4070
|
is_consistent_container_name_enabled: false,
|
|
4071
|
+
// #788: previews off; `archonRollingUpdateDrifted` plans the step again if they come back on.
|
|
4072
|
+
is_preview_deployments_enabled: false,
|
|
3853
4073
|
// #543: the quartet below is the shared `healthCheckSettings` helper
|
|
3854
4074
|
// every other app foundry creates now also goes through -- Archon's own
|
|
3855
4075
|
// fields, unchanged in value, just no longer hand-rolled here.
|
|
@@ -3920,6 +4140,60 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3920
4140
|
await record(PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY, created.clientId);
|
|
3921
4141
|
clientId = created.clientId;
|
|
3922
4142
|
}
|
|
4143
|
+
// ---- in-app sign-in -----------------------------------------------------------
|
|
4144
|
+
// Archon signs people in on its own address, through the owner company's in-app login app. Wanted when
|
|
4145
|
+
// the company has the switch on and that login app is applied. In this order: trusted and bound, then
|
|
4146
|
+
// the env (`SIGNIN_UPSTREAM`) written and deployed, then the client's base URI, so the proxy is never
|
|
4147
|
+
// on for an address the login cannot serve and the base URI never points at an app that does not proxy.
|
|
4148
|
+
// Given up in the reverse order: the base URI back first, then the env, then the binding and the domain.
|
|
4149
|
+
const inAppWanted = Boolean(ownerSignIn.signInInApp && ownerSignIn.signInLoginAppId && ownerCompany);
|
|
4150
|
+
const archonAppZitadelId = context.recordedIds()[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
4151
|
+
const inAppOwed = archonInAppHost(context.recordedIds()) !== '';
|
|
4152
|
+
const inAppCalls = () => {
|
|
4153
|
+
const { ensureTrustedDomain, deleteTrustedDomain, updateOidcAppLoginBaseUri, allowPerAppLoginBaseUri, } = zitadel;
|
|
4154
|
+
if (!ensureTrustedDomain || !deleteTrustedDomain || !updateOidcAppLoginBaseUri) {
|
|
4155
|
+
throw new Error("platform:archon: the owner company's ZITADEL port lacks ensureTrustedDomain, deleteTrustedDomain or updateOidcAppLoginBaseUri, so Archon cannot sign in on its own address");
|
|
4156
|
+
}
|
|
4157
|
+
return {
|
|
4158
|
+
ensureTrustedDomain: ensureTrustedDomain.bind(zitadel),
|
|
4159
|
+
deleteTrustedDomain: deleteTrustedDomain.bind(zitadel),
|
|
4160
|
+
updateOidcAppLoginBaseUri: updateOidcAppLoginBaseUri.bind(zitadel),
|
|
4161
|
+
...(allowPerAppLoginBaseUri
|
|
4162
|
+
? { allowPerAppLoginBaseUri: allowPerAppLoginBaseUri.bind(zitadel) }
|
|
4163
|
+
: {}),
|
|
4164
|
+
};
|
|
4165
|
+
};
|
|
4166
|
+
const inAppTarget = {
|
|
4167
|
+
coolify: deps.coolify,
|
|
4168
|
+
loginHost: ownerSignIn.hostname,
|
|
4169
|
+
host: hostname,
|
|
4170
|
+
organisationId: authOrgId,
|
|
4171
|
+
projectId: project.id,
|
|
4172
|
+
appId: archonAppZitadelId ?? '',
|
|
4173
|
+
};
|
|
4174
|
+
if (inAppWanted && ownerSignIn.signInLoginAppId) {
|
|
4175
|
+
if (!archonAppZitadelId) {
|
|
4176
|
+
throw new Error("platform:archon: Archon's sign-in client is not recorded yet");
|
|
4177
|
+
}
|
|
4178
|
+
// A port that cannot allow a per-app base URI stops the step before anything is written.
|
|
4179
|
+
if (!zitadel.allowPerAppLoginBaseUri) {
|
|
4180
|
+
throw new Error("platform:archon: the owner company's ZITADEL port cannot allow a per-app login base URI, so Archon cannot sign in on its own address");
|
|
4181
|
+
}
|
|
4182
|
+
await archonInAppBind({
|
|
4183
|
+
...inAppTarget,
|
|
4184
|
+
zitadel: inAppCalls(),
|
|
4185
|
+
loginAppId: ownerSignIn.signInLoginAppId,
|
|
4186
|
+
clientId,
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4189
|
+
else if (inAppOwed) {
|
|
4190
|
+
// Given up: the client goes back to the company's login host before the app stops proxying.
|
|
4191
|
+
await archonInAppBack({
|
|
4192
|
+
...inAppTarget,
|
|
4193
|
+
host: archonInAppHost(context.recordedIds()),
|
|
4194
|
+
zitadel: inAppCalls(),
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
3923
4197
|
// ---- env ------------------------------------------------------------------
|
|
3924
4198
|
const envs = [
|
|
3925
4199
|
{ key: 'APP_URL', value: `https://${hostname}` },
|
|
@@ -3929,6 +4203,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3929
4203
|
{ key: 'AUTH_CLIENT_ID', value: clientId },
|
|
3930
4204
|
...(firstOwnerLink ? [{ key: 'ARCHON_FIRST_OWNER_LINK', value: firstOwnerLink }] : []),
|
|
3931
4205
|
];
|
|
4206
|
+
if (inAppWanted)
|
|
4207
|
+
envs.push({ key: SIGN_IN_UPSTREAM_ENV, value: signInUpstream(ownerCompany ?? undefined) });
|
|
3932
4208
|
// #265: an owner URL or reporting site already on the app is kept as it is.
|
|
3933
4209
|
// The owner URL must name the adopted database's host, or the app's two
|
|
3934
4210
|
// database URLs would point at different databases.
|
|
@@ -4103,7 +4379,13 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4103
4379
|
const staleOperator = stale.key === 'AUTHZ_BOOTSTRAP_OPERATOR';
|
|
4104
4380
|
// The link is written only while both ids are known; one that is no longer wanted goes.
|
|
4105
4381
|
const staleLink = !firstOwnerLink && stale.key === 'ARCHON_FIRST_OWNER_LINK';
|
|
4106
|
-
|
|
4382
|
+
// In-app sign-in switched off: the proxy to the login goes.
|
|
4383
|
+
const staleSignIn = !inAppWanted && stale.key === SIGN_IN_UPSTREAM_ENV;
|
|
4384
|
+
if ((stale.key === 'FILES_MANAGEMENT_KEY' ||
|
|
4385
|
+
staleUptime ||
|
|
4386
|
+
staleOperator ||
|
|
4387
|
+
staleLink ||
|
|
4388
|
+
staleSignIn) &&
|
|
4107
4389
|
stale.uuid) {
|
|
4108
4390
|
await deps.coolify.deleteEnv(appId, stale.uuid);
|
|
4109
4391
|
staleKeyDeleted = true;
|
|
@@ -4308,7 +4590,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4308
4590
|
ingress: [
|
|
4309
4591
|
{
|
|
4310
4592
|
hostname,
|
|
4311
|
-
|
|
4593
|
+
// Through the balancer once platform-archon runs blue/green and its balancer is written.
|
|
4594
|
+
service: tunnelOriginFor(archonSubject(platform, ARCHON_REPO), recorded, recorded.coolify_server, `http://${PLATFORM_ARCHON_INTERNAL_NAME}:${ARCHON_PORT}`),
|
|
4312
4595
|
},
|
|
4313
4596
|
{ service: 'http_status:404' },
|
|
4314
4597
|
],
|
|
@@ -4363,6 +4646,25 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4363
4646
|
if (archonDeployedNow && !selfRedeploy) {
|
|
4364
4647
|
await waitForHealthyVersion(appId, { kind: 'ready', url: `https://${hostname}/api/health` }, runtime, sleep, 'platform:archon', `archon stays at ${appliedGitRef ?? 'no recorded ref'} on ${recorded[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] ?? 'no recorded hostname'}`);
|
|
4365
4648
|
}
|
|
4649
|
+
// In-app sign-in, last half. The env is deployed and Archon answers health, or no deploy was owed (the
|
|
4650
|
+
// running env is the one written), so Archon now proxies `/sign-in/*` (on) or no longer does (off).
|
|
4651
|
+
// A host that deploys itself has not confirmed that yet: the next run does the rest, and until then the
|
|
4652
|
+
// marker stays as it was, so the step is planned again. Recorded last of its own calls.
|
|
4653
|
+
if (!archonDeployedNow || !selfRedeploy) {
|
|
4654
|
+
if (inAppWanted) {
|
|
4655
|
+
await archonInAppPoint({ ...inAppTarget, zitadel: inAppCalls() });
|
|
4656
|
+
await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, `${hostname}=${clientId}`);
|
|
4657
|
+
}
|
|
4658
|
+
else if (inAppOwed) {
|
|
4659
|
+
await archonInAppUnbind({
|
|
4660
|
+
...inAppTarget,
|
|
4661
|
+
host: archonInAppHost(context.recordedIds()),
|
|
4662
|
+
zitadel: inAppCalls(),
|
|
4663
|
+
...(ownerSignIn.signInLoginAppId ? { loginAppId: ownerSignIn.signInLoginAppId } : {}),
|
|
4664
|
+
});
|
|
4665
|
+
await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, '');
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4366
4668
|
// #398: the deployment finished and answers health, so Archon now runs with
|
|
4367
4669
|
// the key id its env carries. `provisionPlatformIntegrations` drops the
|
|
4368
4670
|
// previous public key only on this marker.
|
|
@@ -5158,7 +5460,8 @@ async function archonRollingUpdateDrifted(coolify, appId) {
|
|
|
5158
5460
|
const app = await coolify.getApplication(appId);
|
|
5159
5461
|
return (app.settings?.is_consistent_container_name_enabled === true ||
|
|
5160
5462
|
app.custom_network_aliases !== PLATFORM_ARCHON_INTERNAL_NAME ||
|
|
5161
|
-
app.health_check_enabled !== true
|
|
5463
|
+
app.health_check_enabled !== true ||
|
|
5464
|
+
previewsOn(app));
|
|
5162
5465
|
}
|
|
5163
5466
|
catch {
|
|
5164
5467
|
return false;
|
|
@@ -5770,6 +6073,7 @@ async function provisionPlatformAi(context, deps) {
|
|
|
5770
6073
|
const server = await resolvePlatformServer(coolify);
|
|
5771
6074
|
const githubApp = await coolify.requireGithubApp();
|
|
5772
6075
|
return coolify.createGithubAppApplication({
|
|
6076
|
+
is_preview_deployments_enabled: false,
|
|
5773
6077
|
project_uuid: projectUuid,
|
|
5774
6078
|
server_uuid: server.uuid,
|
|
5775
6079
|
environment_name: environment.name,
|
|
@@ -6314,6 +6618,31 @@ export async function findOrCreatePlatformApp(coolify, name, environment, create
|
|
|
6314
6618
|
const created = await create();
|
|
6315
6619
|
return { appId: created.uuid, justCreated: true, running: false };
|
|
6316
6620
|
}
|
|
6621
|
+
const SERVICE_USER_TOKEN_ENV = 'ZITADEL_SERVICE_USER_TOKEN';
|
|
6622
|
+
/**
|
|
6623
|
+
* {@link findOrCreatePlatformApp} for the in-app login app, which adopts only an app that is its own: one
|
|
6624
|
+
* of that name outside this environment, or built from another repository, is refused and left as it is.
|
|
6625
|
+
* The check reads the application list directly: a lookup by name through the limits wrapper would patch
|
|
6626
|
+
* the stranger's resource limits before this could refuse it.
|
|
6627
|
+
*/
|
|
6628
|
+
async function findOrCreateSignInLoginApp(coolify, input) {
|
|
6629
|
+
const { name, environment, label } = input;
|
|
6630
|
+
const listed = await coolify.raw('GET', '/applications');
|
|
6631
|
+
if (!Array.isArray(listed)) {
|
|
6632
|
+
throw new Error(`${label}: Coolify's application list could not be read, so ${name} is not touched`);
|
|
6633
|
+
}
|
|
6634
|
+
for (const app of listed) {
|
|
6635
|
+
if (app.name !== name)
|
|
6636
|
+
continue;
|
|
6637
|
+
if (app.environment_id !== environment.id) {
|
|
6638
|
+
throw new Error(`${label}: an application named ${name} exists outside the ${environment.name} environment of the sign-in project; foundry does not adopt it`);
|
|
6639
|
+
}
|
|
6640
|
+
if (app.git_repository !== undefined && !app.git_repository.endsWith(AUTH_REPO)) {
|
|
6641
|
+
throw new Error(`${label}: the application ${name} is built from ${app.git_repository}, not ${AUTH_REPO}; foundry does not adopt it`);
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
return findOrCreatePlatformApp(coolify, name, environment, input.create);
|
|
6645
|
+
}
|
|
6317
6646
|
/**
|
|
6318
6647
|
* #211: names of the envs the platform's sign-in broker (`wtfalch/auth`'s
|
|
6319
6648
|
* `broker/server.ts`) refuses to start without -- `ZITADEL_SERVICE_USER_TOKEN`/
|
|
@@ -6790,6 +7119,7 @@ export async function provisionFilesHost(scope, deps, target, input) {
|
|
|
6790
7119
|
const server = await resolvePlatformServer(deps.coolify);
|
|
6791
7120
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
6792
7121
|
return deps.coolify.createGithubAppApplication({
|
|
7122
|
+
is_preview_deployments_enabled: false,
|
|
6793
7123
|
project_uuid: projectUuid,
|
|
6794
7124
|
server_uuid: server.uuid,
|
|
6795
7125
|
environment_name: environment.name,
|
|
@@ -7183,6 +7513,7 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
7183
7513
|
const app = await findOrCreatePlatformApp(deps.coolify, EDGE_APP_NAME, environment, async () => {
|
|
7184
7514
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
7185
7515
|
return deps.coolify.createGithubAppApplication({
|
|
7516
|
+
is_preview_deployments_enabled: false,
|
|
7186
7517
|
project_uuid: projectUuid,
|
|
7187
7518
|
server_uuid: server.uuid,
|
|
7188
7519
|
environment_name: environment.name,
|
|
@@ -8082,6 +8413,7 @@ async function provisionPlatformIntegrations(action, context, deps) {
|
|
|
8082
8413
|
const server = await resolvePlatformServer(coolify);
|
|
8083
8414
|
const githubApp = await coolify.requireGithubApp();
|
|
8084
8415
|
return coolify.createGithubAppApplication({
|
|
8416
|
+
is_preview_deployments_enabled: false,
|
|
8085
8417
|
project_uuid: projectUuid,
|
|
8086
8418
|
server_uuid: server.uuid,
|
|
8087
8419
|
environment_name: environment.name,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ZitadelError } from '@wtfalch/providers/zitadel';
|
|
2
2
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
3
|
import { BOULE_MAIL_TOKEN_ENV, BOULE_MANAGED_ENV_KEYS } from './boule.js';
|
|
4
|
+
import { previewsOn } from './coolify-previews.js';
|
|
5
|
+
import { SIGN_IN_UPSTREAM_ENV } from './sign-in.js';
|
|
4
6
|
/**
|
|
5
7
|
* A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
|
|
6
8
|
* — the one place foundry records a provider's own id for an org today,
|
|
@@ -169,9 +171,10 @@ async function withHealthCheckConfig(client, resource, recorded) {
|
|
|
169
171
|
// above notes for `ports_exposes` -- `getApplication` returns the raw
|
|
170
172
|
// Coolify API response regardless.
|
|
171
173
|
const healthCheckEnabled = app.health_check_enabled;
|
|
174
|
+
// #788: the same read says whether pull-request previews are on.
|
|
172
175
|
return {
|
|
173
176
|
...resource,
|
|
174
|
-
config: { ...resource.config, healthCheckEnabled },
|
|
177
|
+
config: { ...resource.config, healthCheckEnabled, previewsEnabled: previewsOn(app) },
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
catch {
|
|
@@ -243,8 +246,9 @@ async function withAgoraEnvConfig(client, resource, recorded) {
|
|
|
243
246
|
]);
|
|
244
247
|
const env = {};
|
|
245
248
|
for (const e of envs) {
|
|
246
|
-
if (!e.is_preview && e.key.startsWith('AGORA_'))
|
|
249
|
+
if (!e.is_preview && (e.key.startsWith('AGORA_') || e.key === SIGN_IN_UPSTREAM_ENV)) {
|
|
247
250
|
env[e.key] = e.value;
|
|
251
|
+
}
|
|
248
252
|
}
|
|
249
253
|
// `@wtfalch/providers`' `CoolifyApplication` type does not declare
|
|
250
254
|
// `ports_exposes` (as of 0.7.3), but `getApplication` returns the raw
|