@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
package/dist/plan/diff.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
2
|
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, heldEnvRef, ownerMailboxRecorded, ownerMailboxes, ownsServices, } from '../org.js';
|
|
3
3
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostMissing, agoraHostnameFor, callsServiceHasName, callsServiceHasTokenEnv, callsTokenEnvMovedMessage, callsTokenHostFrom, chatCallsApplied, chatHasUpstreams, desiredAgoraEnv, withRecordedMessagingProject, } from './agora.js';
|
|
4
|
+
import { planBlueGreen } from './blue-green.js';
|
|
4
5
|
import { BOULE_BRIDGE_SECRET_ENV, BOULE_BROKER_ENTRY_KEY, BOULE_MAIL_TOKEN_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_SECRETS_RESTARTED_SCOPE_KEY, bouleEnvDrift, bouleMailTokenDrift, bouleMailWanted, desiredBouleEnv, wantedBouleBrokerEntry, } from './boule.js';
|
|
5
6
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
7
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
@@ -12,7 +13,7 @@ import { planFilesKeys } from './entry-files-key.js';
|
|
|
12
13
|
import { hasWildcardGate, planGates } from './gates.js';
|
|
13
14
|
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
14
15
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
15
|
-
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
16
|
+
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
16
17
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
17
18
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
18
19
|
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
@@ -23,7 +24,9 @@ import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_
|
|
|
23
24
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
24
25
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
25
26
|
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
27
|
+
import { planRemoteDatabase } from './remote-database.js';
|
|
26
28
|
import { ZITADEL_ORG_DOMAIN_BLOCKED_SCOPE_KEY, ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, signInDomainBlockedText, signInOrgDomain, } from './sign-in-org.js';
|
|
29
|
+
import { SIGN_IN_BASE_PATH, SIGN_IN_IMAGE_TAG_SUFFIX, SIGN_IN_IN_APP_SCOPE_KEY, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_LOGIN_INTERNAL_NAME, SIGN_IN_RATE_LIMIT_SCOPE_KEY, SIGN_IN_TRUSTED_SCOPE_KEY, inAppSignIn, inAppStatePairs, inAppTargets, } from './sign-in.js';
|
|
27
30
|
import { withStepSystem } from './step-system.js';
|
|
28
31
|
import { planWorkers } from './worker.js';
|
|
29
32
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
@@ -228,7 +231,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
228
231
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
229
232
|
// application. Apps created later in this run are not in the binding yet;
|
|
230
233
|
// the next run replans this step and repoints them.
|
|
231
|
-
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false);
|
|
234
|
+
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false, platformReadiness.signInLoginReady ?? false);
|
|
232
235
|
if (loginHost.step)
|
|
233
236
|
plannedSteps.push(loginHost.step);
|
|
234
237
|
// #444a: the org's one shared Postgres server and its fixed schema
|
|
@@ -455,6 +458,18 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
455
458
|
const edgeReservedLabels = planEdgeReservedLabels(org, recordedIds, platformReadiness, options.platformHostnames ?? []);
|
|
456
459
|
if (edgeReservedLabels.step)
|
|
457
460
|
plannedSteps.push(edgeReservedLabels.step);
|
|
461
|
+
// Blue/green (`blue-green.ts`): the apps and balancers before the tunnel routes to them, a second
|
|
462
|
+
// server's connector after the tunnel exists.
|
|
463
|
+
const blueGreen = planBlueGreen({
|
|
464
|
+
org,
|
|
465
|
+
recordedIds,
|
|
466
|
+
servers: options.blueGreenServers,
|
|
467
|
+
canHoldSecrets: platformReadiness.canHoldSecrets === true,
|
|
468
|
+
appPlanned: (entryKey) => plannedSteps.some((p) => p.action.system === 'coolify' &&
|
|
469
|
+
p.action.kind === 'app' &&
|
|
470
|
+
p.action.key === appStepKey(org, entryKey)),
|
|
471
|
+
});
|
|
472
|
+
plannedSteps.push(...blueGreen.steps.filter((x) => x.phase === 'before').map(blueGreenDraft()));
|
|
458
473
|
const tunnel = planTunnel(org, byKey, claimed, recordedIds, platformReadiness.platformApex);
|
|
459
474
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
460
475
|
// one never does.
|
|
@@ -481,6 +496,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
481
496
|
else if (tunnel) {
|
|
482
497
|
plannedSteps.push(tunnel);
|
|
483
498
|
}
|
|
499
|
+
plannedSteps.push(...blueGreen.steps.filter((x) => x.phase === 'after').map(blueGreenDraft()));
|
|
484
500
|
// Right after the zone (#43): `cloudflareRateLimit` resolves the zone by
|
|
485
501
|
// apex domain, so the zone step above must already have run (or the zone
|
|
486
502
|
// must already exist live) by the time this one's action executes.
|
|
@@ -548,6 +564,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
548
564
|
(jobsAvailable && !ownsServices(org)) || platformReadiness.jobsReady, platformReadiness.missingKeys.jobs, platformReadiness.noKeyReader, platformReadiness.jobsUrlMissing ?? false);
|
|
549
565
|
if (jobsRegistration.step)
|
|
550
566
|
plannedSteps.push(jobsRegistration.step);
|
|
567
|
+
const remoteDatabase = planRemoteDatabase(org, options.remoteDatabase, recordedIds);
|
|
568
|
+
if (remoteDatabase.step)
|
|
569
|
+
plannedSteps.push(remoteDatabase.step);
|
|
551
570
|
// F8 (#318), after Agora and Boule, not right after the zone: a route
|
|
552
571
|
// already bound by a hand-made entry, another entry's route, a taken
|
|
553
572
|
// script name, or no Workers token all throw at apply, and `runSteps`
|
|
@@ -614,6 +633,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
614
633
|
// orphan a second live key.
|
|
615
634
|
if (aiKey.blocked)
|
|
616
635
|
unmanaged.push({ description: aiKey.blocked, key: 'ai:key' });
|
|
636
|
+
unmanaged.push(...blueGreen.unmanaged);
|
|
617
637
|
unmanaged.push(...filesKeys.blocked);
|
|
618
638
|
unmanaged.push(...entryAiKeys.blocked);
|
|
619
639
|
unmanaged.push(...edgeWorkers.blocked);
|
|
@@ -643,6 +663,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
643
663
|
if (jobsRegistration.blocked) {
|
|
644
664
|
unmanaged.push({ description: jobsRegistration.blocked, key: 'jobs:registration' });
|
|
645
665
|
}
|
|
666
|
+
if (remoteDatabase.blocked)
|
|
667
|
+
unmanaged.push(remoteDatabase.blocked);
|
|
646
668
|
// #177: an org step whose own shared platform service is off or not yet
|
|
647
669
|
// applied is blocked here, not planned -- the same rule, now driven by
|
|
648
670
|
// `platformReadiness` instead of a host-supplied port. Each is reported at
|
|
@@ -739,6 +761,13 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
739
761
|
orgPostgres,
|
|
740
762
|
};
|
|
741
763
|
}
|
|
764
|
+
/** A blue/green plan step as the draft step `computePlanDetail` collects. */
|
|
765
|
+
function blueGreenDraft() {
|
|
766
|
+
return (step) => ({
|
|
767
|
+
action: { system: 'coolify', kind: step.kind, key: step.key, desired: step.desired },
|
|
768
|
+
step: { description: step.description, destroys: false, migrates: false },
|
|
769
|
+
});
|
|
770
|
+
}
|
|
742
771
|
/** Steps that run against the org's own ZITADEL instance (its projects, apps and broker entries). */
|
|
743
772
|
const NEEDS_OWN_ZITADEL = new Set([
|
|
744
773
|
'zitadel:issuer',
|
|
@@ -1085,13 +1114,21 @@ export function isPlatformOwnOrg(org, platformOwnerOrgId) {
|
|
|
1085
1114
|
* platform's own domain, never plans the shared ZITADEL instance's own login
|
|
1086
1115
|
* host as its own -- `auth.companyincluded.com` colliding with itself.
|
|
1087
1116
|
*/
|
|
1088
|
-
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired) {
|
|
1117
|
+
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired, signInLoginReady = false) {
|
|
1089
1118
|
const none = { step: null, blocked: null };
|
|
1090
1119
|
// An own org's instance has no shared login host to bind: its issuer is the sign-in service's own.
|
|
1120
|
+
// Only in-app sign-in plans a step for it: the app hosts, through its own in-app login app.
|
|
1091
1121
|
if (ownsServices(org))
|
|
1092
|
-
return
|
|
1122
|
+
return planOwnLoginHost(org, recordedIds);
|
|
1093
1123
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
1094
1124
|
const host = loginHostnameFor(org);
|
|
1125
|
+
// In-app sign-in: each app host gets its own binding, trusted domain and base URI. Until the second
|
|
1126
|
+
// login app that serves `/sign-in` is applied, no app host is planned (a blocked line says so).
|
|
1127
|
+
const wantedApps = inAppApps(org, recordedIds);
|
|
1128
|
+
const apps = signInLoginReady ? wantedApps : [];
|
|
1129
|
+
const inAppWaiting = wantedApps.length > 0 && !signInLoginReady
|
|
1130
|
+
? `${org.slug}'s in-app sign-in waits for the login app that serves ${SIGN_IN_BASE_PATH} (${SIGN_IN_LOGIN_INTERNAL_NAME}), which the platform has not applied`
|
|
1131
|
+
: null;
|
|
1095
1132
|
function stepFor() {
|
|
1096
1133
|
return {
|
|
1097
1134
|
step: {
|
|
@@ -1099,7 +1136,11 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1099
1136
|
system: 'zitadel',
|
|
1100
1137
|
kind: 'login-host',
|
|
1101
1138
|
key: org.id,
|
|
1102
|
-
desired:
|
|
1139
|
+
desired: apps.length > 0 ||
|
|
1140
|
+
recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ||
|
|
1141
|
+
recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY]
|
|
1142
|
+
? { host, apps }
|
|
1143
|
+
: { host },
|
|
1103
1144
|
},
|
|
1104
1145
|
step: {
|
|
1105
1146
|
description: `set ${org.slug}'s login host to ${host}`,
|
|
@@ -1107,7 +1148,7 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1107
1148
|
migrates: false,
|
|
1108
1149
|
},
|
|
1109
1150
|
},
|
|
1110
|
-
blocked:
|
|
1151
|
+
blocked: inAppWaiting,
|
|
1111
1152
|
};
|
|
1112
1153
|
}
|
|
1113
1154
|
// Already applied -- a re-run plans nothing unless the host changed, an app
|
|
@@ -1116,11 +1157,14 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1116
1157
|
// `zitadelLoginHost` always advances the recorded host to the current one,
|
|
1117
1158
|
// so a pending removal is the only way to know one is still owed, and
|
|
1118
1159
|
// nothing else here would replan the step to retry it.
|
|
1160
|
+
const targets = inAppTargets(apps, recordedIds);
|
|
1119
1161
|
if (recordedIds[key] === host &&
|
|
1120
1162
|
recordedIds[LOGIN_BINDING_SCOPE_KEY] ===
|
|
1121
|
-
loginBindingState(host, loginHostClientIds(org, recordedIds)) &&
|
|
1163
|
+
loginBindingState(host, loginHostClientIds(org, recordedIds, inAppStatePairs(targets.active).map((pair) => pair.clientId))) &&
|
|
1164
|
+
(recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1165
|
+
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1122
1166
|
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1123
|
-
return
|
|
1167
|
+
return { step: null, blocked: inAppWaiting };
|
|
1124
1168
|
}
|
|
1125
1169
|
// #177: nothing to add a trusted domain to yet -- `planIssuer` already
|
|
1126
1170
|
// reports the platform's sign-in as blocked, so this step stays silent
|
|
@@ -1132,6 +1176,55 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
1132
1176
|
// repoint nothing until the port that writes the binding is wired.
|
|
1133
1177
|
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor();
|
|
1134
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* In-app sign-in for a company that runs its own services: each app host is trusted in the company's
|
|
1181
|
+
* own ZITADEL, bound in its in-app login app (`<slug>-auth-login-sign-in`) and given
|
|
1182
|
+
* `https://<app host>/sign-in/` as its Login V2 base URI. Planned once the company's own sign-in is
|
|
1183
|
+
* applied and that login app is recorded; until then one blocked line says what it waits for.
|
|
1184
|
+
* Archon's host is not here: the platform's Archon step owns it.
|
|
1185
|
+
*/
|
|
1186
|
+
function planOwnLoginHost(org, recordedIds) {
|
|
1187
|
+
const none = { step: null, blocked: null };
|
|
1188
|
+
// Nothing wanted and nothing owed.
|
|
1189
|
+
if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY])
|
|
1190
|
+
return none;
|
|
1191
|
+
// The company's own sign-in is reported on its own line (`coolify:own-auth`).
|
|
1192
|
+
if (!ownAuthApplied(org, recordedIds))
|
|
1193
|
+
return none;
|
|
1194
|
+
const host = ownHostnames(org).auth;
|
|
1195
|
+
const wantedApps = inAppApps(org, recordedIds);
|
|
1196
|
+
const ready = Boolean(recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY]);
|
|
1197
|
+
const apps = ready ? wantedApps : [];
|
|
1198
|
+
const waiting = wantedApps.length > 0 && !ready
|
|
1199
|
+
? `${org.slug}'s in-app sign-in waits for the login app that serves ${SIGN_IN_BASE_PATH} (${org.slug}-auth-login${SIGN_IN_IMAGE_TAG_SUFFIX}), which the company's own sign-in has not applied`
|
|
1200
|
+
: null;
|
|
1201
|
+
// The company's own login host needs nothing here, so only the app hosts decide whether work is owed.
|
|
1202
|
+
const targets = inAppTargets(apps, recordedIds);
|
|
1203
|
+
if ((recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
|
|
1204
|
+
(recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
|
|
1205
|
+
pendingRemovalHosts(recordedIds).length === 0) {
|
|
1206
|
+
return { step: null, blocked: waiting };
|
|
1207
|
+
}
|
|
1208
|
+
// Nothing to trust a domain in, or to bind in, before the org exists in the company's ZITADEL.
|
|
1209
|
+
if (!recordedIds.zitadel_org)
|
|
1210
|
+
return { step: null, blocked: waiting };
|
|
1211
|
+
return {
|
|
1212
|
+
step: {
|
|
1213
|
+
action: {
|
|
1214
|
+
system: 'zitadel',
|
|
1215
|
+
kind: 'login-host',
|
|
1216
|
+
key: org.id,
|
|
1217
|
+
desired: { host, apps },
|
|
1218
|
+
},
|
|
1219
|
+
step: {
|
|
1220
|
+
description: `set ${org.slug}'s app hosts to sign in on their own addresses`,
|
|
1221
|
+
destroys: false,
|
|
1222
|
+
migrates: false,
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
blocked: waiting,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1135
1228
|
/**
|
|
1136
1229
|
* Every sign-in app this org already has live (web or native, not api) gets
|
|
1137
1230
|
* "user info inside the ID token" switched on once: apps made before the
|
|
@@ -2905,6 +2998,21 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2905
2998
|
blocked,
|
|
2906
2999
|
};
|
|
2907
3000
|
}
|
|
3001
|
+
// #788: pull-request previews on a live app. `coolifyApp` patches them off
|
|
3002
|
+
// on every run, so replanning this one step is the whole repair.
|
|
3003
|
+
if (live.config.previewsEnabled === true) {
|
|
3004
|
+
return {
|
|
3005
|
+
step: {
|
|
3006
|
+
action,
|
|
3007
|
+
step: {
|
|
3008
|
+
description: `turn Coolify's preview deployments off for ${org.slug}'s ${label} (#788)`,
|
|
3009
|
+
destroys: false,
|
|
3010
|
+
migrates: false,
|
|
3011
|
+
},
|
|
3012
|
+
},
|
|
3013
|
+
blocked,
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
2908
3016
|
// #239 (rule 4), from live state: an app with a database but no
|
|
2909
3017
|
// `DATABASE_URL_OWNER` env still has the pre-split superuser URL, or never
|
|
2910
3018
|
// got the push. Names only, never values. A reader that did not list env
|
|
@@ -3256,7 +3364,7 @@ function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
|
3256
3364
|
(org.calls.enabled && org.calls.usesDomain && Boolean(org.domain)) ||
|
|
3257
3365
|
Boolean(org.domain) ||
|
|
3258
3366
|
// A company without a domain still gets a tunnel of its own for an entry at `<slug>.oikos.host`.
|
|
3259
|
-
entryRoutes(org).length > 0;
|
|
3367
|
+
entryRoutes(org, recordedIds).length > 0;
|
|
3260
3368
|
const toggled = planHostingToggle('cloudflare', 'tunnel', desired, org, byKey, claimed, () => `provision a Cloudflare tunnel for ${org.slug}'s app`, () => `park ${org.slug}'s Cloudflare tunnel (configuration is retained, not deleted -- #7 corrections 1)`, org.id,
|
|
3261
3369
|
// Cloudflare's status, read against what is declared: a park cannot change
|
|
3262
3370
|
// it, so judging on/off by status alone would never let the plan empty.
|
|
@@ -3313,7 +3421,8 @@ function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
|
3313
3421
|
// An entry's hostname goes straight to its container, so a changed port, key or name, or an ingress
|
|
3314
3422
|
// written before entries were routed that way, needs the ingress written again. Nothing recorded
|
|
3315
3423
|
// equals no routes.
|
|
3316
|
-
const routesStale = (recordedIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY] ?? '[]') !==
|
|
3424
|
+
const routesStale = (recordedIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY] ?? '[]') !==
|
|
3425
|
+
entryRoutesMarker(org, recordedIds);
|
|
3317
3426
|
if (!connectorMissing && !dnsStale && !routesStale)
|
|
3318
3427
|
return null;
|
|
3319
3428
|
return {
|
|
@@ -3738,8 +3847,11 @@ function accessRuleDescription(org, rule) {
|
|
|
3738
3847
|
function planRateLimit(org, recordedIds) {
|
|
3739
3848
|
if (!(org.hosting.usesDomain || org.site.enabled) || !org.domain)
|
|
3740
3849
|
return null;
|
|
3741
|
-
|
|
3850
|
+
// Recorded, and the rule is the variant the switch asks for (the in-app one also covers POSTs under `/sign-in/`).
|
|
3851
|
+
if (recordedIds[resourceKey('cloudflare', 'ratelimit', org.id)] &&
|
|
3852
|
+
(recordedIds[SIGN_IN_RATE_LIMIT_SCOPE_KEY] === '1') === inAppSignIn(org)) {
|
|
3742
3853
|
return null;
|
|
3854
|
+
}
|
|
3743
3855
|
return {
|
|
3744
3856
|
action: { system: 'cloudflare', kind: 'ratelimit', key: org.id, desired: {} },
|
|
3745
3857
|
step: {
|
|
@@ -29,6 +29,11 @@ export interface RateLimitPort {
|
|
|
29
29
|
createRateLimitRule(zoneId: string, rule: RateLimitRuleInput): Promise<{
|
|
30
30
|
id: string;
|
|
31
31
|
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Deletes that one rule. Absent on a port that cannot; a step that needs it then fails by name.
|
|
34
|
+
* Rejects with a not-found error when the rule is already gone.
|
|
35
|
+
*/
|
|
36
|
+
deleteRateLimitRule?(zoneId: string, id: string): Promise<void>;
|
|
32
37
|
}
|
|
33
38
|
/** Edge answers a create another create of the same rule is making `conflict` + `busy`. */
|
|
34
39
|
export declare const EDGE_RATELIMITS_BUSY_ATTEMPTS = 5;
|
|
@@ -45,14 +45,16 @@ export function createEdgeRateLimits(client, options = {}) {
|
|
|
45
45
|
return;
|
|
46
46
|
const { rules } = await retrying(() => client.list({ zoneId, description }));
|
|
47
47
|
const view = rules.find((r) => r.description === description && r.state !== 'removed');
|
|
48
|
-
if (view && view.owner === null)
|
|
49
|
-
|
|
50
|
-
if (result.skipped.length > 0) {
|
|
51
|
-
throw new Error(`edge rate limits: rule '${description}' in zone ${zoneId} cannot be adopted (${result.skipped[0]?.reason}); remove or fix it by hand`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
48
|
+
if (view && view.owner === null)
|
|
49
|
+
await adoptView(zoneId, view);
|
|
54
50
|
adopted.add(key);
|
|
55
51
|
};
|
|
52
|
+
const adoptView = async (zoneId, view) => {
|
|
53
|
+
const result = await writeSlot(() => retrying(() => client.adopt({ rules: [{ zoneId, description: view.description ?? '', id: view.id }] })));
|
|
54
|
+
if (result.skipped.length > 0) {
|
|
55
|
+
throw new Error(`edge rate limits: rule '${view.description}' in zone ${zoneId} cannot be adopted (${result.skipped[0]?.reason}); remove or fix it by hand`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
56
58
|
return {
|
|
57
59
|
async listRateLimitRules(zoneId) {
|
|
58
60
|
const { rules } = await retrying(() => client.list({ zoneId }));
|
|
@@ -60,6 +62,14 @@ export function createEdgeRateLimits(client, options = {}) {
|
|
|
60
62
|
.filter((r) => r.state !== 'removed')
|
|
61
63
|
.map((r) => ({ id: r.id, description: r.description }));
|
|
62
64
|
},
|
|
65
|
+
async deleteRateLimitRule(zoneId, id) {
|
|
66
|
+
// Edge removes only a rule it owns: a live rule it has no row for is adopted first, as a create does.
|
|
67
|
+
const { rules } = await retrying(() => client.list({ zoneId }));
|
|
68
|
+
const view = rules.find((r) => r.id === id && r.state !== 'removed');
|
|
69
|
+
if (view && view.owner === null)
|
|
70
|
+
await adoptView(zoneId, view);
|
|
71
|
+
await writeSlot(() => retrying(() => client.remove(zoneId, id)));
|
|
72
|
+
},
|
|
63
73
|
async createRateLimitRule(zoneId, rule) {
|
|
64
74
|
await adoptLive(zoneId, rule.description);
|
|
65
75
|
const create = () => writeSlot(() => retrying(() => client.create({ zoneId, ...rule })));
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OrgMailOptions, OrgMailOrg, OrgMailResult } from '@wtfalch/email/p
|
|
|
2
2
|
import { type CallsSfuUrls } from '../calls-sfu.js';
|
|
3
3
|
import type { DeclaredOrg, HostingBuildPack } from '../org.js';
|
|
4
4
|
import type { DeclaredOrgStore } from '../store.js';
|
|
5
|
+
import { type BlueGreenServer, type BlueGreenSubject } from './blue-green.js';
|
|
5
6
|
import { type BrokerPort } from './broker-entries.js';
|
|
6
7
|
import { type DeployHealthRuntime } from './deploy-health.js';
|
|
7
8
|
import { type StepAction } from './diff.js';
|
|
@@ -16,6 +17,7 @@ import { type OwnServicesPort } from './own-services.js';
|
|
|
16
17
|
import type { DestroyTarget, StepDestroyer, StepExecutionContext, StepExecutor } from './plan-apply.js';
|
|
17
18
|
import type { EdgePortName } from './platform-org.js';
|
|
18
19
|
import type { RequestHeaderPort } from './platform-providers.js';
|
|
20
|
+
import { type ReleaseContext, type ReleaseHost } from './release-host.js';
|
|
19
21
|
export declare function coolifyProjectMark(org: Pick<DeclaredOrg, 'id'>): string;
|
|
20
22
|
/**
|
|
21
23
|
* The Coolify calls this executor makes, shaped to match
|
|
@@ -103,11 +105,16 @@ export interface CoolifyPort {
|
|
|
103
105
|
*/
|
|
104
106
|
getApplication(uuid: string): Promise<{
|
|
105
107
|
uuid: string;
|
|
108
|
+
/** Coolify's name for the app; a blue/green entry's recorded app is its blue copy only when this is the blue name. */
|
|
109
|
+
name?: string;
|
|
106
110
|
status?: string;
|
|
107
111
|
custom_labels?: string | null;
|
|
108
112
|
settings?: {
|
|
109
113
|
is_consistent_container_name_enabled?: boolean;
|
|
114
|
+
is_preview_deployments_enabled?: boolean;
|
|
110
115
|
};
|
|
116
|
+
/** #788: where Coolify reports previews is not documented, so `previewsOn` reads both places. */
|
|
117
|
+
is_preview_deployments_enabled?: boolean;
|
|
111
118
|
custom_network_aliases?: string | null;
|
|
112
119
|
health_check_enabled?: boolean;
|
|
113
120
|
ports_exposes?: string;
|
|
@@ -120,6 +127,8 @@ export interface CoolifyPort {
|
|
|
120
127
|
custom_docker_run_options?: string | null;
|
|
121
128
|
}>;
|
|
122
129
|
createGithubAppApplication(input: {
|
|
130
|
+
/** #788: previews off; every git app create sends `false`. */
|
|
131
|
+
is_preview_deployments_enabled?: boolean;
|
|
123
132
|
project_uuid: string;
|
|
124
133
|
server_uuid: string;
|
|
125
134
|
/** The Docker network the app joins (`plan/destinations.ts`); absent: the server's default. */
|
|
@@ -1140,6 +1149,14 @@ export interface ZitadelPort {
|
|
|
1140
1149
|
* the binding holds; `zitadelApp` never sets it at creation.
|
|
1141
1150
|
*/
|
|
1142
1151
|
updateOidcAppLoginBaseUri(orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|
|
1152
|
+
/**
|
|
1153
|
+
* Lets each app's own Login V2 base URI count: foundry starts ZITADEL with `LOGINV2_REQUIRED=true`,
|
|
1154
|
+
* which makes the instance ignore it. The host sets the instance feature (`PUT /v2/features/instance`
|
|
1155
|
+
* with `loginV2.required` false and the instance default as `baseUri`). Called before an app is
|
|
1156
|
+
* pointed at its own `/sign-in/` on a company that runs its own ZITADEL; the shared instance's
|
|
1157
|
+
* bindings port does the same for the platform's. Optional: absent, nothing is called.
|
|
1158
|
+
*/
|
|
1159
|
+
allowPerAppLoginBaseUri?(instanceBaseUri: string): Promise<void>;
|
|
1143
1160
|
/**
|
|
1144
1161
|
* Occupies `userName` in `orgId` with a machine user that has no role and
|
|
1145
1162
|
* no token, so nobody can register under that name. Idempotent: a user
|
|
@@ -2006,6 +2023,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2006
2023
|
* client id that binding holds. Apps created later are not in it yet:
|
|
2007
2024
|
* the next run replans (`diff.ts`'s `planLoginHost`) and moves them.
|
|
2008
2025
|
*
|
|
2026
|
+
* In-app sign-in (`signIn.inApp`, `plan/sign-in.ts` has the order): an app host is trusted first. It is
|
|
2027
|
+
* bound, and its app pointed at `/sign-in/`, only once its app runs `SIGNIN_UPSTREAM` (the app step
|
|
2028
|
+
* records that). The bind moves the app's client id off the company's binding in the same write (the
|
|
2029
|
+
* login refuses a client id in two bindings); a company binding left with no client is removed.
|
|
2030
|
+
* Switched off, the client id goes back and the base URI returns to the login host, and a trusted
|
|
2031
|
+
* domain is given up only after the app dropped the env.
|
|
2032
|
+
*
|
|
2009
2033
|
* #274: when the org's host has moved (`recorded[key]`, the *previous*
|
|
2010
2034
|
* run's resourceId, differs from this run's `host`), also removes every
|
|
2011
2035
|
* host that is no longer the org's current one from both places --
|
|
@@ -2067,6 +2091,12 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2067
2091
|
* the snapshot taken before the repoint still named it.
|
|
2068
2092
|
*/
|
|
2069
2093
|
private zitadelLoginHost;
|
|
2094
|
+
/**
|
|
2095
|
+
* The host table of a company's own in-app login app as a bindings port that collects its edits and
|
|
2096
|
+
* writes them once on `flush` (one redeploy, none when nothing changed). Used for a company that runs
|
|
2097
|
+
* its own services, which has no host-supplied bindings port.
|
|
2098
|
+
*/
|
|
2099
|
+
private ownSignInBindings;
|
|
2070
2100
|
/**
|
|
2071
2101
|
* `plan/diff.ts`'s `planCustomers` (plan gap A9): the org's customers sign
|
|
2072
2102
|
* in through a second ZITADEL org, `<slug> customers`, with
|
|
@@ -2115,6 +2145,34 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2115
2145
|
* actually be created on.
|
|
2116
2146
|
*/
|
|
2117
2147
|
private resolveServer;
|
|
2148
|
+
/**
|
|
2149
|
+
* Contract C3: every usable, reachable Coolify server, the org's primary first. The primary is
|
|
2150
|
+
* {@link resolveServer}'s (databases and single-copy services stay on it, unchanged). A server that is
|
|
2151
|
+
* down is named in `down` and left out of `servers`; it is not an error.
|
|
2152
|
+
*/
|
|
2153
|
+
resolveServers(org: DeclaredOrg, context: StepExecutionContext): Promise<{
|
|
2154
|
+
primary: {
|
|
2155
|
+
uuid: string;
|
|
2156
|
+
};
|
|
2157
|
+
servers: Array<{
|
|
2158
|
+
uuid: string;
|
|
2159
|
+
}>;
|
|
2160
|
+
down: string[];
|
|
2161
|
+
}>;
|
|
2162
|
+
/**
|
|
2163
|
+
* The release runner's outside calls for one blue/green entry (`plan/release-host.ts`), or `null` while its
|
|
2164
|
+
* blue copy is not recorded. The health token is only read here, never minted.
|
|
2165
|
+
*/
|
|
2166
|
+
releaseHost(org: DeclaredOrg, subject: BlueGreenSubject, context: ReleaseContext, renew: () => Promise<void>): ReleaseHost | null;
|
|
2167
|
+
/** Every server Coolify lists with whether it is usable and reachable, for the planner. */
|
|
2168
|
+
blueGreenServers(): Promise<BlueGreenServer[]>;
|
|
2169
|
+
/**
|
|
2170
|
+
* The blue/green steps (`plan/blue-green.ts`): `bluegreen` makes the apps and the balancer on one server,
|
|
2171
|
+
* `bluegreen-lb` only writes the balancer's weights, `bluegreen-connector` runs the tunnel's connector on a
|
|
2172
|
+
* server. A server that went down since the plan is skipped by `bluegreen-connector`'s caller, not here:
|
|
2173
|
+
* a step for it fails, which the next plan reports instead.
|
|
2174
|
+
*/
|
|
2175
|
+
private coolifyBlueGreen;
|
|
2118
2176
|
private coolifyApp;
|
|
2119
2177
|
/**
|
|
2120
2178
|
* #75: mounts a persistent volume at {@link LOG_DIR} on the app, unless one
|
|
@@ -2825,6 +2883,18 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2825
2883
|
* behind it.
|
|
2826
2884
|
*/
|
|
2827
2885
|
private coolifyAgora;
|
|
2886
|
+
/**
|
|
2887
|
+
* Records whether an app's running env holds `SIGNIN_UPSTREAM` (the app's host while it does, `''` once it
|
|
2888
|
+
* does not), so the login-host step knows when to point the base URI at the app and when it may give the
|
|
2889
|
+
* host up. Written only when the value changes.
|
|
2890
|
+
*/
|
|
2891
|
+
private recordSignInEnvLive;
|
|
2892
|
+
/**
|
|
2893
|
+
* In-app sign-in switched off (or its host given up): the app's live `SIGNIN_UPSTREAM` is deleted, because
|
|
2894
|
+
* `bulkUpsertEnvs` never removes a key. Left in place, the app would keep proxying `/sign-in/*` to the login.
|
|
2895
|
+
* Nothing happens while the env is desired or the app does not hold it.
|
|
2896
|
+
*/
|
|
2897
|
+
private dropStaleSignInUpstream;
|
|
2828
2898
|
/**
|
|
2829
2899
|
* `plan/diff.ts`'s `planBoule`: provisions the org's own Boule
|
|
2830
2900
|
* (https://github.com/wtfalch/boule) as one more Coolify application, from
|
|
@@ -2971,6 +3041,8 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2971
3041
|
*/
|
|
2972
3042
|
private ownSfu;
|
|
2973
3043
|
private jobsRegistration;
|
|
3044
|
+
/** The blue/green database route (`plan/remote-database.ts`). */
|
|
3045
|
+
private remoteDatabaseRoute;
|
|
2974
3046
|
/** `deliverSecret`, tried up to {@link JOBS_DELIVERY_ATTEMPTS} times; true once one attempt succeeds. */
|
|
2975
3047
|
private deliverWithRetry;
|
|
2976
3048
|
private cloudflareTunnel;
|
|
@@ -3433,3 +3505,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
3433
3505
|
* `:`. It never scans the path or the response body.
|
|
3434
3506
|
*/
|
|
3435
3507
|
export declare function isNotFoundError(error: unknown): boolean;
|
|
3508
|
+
/** True when ZITADEL refused an update because the value already is the wanted one. Matches the error code, never the free text. */
|
|
3509
|
+
export declare function isNoChangesError(error: unknown): boolean;
|
|
3510
|
+
/**
|
|
3511
|
+
* {@link ZitadelPort.updateOidcAppLoginBaseUri} that counts an app already holding the wanted base URI as
|
|
3512
|
+
* done: ZITADEL answers that write with "No changes" and a repeat apply must not fail on it.
|
|
3513
|
+
*/
|
|
3514
|
+
export declare function setOidcAppLoginBaseUri(zitadel: Pick<ZitadelPort, 'updateOidcAppLoginBaseUri'>, orgId: string, projectId: string, appId: string, loginBaseUri: string): Promise<void>;
|