@wtfalch/foundry 0.33.1 → 0.35.1
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/bootstrap.d.ts +1 -1
- package/dist/bootstrap/bootstrap.js +49 -11
- package/dist/bootstrap/declare.d.ts +12 -5
- package/dist/bootstrap/declare.js +33 -18
- package/dist/bootstrap/versions.d.ts +24 -0
- package/dist/bootstrap/versions.js +46 -0
- package/dist/calls-sfu.d.ts +10 -0
- package/dist/calls-sfu.js +44 -3
- package/dist/index.d.ts +21 -8
- package/dist/index.js +11 -4
- package/dist/migrations/0051_org_services.sql +13 -0
- package/dist/migrations/0052_platform_mail_relay.sql +8 -0
- package/dist/migrations/0053_heartbeats_provider.sql +9 -0
- package/dist/migrations/0053_org_version_pin.sql +6 -0
- package/dist/org.d.ts +76 -12
- package/dist/org.js +136 -20
- package/dist/plan/backup-gate.d.ts +93 -0
- package/dist/plan/backup-gate.js +261 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +20 -3
- package/dist/plan/build-record.d.ts +56 -0
- package/dist/plan/build-record.js +109 -0
- package/dist/plan/calls-matrix.d.ts +9 -0
- package/dist/plan/calls-matrix.js +18 -1
- package/dist/plan/cloudflared-connector.d.ts +7 -1
- package/dist/plan/cloudflared-connector.js +15 -2
- package/dist/plan/company-backups.d.ts +31 -0
- package/dist/plan/company-backups.js +101 -0
- package/dist/plan/deploy-health.d.ts +12 -2
- package/dist/plan/deploy-health.js +3 -1
- package/dist/plan/destinations.d.ts +73 -0
- package/dist/plan/destinations.js +217 -0
- package/dist/plan/diff.d.ts +46 -8
- package/dist/plan/diff.js +367 -126
- package/dist/plan/edge-access.d.ts +15 -0
- package/dist/plan/edge-access.js +21 -2
- package/dist/plan/executor.d.ts +276 -5
- package/dist/plan/executor.js +1111 -109
- package/dist/plan/health-check-settings.d.ts +28 -0
- package/dist/plan/health-check-settings.js +36 -0
- package/dist/plan/health-gaps.d.ts +33 -0
- package/dist/plan/health-gaps.js +89 -0
- package/dist/plan/health-targets.d.ts +1 -3
- package/dist/plan/health-targets.js +36 -36
- package/dist/plan/held-key-refs.d.ts +15 -0
- package/dist/plan/held-key-refs.js +15 -0
- package/dist/plan/hostnames.d.ts +10 -0
- package/dist/plan/hostnames.js +14 -0
- package/dist/plan/index.d.ts +19 -6
- package/dist/plan/index.js +10 -3
- package/dist/plan/live.d.ts +11 -2
- package/dist/plan/live.js +16 -2
- package/dist/plan/locked-hostnames.d.ts +7 -3
- package/dist/plan/locked-hostnames.js +25 -4
- package/dist/plan/login-host.d.ts +61 -12
- package/dist/plan/login-host.js +99 -11
- package/dist/plan/mail-relay.d.ts +139 -0
- package/dist/plan/mail-relay.js +400 -0
- package/dist/plan/network-move.d.ts +20 -0
- package/dist/plan/network-move.js +41 -0
- package/dist/plan/oikos-ingress.d.ts +111 -0
- package/dist/plan/oikos-ingress.js +231 -0
- package/dist/plan/org-names.d.ts +30 -0
- package/dist/plan/org-names.js +108 -0
- package/dist/plan/own-diff.d.ts +170 -0
- package/dist/plan/own-diff.js +440 -0
- package/dist/plan/own-dns.d.ts +6 -0
- package/dist/plan/own-dns.js +15 -0
- package/dist/plan/own-hostnames.d.ts +27 -0
- package/dist/plan/own-hostnames.js +24 -0
- package/dist/plan/own-limits.d.ts +213 -0
- package/dist/plan/own-limits.js +232 -0
- package/dist/plan/own-services.d.ts +193 -0
- package/dist/plan/own-services.js +169 -0
- package/dist/plan/own-sfu.d.ts +69 -0
- package/dist/plan/own-sfu.js +256 -0
- package/dist/plan/plan-apply.d.ts +121 -10
- package/dist/plan/plan-apply.js +357 -18
- package/dist/plan/platform-diff.d.ts +155 -11
- package/dist/plan/platform-diff.js +271 -66
- package/dist/plan/platform-org.d.ts +107 -4
- package/dist/plan/platform-org.js +175 -3
- package/dist/plan/platform-providers.d.ts +474 -81
- package/dist/plan/platform-providers.js +1407 -560
- package/dist/plan/proxy-labels.d.ts +57 -11
- package/dist/plan/proxy-labels.js +94 -14
- package/dist/plan/run-options.d.ts +19 -0
- package/dist/plan/run-options.js +46 -0
- package/dist/plan/sign-in-org.d.ts +1 -1
- package/dist/plan/sign-in-org.js +20 -2
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +77 -9
- package/dist/plan/zitadel-system-api.d.ts +22 -1
- package/dist/plan/zitadel-system-api.js +49 -8
- package/dist/platform.d.ts +40 -0
- package/dist/platform.js +16 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +3 -1
- package/dist/slug.d.ts +3 -1
- package/dist/slug.js +26 -3
- package/dist/store/postgres.js +93 -2
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.js +9 -0
- package/package.json +3 -3
- package/versions.digests.json +11 -0
- package/versions.json +3 -1
package/dist/plan/diff.js
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
|
-
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, ownerMailboxRecorded, ownerMailboxes, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, 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
4
|
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
5
|
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
6
|
import { chatInternalNames, chatUpstreams } from './chat-matrix.js';
|
|
7
|
+
import { planCompanyBackups } from './company-backups.js';
|
|
8
|
+
import { onPlatformNetwork } from './destinations.js';
|
|
7
9
|
import { planEntryAiKeys } from './entry-ai-key.js';
|
|
8
10
|
import { planEdgeReservedLabels, planEntryEdgeKeys } from './entry-edge-key.js';
|
|
9
11
|
import { planFilesKeys } from './entry-files-key.js';
|
|
10
12
|
import { hasWildcardGate, planGates } from './gates.js';
|
|
11
|
-
import { appHostname,
|
|
13
|
+
import { appHostname, tunnelDnsHostnames } from './hostnames.js';
|
|
12
14
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
13
|
-
import { LOGIN_BINDING_SCOPE_KEY, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
15
|
+
import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
|
|
14
16
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
15
|
-
import {
|
|
17
|
+
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
18
|
+
import { OWN_AUTH_BLOCKED_KEY, OWN_FILES_BLOCKED_KEY, OWN_FILES_R2_BLOCKED_KEY, OWN_JOBS_BLOCKED_KEY, OWN_MAIL_BLOCKED_KEY, OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, } from './own-diff.js';
|
|
19
|
+
import { ownDnsHostnames } from './own-dns.js';
|
|
20
|
+
import { ownHostnames } from './own-hostnames.js';
|
|
21
|
+
import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY, ORG_MAIL_APPLIED_APEX_SCOPE_KEY, ORG_MAIL_APPLIED_HOST_SCOPE_KEY, ORG_MAIL_APPLIED_MODE_SCOPE_KEY, ORG_MAIL_APPLIED_USES_DOMAIN_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, appEnvAppliedScopeKey, appFolderDiffers, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, serviceDbEnvAppliedScopeKey, versionNotAppliedLine, zitadelAppIdTokenUserinfoAppliedScopeKey, } from './platform-diff.js';
|
|
16
22
|
import { DEFAULT_ORG_PLATFORM_READINESS, } from './platform-org.js';
|
|
17
23
|
import { PLATFORM_AUTH_BROKER_MAIL_KEY_REF } from './platform-providers.js';
|
|
18
|
-
import { appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec } from './proxy-labels.js';
|
|
24
|
+
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryRoutes, entryRoutesMarker, } from './proxy-labels.js';
|
|
19
25
|
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';
|
|
20
26
|
import { withStepSystem } from './step-system.js';
|
|
21
27
|
import { planWorkers } from './worker.js';
|
|
@@ -56,6 +62,10 @@ function resourceKey(system, kind, key) {
|
|
|
56
62
|
function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
57
63
|
if (PROVIDER_KINDS[r.system]?.includes(r.kind) && recordedValues.has(r.key))
|
|
58
64
|
return true;
|
|
65
|
+
// The reader maps an applied own step's app onto its marker (`coolify:own-<service>:<org.id>`); only this org's id is its own.
|
|
66
|
+
if (r.system === 'coolify' && OWN_MARKER_KINDS.has(r.kind) && r.key === org.id) {
|
|
67
|
+
return Boolean(recordedIds[`coolify:${r.kind}:${org.id}`]);
|
|
68
|
+
}
|
|
59
69
|
if (r.system === 'coolify') {
|
|
60
70
|
if (r.kind === 'environment')
|
|
61
71
|
return true;
|
|
@@ -78,6 +88,11 @@ function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
|
78
88
|
if (type === 'CNAME' && content.endsWith('.cfargotunnel.com') && tunnelHostnames.includes(name)) {
|
|
79
89
|
return true;
|
|
80
90
|
}
|
|
91
|
+
if (type === 'CNAME' &&
|
|
92
|
+
content.endsWith('.cfargotunnel.com') &&
|
|
93
|
+
ownDnsHostnames(recordedIds).includes(name)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
81
96
|
const mailDomain = recordedIds[MAIL_DOMAIN_SCOPE_KEY]?.toLowerCase();
|
|
82
97
|
if (!mailDomain)
|
|
83
98
|
return false;
|
|
@@ -86,6 +101,14 @@ function madeByFoundry(org, r, recordedIds, recordedValues, platformNames) {
|
|
|
86
101
|
}
|
|
87
102
|
return name === `_dmarc.${mailDomain}` || name.endsWith(`._domainkey.${mailDomain}`);
|
|
88
103
|
}
|
|
104
|
+
const OWN_MARKER_KINDS = new Set([
|
|
105
|
+
'own-auth',
|
|
106
|
+
'own-mail',
|
|
107
|
+
'own-files',
|
|
108
|
+
'own-files-r2',
|
|
109
|
+
'own-jobs',
|
|
110
|
+
'own-sfu',
|
|
111
|
+
]);
|
|
89
112
|
/** The kinds a provider lists a resource under before a recorded `system:kind:key` maps it to foundry's own. */
|
|
90
113
|
const PROVIDER_KINDS = {
|
|
91
114
|
coolify: ['application', 'database'],
|
|
@@ -163,11 +186,22 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
163
186
|
const byKey = new Map(live.map((r) => [resourceKey(r.system, r.kind, r.key), r]));
|
|
164
187
|
const claimed = new Set();
|
|
165
188
|
const plannedSteps = [];
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
// An own org's sign-in comes before every step that signs in through it.
|
|
190
|
+
const ownAuth = planOwnAuth({
|
|
191
|
+
org,
|
|
192
|
+
recordedIds,
|
|
193
|
+
readiness: platformReadiness,
|
|
194
|
+
ownServices: options.ownServices ?? false,
|
|
195
|
+
zone: org.domain ? zoneReadiness(org.domain, byKey) : null,
|
|
196
|
+
oikosZoneReady: options.oikosZoneReady ?? false,
|
|
197
|
+
mailKeyHeld: options.ownMailKeyHeld ?? false,
|
|
198
|
+
mailKeyMintable: options.ownMailKeyMintable ?? false,
|
|
199
|
+
});
|
|
200
|
+
if (ownAuth.step)
|
|
201
|
+
plannedSteps.push(ownAuth.step);
|
|
202
|
+
// An own org's ZITADEL is the company's own instance: nothing that needs it plans before it exists.
|
|
203
|
+
const ownSignInPending = ownsServices(org) && !ownAuthApplied(org, recordedIds);
|
|
204
|
+
const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady);
|
|
171
205
|
if (issuer.step)
|
|
172
206
|
plannedSteps.push(issuer.step);
|
|
173
207
|
// Before the domain and the owners: no invite runs on an org still open to registration.
|
|
@@ -182,12 +216,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
182
216
|
// #267: after the ZITADEL org step, before any app's own issuer
|
|
183
217
|
// application. Apps created later in this run are not in the binding yet;
|
|
184
218
|
// the next run replans this step and repoints them.
|
|
185
|
-
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false
|
|
186
|
-
// #488: `agoraHost.issuer` is the shared ZITADEL instance's own issuer
|
|
187
|
-
// URL (`https://<platform.auth.hostname>`), the same value every org's
|
|
188
|
-
// sign-in already reads through `planOwners` above -- no separate port
|
|
189
|
-
// or plumbing needed to know the platform's own auth hostname.
|
|
190
|
-
platformAuthHostnameFrom('error' in agoraHost ? null : agoraHost.issuer), isPlatformOrg);
|
|
219
|
+
const loginHost = planLoginHost(org, recordedIds, platformReadiness.authReady, issuer.step !== null, options.loginHostBindings ?? false);
|
|
191
220
|
if (loginHost.step)
|
|
192
221
|
plannedSteps.push(loginHost.step);
|
|
193
222
|
// #444a: the org's one shared Postgres server and its fixed schema
|
|
@@ -199,6 +228,10 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
199
228
|
// is pushed just below, before mail.
|
|
200
229
|
const zone = planZone(org, byKey, claimed);
|
|
201
230
|
const zoneWaiting = zone.blocked !== null;
|
|
231
|
+
// A company without a domain has no zone of its own: it waits for the `oikos.host` zone at the platform's edge.
|
|
232
|
+
const ownZoneActive = org.domain
|
|
233
|
+
? zone.step === null && !zoneWaiting
|
|
234
|
+
: (options.oikosZoneReady ?? false);
|
|
202
235
|
const zoneBlocked = [];
|
|
203
236
|
// #555: steps that write DNS records, which go through edge.
|
|
204
237
|
const dnsBlocked = [];
|
|
@@ -213,7 +246,20 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
213
246
|
else if (zone.step) {
|
|
214
247
|
plannedSteps.push(zone.step);
|
|
215
248
|
}
|
|
216
|
-
const
|
|
249
|
+
const ownMail = planOwnMail({
|
|
250
|
+
org,
|
|
251
|
+
recordedIds,
|
|
252
|
+
readiness: platformReadiness,
|
|
253
|
+
ownServices: options.ownServices ?? false,
|
|
254
|
+
mailRelayApplied: options.mailRelayApplied ?? false,
|
|
255
|
+
zoneActive: ownZoneActive,
|
|
256
|
+
...(options.ownMailSmtpPort !== undefined ? { smtpPort: options.ownMailSmtpPort } : {}),
|
|
257
|
+
});
|
|
258
|
+
if (ownMail.step)
|
|
259
|
+
plannedSteps.push(ownMail.step);
|
|
260
|
+
const mail = planMail(org, byKey, claimed, recordedIds, platformReadiness.mailHost, platformReadiness.platformApex, platformReadiness.platformLettermintProjectId, options.mailRelayRoute === false
|
|
261
|
+
? `the mail relay has no route for ${ownHostnames(org).mailDomain} yet: apply the platform`
|
|
262
|
+
: undefined);
|
|
217
263
|
// #238: own-domain mail publishes DNS records into the zone, so it waits for
|
|
218
264
|
// an active zone -- blocked, not planned (rule 2).
|
|
219
265
|
if (mail.step && zoneWaiting && org.mail.usesDomain && org.domain) {
|
|
@@ -232,12 +278,25 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
232
278
|
const mailCopy = planMailCopy(org, byKey, recordedIds);
|
|
233
279
|
if (mailCopy)
|
|
234
280
|
plannedSteps.push(mailCopy);
|
|
281
|
+
const versionPin = org.versionPin;
|
|
235
282
|
const chat = planChat(org, byKey, claimed, recordedIds, agoraHost, issuer.step !== null);
|
|
236
283
|
if (chat.step)
|
|
237
284
|
plannedSteps.push(chat.step);
|
|
238
|
-
const calls = planCalls(org, byKey, claimed, platformReadiness.callsReady);
|
|
285
|
+
const calls = planCalls(org, byKey, claimed, platformReadiness.callsReady, versionPin?.calls);
|
|
239
286
|
if (calls.step)
|
|
240
287
|
plannedSteps.push(calls.step);
|
|
288
|
+
// An own org's media server, before the calls service that reads its addresses.
|
|
289
|
+
const ownSfu = planOwnSfu({
|
|
290
|
+
org,
|
|
291
|
+
recordedIds,
|
|
292
|
+
ownServices: options.ownServices ?? false,
|
|
293
|
+
zoneActive: ownZoneActive,
|
|
294
|
+
readiness: platformReadiness,
|
|
295
|
+
sfuPortsOpen: options.sfuPortsOpen ?? false,
|
|
296
|
+
keysHeld: options.ownSfuKeysHeld ?? false,
|
|
297
|
+
});
|
|
298
|
+
if (ownSfu.step)
|
|
299
|
+
plannedSteps.push(ownSfu.step);
|
|
241
300
|
// Right after the Matrix stack (#145): its own executor
|
|
242
301
|
// (`executor.ts`'s `coolifyCallsService`) reads the Synapse/MAS addresses
|
|
243
302
|
// and MAS client credentials `coolifyCalls` just recorded, the same
|
|
@@ -262,14 +321,35 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
262
321
|
const versionChanges = [];
|
|
263
322
|
if (org.calls.enabled && !calls.blocked) {
|
|
264
323
|
const callsAppLive = Boolean(byKey.get(resourceKey('coolify', 'calls', org.id)));
|
|
265
|
-
const synapseChange = versionNotAppliedLine('synapse', callsAppLive, recordedIds[CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.synapse);
|
|
324
|
+
const synapseChange = versionNotAppliedLine('synapse', callsAppLive, recordedIds[CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY], versionPin?.calls ?? VERSIONS.synapse);
|
|
266
325
|
if (synapseChange)
|
|
267
326
|
versionChanges.push(synapseChange);
|
|
268
327
|
const masChange = versionNotAppliedLine('mas', callsAppLive, recordedIds[CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.mas);
|
|
269
328
|
if (masChange)
|
|
270
329
|
versionChanges.push(masChange);
|
|
271
330
|
}
|
|
272
|
-
|
|
331
|
+
// An own org's files host, before its organisation on it.
|
|
332
|
+
const ownFiles = planOwnFiles({
|
|
333
|
+
org,
|
|
334
|
+
recordedIds,
|
|
335
|
+
readiness: platformReadiness,
|
|
336
|
+
ownServices: options.ownServices ?? false,
|
|
337
|
+
filesDeps: options.ownFilesDeps ?? { keys: false, bucket: false },
|
|
338
|
+
});
|
|
339
|
+
if (ownFiles.step)
|
|
340
|
+
plannedSteps.push(ownFiles.step);
|
|
341
|
+
// After the host is applied: its storage key is delivered to the running host.
|
|
342
|
+
const ownFilesR2 = options.ownFilesR2Ports
|
|
343
|
+
? planOwnFilesR2({
|
|
344
|
+
org,
|
|
345
|
+
recordedIds,
|
|
346
|
+
ownServices: options.ownServices ?? false,
|
|
347
|
+
ports: options.ownFilesR2Ports,
|
|
348
|
+
})
|
|
349
|
+
: { step: null, blocked: null };
|
|
350
|
+
if (ownFilesR2.step)
|
|
351
|
+
plannedSteps.push(ownFilesR2.step);
|
|
352
|
+
const filesOrganisation = planFilesOrganisation(org, byKey, claimed, platformReadiness.filesReady, platformReadiness.missingKeys.files, platformReadiness.noKeyReader, ownsServices(org) && !recordedIds[`coolify:own-files:${org.id}`]);
|
|
273
353
|
if (filesOrganisation.step)
|
|
274
354
|
plannedSteps.push(filesOrganisation.step);
|
|
275
355
|
const aiOrganisation = planAiOrganisation(org, byKey, claimed, platformReadiness.aiReady, platformReadiness.missingKeys.ai, platformReadiness.noKeyReader);
|
|
@@ -360,7 +440,7 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
360
440
|
const edgeReservedLabels = planEdgeReservedLabels(org, recordedIds, platformReadiness, options.platformHostnames ?? []);
|
|
361
441
|
if (edgeReservedLabels.step)
|
|
362
442
|
plannedSteps.push(edgeReservedLabels.step);
|
|
363
|
-
const tunnel = planTunnel(org, byKey, claimed, recordedIds,
|
|
443
|
+
const tunnel = planTunnel(org, byKey, claimed, recordedIds, platformReadiness.platformApex);
|
|
364
444
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
365
445
|
// one never does.
|
|
366
446
|
// #555: provisioning writes the tunnel config, the zone lookup and DNS; a
|
|
@@ -411,7 +491,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
411
491
|
const agoraGuarded = guardApp(agoraClient);
|
|
412
492
|
if (agoraGuarded)
|
|
413
493
|
plannedSteps.push(agoraGuarded);
|
|
414
|
-
const agora = planAgora(org, byKey, claimed, recordedIds, agoraHost
|
|
494
|
+
const agora = planAgora(org, byKey, claimed, recordedIds, agoraHost, {
|
|
495
|
+
servesDiscovery: options.loginHostServesDiscovery ?? false,
|
|
496
|
+
});
|
|
415
497
|
if (agora.step)
|
|
416
498
|
plannedSteps.push(agora.step);
|
|
417
499
|
// #161 (Boule part): every org's own Boule -- always on, no toggle, the same
|
|
@@ -433,10 +515,22 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
433
515
|
const bouleBroker = planBouleBrokerEntry(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
434
516
|
if (bouleBroker.step)
|
|
435
517
|
plannedSteps.push(bouleBroker.step);
|
|
518
|
+
// An own org's scheduler, before its registration below.
|
|
519
|
+
const ownJobs = planOwnJobs({
|
|
520
|
+
org,
|
|
521
|
+
recordedIds,
|
|
522
|
+
readiness: platformReadiness,
|
|
523
|
+
ownServices: options.ownServices ?? false,
|
|
524
|
+
tokenHeld: options.ownJobsTokenHeld ?? false,
|
|
525
|
+
});
|
|
526
|
+
if (ownJobs.step)
|
|
527
|
+
plannedSteps.push(ownJobs.step);
|
|
436
528
|
// #173: register the org's own app with the platform's jobs scheduler.
|
|
437
529
|
// After Agora, so a failing registration can never stop Agora from
|
|
438
530
|
// provisioning (`runSteps` stops at the first failing step).
|
|
439
|
-
const jobsRegistration = planJobsRegistration(org, recordedIds,
|
|
531
|
+
const jobsRegistration = planJobsRegistration(org, recordedIds,
|
|
532
|
+
// An own org never registers with the shared scheduler the host supplies.
|
|
533
|
+
(jobsAvailable && !ownsServices(org)) || platformReadiness.jobsReady, platformReadiness.missingKeys.jobs, platformReadiness.noKeyReader, platformReadiness.jobsUrlMissing ?? false);
|
|
440
534
|
if (jobsRegistration.step)
|
|
441
535
|
plannedSteps.push(jobsRegistration.step);
|
|
442
536
|
// F8 (#318), after Agora and Boule, not right after the zone: a route
|
|
@@ -476,6 +570,11 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
476
570
|
const recordedValues = new Set(Object.values(recordedIds));
|
|
477
571
|
for (const own of ['', org.id, org.slug, org.domain ?? ''])
|
|
478
572
|
recordedValues.delete(own);
|
|
573
|
+
// Archon's and the Access provider's project and client live in the owner company's ZITADEL.
|
|
574
|
+
if (platformReadiness.platformOwnerOrgId === org.id) {
|
|
575
|
+
for (const id of platformReadiness.platformZitadelIds ?? [])
|
|
576
|
+
recordedValues.add(id);
|
|
577
|
+
}
|
|
479
578
|
const platformNames = new Set([...(options.platformHostnames ?? []), platformReadiness.mailHost?.mailHost ?? ''].map((h) => h.toLowerCase()));
|
|
480
579
|
platformNames.delete('');
|
|
481
580
|
const unmanaged = live
|
|
@@ -489,6 +588,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
489
588
|
// item list, so it rides along with the unmanaged items.
|
|
490
589
|
if (agora.blocked)
|
|
491
590
|
unmanaged.push({ description: agora.blocked, key: 'coolify:agora' });
|
|
591
|
+
if (agora.issuerNote)
|
|
592
|
+
unmanaged.push({ description: agora.issuerNote, key: 'coolify:agora' });
|
|
492
593
|
if (boule.blocked)
|
|
493
594
|
unmanaged.push({ description: boule.blocked, key: 'coolify:boule' });
|
|
494
595
|
if (boule.mailBlocked) {
|
|
@@ -503,6 +604,22 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
503
604
|
unmanaged.push(...edgeWorkers.blocked);
|
|
504
605
|
unmanaged.push(...entryEdgeKeys.blocked);
|
|
505
606
|
unmanaged.push(...edgeReservedLabels.blocked);
|
|
607
|
+
if (ownAuth.blocked)
|
|
608
|
+
unmanaged.push({ description: ownAuth.blocked, key: OWN_AUTH_BLOCKED_KEY });
|
|
609
|
+
if (ownJobs.blocked)
|
|
610
|
+
unmanaged.push({ description: ownJobs.blocked, key: OWN_JOBS_BLOCKED_KEY });
|
|
611
|
+
if (ownMail.blocked)
|
|
612
|
+
unmanaged.push({ description: ownMail.blocked, key: OWN_MAIL_BLOCKED_KEY });
|
|
613
|
+
if (ownSfu.blocked)
|
|
614
|
+
unmanaged.push({ description: ownSfu.blocked, key: OWN_SFU_BLOCKED_KEY });
|
|
615
|
+
if (ownSfu.firewall)
|
|
616
|
+
unmanaged.push({ description: ownSfu.firewall, key: OWN_SFU_FIREWALL_KEY });
|
|
617
|
+
if (ownFiles.blocked) {
|
|
618
|
+
unmanaged.push({ description: ownFiles.blocked, key: OWN_FILES_BLOCKED_KEY });
|
|
619
|
+
}
|
|
620
|
+
if (ownFilesR2.blocked) {
|
|
621
|
+
unmanaged.push({ description: ownFilesR2.blocked, key: OWN_FILES_R2_BLOCKED_KEY });
|
|
622
|
+
}
|
|
506
623
|
// Same for a jobs registration that cannot run (no scheduler wired) or must
|
|
507
624
|
// not run again (minted, never delivered).
|
|
508
625
|
if (jobsRegistration.blocked) {
|
|
@@ -513,9 +630,20 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
513
630
|
// `platformReadiness` instead of a host-supplied port. Each is reported at
|
|
514
631
|
// most once even though a dependent step (owner, calls-service, storage,
|
|
515
632
|
// budget, key) would otherwise also have nothing to act on.
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
633
|
+
if (ownSignInPending) {
|
|
634
|
+
// The sign-in step's own reason, when it has one, already says why the rest waits.
|
|
635
|
+
if (!ownAuth.blocked) {
|
|
636
|
+
unmanaged.push({
|
|
637
|
+
description: `${org.slug}'s ZITADEL steps are blocked, not planned: its own sign-in service is not applied yet`,
|
|
638
|
+
key: 'coolify:own-auth',
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
if (issuer.blocked)
|
|
644
|
+
unmanaged.push({ description: issuer.blocked, key: 'zitadel:issuer' });
|
|
645
|
+
unmanaged.push(...owners.blocked);
|
|
646
|
+
}
|
|
519
647
|
const domainBlocked = orgDomainBlocked(org, recordedIds);
|
|
520
648
|
if (domainBlocked)
|
|
521
649
|
unmanaged.push(domainBlocked);
|
|
@@ -566,14 +694,50 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
566
694
|
unmanaged.push(...app.blocked);
|
|
567
695
|
unmanaged.push(...brokerEntries.blocked);
|
|
568
696
|
unmanaged.push(...bouleBroker.blocked);
|
|
697
|
+
// F6: last, so every database step above runs before the backup of its database.
|
|
698
|
+
const backups = planCompanyBackups(org, recordedIds, options.backupStorageUuid);
|
|
699
|
+
plannedSteps.push(...backups.steps);
|
|
700
|
+
unmanaged.push(...backups.blocked);
|
|
701
|
+
// A company built on the shared network plans the move and nothing else: its other steps would act on
|
|
702
|
+
// resources the move deletes.
|
|
703
|
+
// The platform's own company stays on the platform network, so it is never moved.
|
|
704
|
+
const ownerCompany = onPlatformNetwork(isPlatformOwnOrg(org, platformReadiness.platformOwnerOrgId));
|
|
705
|
+
const networkMove = ownerCompany ? null : planNetworkMove(org, recordedIds);
|
|
706
|
+
if (!ownerCompany && networkMoveNeeded(recordedIds) && hasAdoptedResources(recordedIds)) {
|
|
707
|
+
unmanaged.push({
|
|
708
|
+
description: `${org.slug}'s adopted app and database stay on the shared Docker network: foundry did not create them and does not move them`,
|
|
709
|
+
key: 'coolify:network',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
569
712
|
return {
|
|
570
713
|
orgId: org.id,
|
|
571
|
-
plannedSteps:
|
|
714
|
+
plannedSteps: (networkMove
|
|
715
|
+
? [networkMove]
|
|
716
|
+
: ownSignInPending
|
|
717
|
+
? plannedSteps.filter((s) => !NEEDS_OWN_ZITADEL.has(`${s.action.system}:${s.action.kind}`))
|
|
718
|
+
: plannedSteps).map(withStepSystem),
|
|
572
719
|
unmanaged,
|
|
573
720
|
versionChanges,
|
|
574
721
|
orgPostgres,
|
|
575
722
|
};
|
|
576
723
|
}
|
|
724
|
+
/** Steps that run against the org's own ZITADEL instance (its projects, apps and broker entries). */
|
|
725
|
+
const NEEDS_OWN_ZITADEL = new Set([
|
|
726
|
+
'zitadel:issuer',
|
|
727
|
+
'zitadel:registration-closed',
|
|
728
|
+
'zitadel:domain',
|
|
729
|
+
'zitadel:owner',
|
|
730
|
+
'zitadel:app',
|
|
731
|
+
'zitadel:customers',
|
|
732
|
+
'zitadel:id-token-userinfo',
|
|
733
|
+
'zitadel:login-host',
|
|
734
|
+
'coolify:chat',
|
|
735
|
+
'coolify:calls-service',
|
|
736
|
+
'coolify:calls-chat',
|
|
737
|
+
'coolify:broker-entry',
|
|
738
|
+
'coolify:boule-broker-entry',
|
|
739
|
+
'files:organisation',
|
|
740
|
+
]);
|
|
577
741
|
/**
|
|
578
742
|
* #553: a plain ZITADEL app provision (not a `recreate`) is planned only when
|
|
579
743
|
* the live listing lacks the app by name. If an app id is recorded for it,
|
|
@@ -643,7 +807,7 @@ function edgeBlockedItem(org, what, key, reason) {
|
|
|
643
807
|
* already exists live is unaffected: its own domain-move logic runs
|
|
644
808
|
* regardless, since that never re-provisions the ZITADEL org itself.
|
|
645
809
|
*/
|
|
646
|
-
function planIssuer(org, byKey, claimed, authReady
|
|
810
|
+
function planIssuer(org, byKey, claimed, authReady) {
|
|
647
811
|
const key = resourceKey('zitadel', 'issuer', org.id);
|
|
648
812
|
const live = byKey.get(key);
|
|
649
813
|
const desiredUsesDomain = org.issuer.usesDomain;
|
|
@@ -681,10 +845,8 @@ function planIssuer(org, byKey, claimed, authReady, isPlatformOrg = false) {
|
|
|
681
845
|
Boolean(org.domain) &&
|
|
682
846
|
liveHost !== null &&
|
|
683
847
|
liveHost !== loginHostnameFor(org);
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
if (isPlatformOrg ||
|
|
687
|
-
live.config.usesDomain === undefined ||
|
|
848
|
+
// A live issuer that does not say is not read as a different one either.
|
|
849
|
+
if (live.config.usesDomain === undefined ||
|
|
688
850
|
(Boolean(live.config.usesDomain) === desiredUsesDomain && !domainMoved)) {
|
|
689
851
|
return { step: null, blocked: null };
|
|
690
852
|
}
|
|
@@ -867,27 +1029,10 @@ function planOwners(org, recordedIds, authReady, authIssuer, ownerInviteMail, re
|
|
|
867
1029
|
});
|
|
868
1030
|
return { steps, blocked };
|
|
869
1031
|
}
|
|
870
|
-
/**
|
|
871
|
-
* #488: the bare hostname out of an issuer URL (`https://<hostname>`, the
|
|
872
|
-
* only shape {@link agoraHostValuesFromPlatform} or a host-supplied
|
|
873
|
-
* `AgoraHostPort` ever hands back), or `null` when there is no issuer to
|
|
874
|
-
* read one from. What {@link loginHostnameFor} compares the org's own
|
|
875
|
-
* computed `auth.<domain>` against, so the platform's own org never plans
|
|
876
|
-
* its login host equal to the shared ZITADEL instance's own.
|
|
877
|
-
*/
|
|
1032
|
+
/** True when `org` is the platform's owner company. */
|
|
878
1033
|
export function isPlatformOwnOrg(org, platformOwnerOrgId) {
|
|
879
1034
|
return Boolean(platformOwnerOrgId && org.id === platformOwnerOrgId);
|
|
880
1035
|
}
|
|
881
|
-
function platformAuthHostnameFrom(issuer) {
|
|
882
|
-
if (!issuer)
|
|
883
|
-
return null;
|
|
884
|
-
try {
|
|
885
|
-
return new URL(issuer).hostname;
|
|
886
|
-
}
|
|
887
|
-
catch {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
1036
|
/**
|
|
892
1037
|
* #267: the org's own company login host (`login-host.ts`'s
|
|
893
1038
|
* `loginHostnameFor`) -- `auth.<domain>` once the org has one,
|
|
@@ -924,51 +1069,24 @@ function platformAuthHostnameFrom(issuer) {
|
|
|
924
1069
|
* platform's own domain, never plans the shared ZITADEL instance's own login
|
|
925
1070
|
* host as its own -- `auth.companyincluded.com` colliding with itself.
|
|
926
1071
|
*/
|
|
927
|
-
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
1072
|
+
function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired) {
|
|
928
1073
|
const none = { step: null, blocked: null };
|
|
1074
|
+
// An own org's instance has no shared login host to bind: its issuer is the sign-in service's own.
|
|
1075
|
+
if (ownsServices(org))
|
|
1076
|
+
return none;
|
|
929
1077
|
const key = resourceKey('zitadel', 'login-host', org.id);
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
// Before that host is known, plan nothing: a guessed host would delete the
|
|
933
|
-
// working one, and a normal step would trust a domain `destroy` later deletes.
|
|
934
|
-
if (isPlatformOrg && platformAuthHostname === null) {
|
|
935
|
-
return {
|
|
936
|
-
step: null,
|
|
937
|
-
blocked: `${org.slug}'s login host is waiting for the platform auth host`,
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
const sharedInstanceHost = isPlatformOrg && platformAuthHostname !== null;
|
|
941
|
-
const host = sharedInstanceHost ? platformAuthHostname : loginHostnameFor(org);
|
|
942
|
-
// #488: the guard in `loginHostnameFor` fired -- the org's own domain host
|
|
943
|
-
// would have been the platform's shared ZITADEL instance host.
|
|
944
|
-
if (sharedInstanceHost) {
|
|
945
|
-
// The host is the shared instance's own: nothing to trust or bind, and an
|
|
946
|
-
// org with no recorded host already signs in there. Only an org recorded
|
|
947
|
-
// on another host (the old `<slug>-auth.oikos.host`) plans one step, to
|
|
948
|
-
// move its apps over and remove the old host; then it plans nothing.
|
|
949
|
-
const recordedHost = recordedIds[key];
|
|
950
|
-
const moving = recordedHost !== undefined && recordedHost !== host;
|
|
951
|
-
const removalOwed = pendingRemovalHosts(recordedIds).length > 0;
|
|
952
|
-
if (!authReady || !(moving || removalOwed))
|
|
953
|
-
return none;
|
|
954
|
-
// The same gates as the normal branch: the executor's shared step also
|
|
955
|
-
// needs the binding port and the ZITADEL org.
|
|
956
|
-
const waiting = loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned);
|
|
957
|
-
return waiting ?? stepFor(true);
|
|
958
|
-
}
|
|
959
|
-
function stepFor(shared) {
|
|
1078
|
+
const host = loginHostnameFor(org);
|
|
1079
|
+
function stepFor() {
|
|
960
1080
|
return {
|
|
961
1081
|
step: {
|
|
962
1082
|
action: {
|
|
963
1083
|
system: 'zitadel',
|
|
964
1084
|
kind: 'login-host',
|
|
965
1085
|
key: org.id,
|
|
966
|
-
desired:
|
|
1086
|
+
desired: { host },
|
|
967
1087
|
},
|
|
968
1088
|
step: {
|
|
969
|
-
description:
|
|
970
|
-
? `move ${org.slug}'s apps onto the platform's own sign-in host ${host}`
|
|
971
|
-
: `set ${org.slug}'s login host to ${host}`,
|
|
1089
|
+
description: `set ${org.slug}'s login host to ${host}`,
|
|
972
1090
|
destroys: false,
|
|
973
1091
|
migrates: false,
|
|
974
1092
|
},
|
|
@@ -996,7 +1114,7 @@ function planLoginHost(org, recordedIds, authReady, issuerPlanned, bindingsWired
|
|
|
996
1114
|
// Isolation between orgs rests on zitadel-login's binding, and repointing
|
|
997
1115
|
// an app at a host with no binding breaks its sign-in: trust nothing and
|
|
998
1116
|
// repoint nothing until the port that writes the binding is wired.
|
|
999
|
-
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor(
|
|
1117
|
+
return loginHostWaiting(org, host, recordedIds, bindingsWired, issuerPlanned) ?? stepFor();
|
|
1000
1118
|
}
|
|
1001
1119
|
/**
|
|
1002
1120
|
* Every sign-in app this org already has live (web or native, not api) gets
|
|
@@ -1126,7 +1244,9 @@ export const JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY = 'jobs_registration_undeli
|
|
|
1126
1244
|
*/
|
|
1127
1245
|
function planMail(org, byKey, claimed, recordedIds, mailHost,
|
|
1128
1246
|
/** #491 (review round 2): read off `platformReadiness` directly, not off `mailHost` -- see `OrgPlatformReadiness.platformApex`'s own doc comment for why. */
|
|
1129
|
-
platformApex, platformLettermintProjectId
|
|
1247
|
+
platformApex, platformLettermintProjectId,
|
|
1248
|
+
/** Why the MX cannot be published yet: the relay does not route this org's mail domain. */
|
|
1249
|
+
relayBlocked) {
|
|
1130
1250
|
const key = resourceKey('coolify', 'mail', org.id);
|
|
1131
1251
|
const live = byKey.get(key);
|
|
1132
1252
|
// `off` means mail is not foundry's concern here (org.ts's MailMode doc);
|
|
@@ -1142,7 +1262,14 @@ platformApex, platformLettermintProjectId) {
|
|
|
1142
1262
|
if (!mailHost) {
|
|
1143
1263
|
return {
|
|
1144
1264
|
step: null,
|
|
1145
|
-
blocked: `${org.slug}'s mail is blocked, not planned:
|
|
1265
|
+
blocked: `${org.slug}'s mail is blocked, not planned: its own mail server is not applied yet`,
|
|
1266
|
+
mailboxBlocked: null,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (relayBlocked) {
|
|
1270
|
+
return {
|
|
1271
|
+
step: null,
|
|
1272
|
+
blocked: `${org.slug}'s mail is blocked, not planned: ${relayBlocked}`,
|
|
1146
1273
|
mailboxBlocked: null,
|
|
1147
1274
|
};
|
|
1148
1275
|
}
|
|
@@ -1490,12 +1617,14 @@ function planChat(org, byKey, claimed, recordedIds, reading, issuerPlanned) {
|
|
|
1490
1617
|
* `sfu:livekit` step is declared. Parking, and an already-provisioned stack,
|
|
1491
1618
|
* are unaffected.
|
|
1492
1619
|
*/
|
|
1493
|
-
function planCalls(org, byKey, claimed, callsReady) {
|
|
1620
|
+
function planCalls(org, byKey, claimed, callsReady, synapseTag) {
|
|
1494
1621
|
const key = resourceKey('coolify', 'calls', org.id);
|
|
1495
1622
|
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
1496
1623
|
return {
|
|
1497
1624
|
step: null,
|
|
1498
|
-
blocked:
|
|
1625
|
+
blocked: ownsServices(org)
|
|
1626
|
+
? `${org.slug}'s calls Matrix stack is blocked, not planned: its own media server (coolify:own-sfu) is not applied yet`
|
|
1627
|
+
: `${org.slug}'s calls Matrix stack is blocked, not planned: the platform owner's calls SFU (the platform's) is off`,
|
|
1499
1628
|
};
|
|
1500
1629
|
}
|
|
1501
1630
|
// #248: the two images this stack's own apply would run, checked the same
|
|
@@ -1503,7 +1632,7 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
1503
1632
|
// own tag -- a re-apply of the same declaration must not silently run a
|
|
1504
1633
|
// different version with no diff shown anywhere.
|
|
1505
1634
|
if (org.calls.enabled && !byKey.get(key)) {
|
|
1506
|
-
const synapseBlocked = floatingTagBlocked('Synapse', VERSIONS.synapse, 'v1.161.0');
|
|
1635
|
+
const synapseBlocked = floatingTagBlocked('Synapse', synapseTag ?? VERSIONS.synapse, 'v1.161.0');
|
|
1507
1636
|
if (synapseBlocked) {
|
|
1508
1637
|
return {
|
|
1509
1638
|
step: null,
|
|
@@ -1518,10 +1647,12 @@ function planCalls(org, byKey, claimed, callsReady) {
|
|
|
1518
1647
|
};
|
|
1519
1648
|
}
|
|
1520
1649
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1650
|
+
const toggle = planHostingToggle('coolify', 'calls', org.calls.enabled, org, byKey, claimed, () => `provision ${org.slug}'s calls Matrix stack (Synapse + matrix-authentication-service)`, () => `park ${org.slug}'s calls Matrix stack (stops serving; data is retained, not deleted -- #7 corrections 1)`);
|
|
1651
|
+
// A held Synapse tag rides in the provisioning step; a parking step carries none.
|
|
1652
|
+
if (toggle && synapseTag && toggle.action.desired.enabled === true) {
|
|
1653
|
+
toggle.action.desired = { ...toggle.action.desired, synapseTag };
|
|
1654
|
+
}
|
|
1655
|
+
return { step: toggle, blocked: null };
|
|
1525
1656
|
}
|
|
1526
1657
|
/**
|
|
1527
1658
|
* The org's own calls service (#145, part 2 of #128) -- `wtfalch/calls`'s
|
|
@@ -1544,6 +1675,13 @@ function planCallsService(org, byKey, claimed, callsReady, recordedIds, callsBlo
|
|
|
1544
1675
|
if (org.calls.enabled && !byKey.get(key) && !callsReady) {
|
|
1545
1676
|
return { step: null, blocked: null };
|
|
1546
1677
|
}
|
|
1678
|
+
// An own org's service names its own media server, so it waits for it even when already provisioned.
|
|
1679
|
+
if (org.calls.enabled && ownsServices(org) && !callsReady) {
|
|
1680
|
+
return {
|
|
1681
|
+
step: null,
|
|
1682
|
+
blocked: `${org.slug}'s calls service is blocked, not planned: its own media server (coolify:own-sfu) is not applied yet`,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1547
1685
|
// The introspection secret is shown once. One minted but never written to
|
|
1548
1686
|
// the app is lost, so this reports it and mints no second (platform rule 1).
|
|
1549
1687
|
const undelivered = recordedIds[CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY];
|
|
@@ -1771,13 +1909,21 @@ function notReadyReason(service, keyName, missingKeyRef, noKeyReader) {
|
|
|
1771
1909
|
* Files organisation already exists -- the old origin is left allowed
|
|
1772
1910
|
* rather than silently dropped.
|
|
1773
1911
|
*/
|
|
1774
|
-
function planFilesOrganisation(org, byKey, claimed, filesReady, missingKeyRef, noKeyReader = false
|
|
1912
|
+
function planFilesOrganisation(org, byKey, claimed, filesReady, missingKeyRef, noKeyReader = false,
|
|
1913
|
+
/** An own org whose files host (`coolify:own-files:<org.id>`) is not recorded yet. */
|
|
1914
|
+
ownFilesPending = false) {
|
|
1775
1915
|
const key = resourceKey('files', 'organisation', org.id);
|
|
1776
1916
|
const live = byKey.get(key);
|
|
1777
1917
|
if (live) {
|
|
1778
1918
|
claimed.add(key);
|
|
1779
1919
|
return { step: null, blocked: null };
|
|
1780
1920
|
}
|
|
1921
|
+
if (ownFilesPending) {
|
|
1922
|
+
return {
|
|
1923
|
+
step: null,
|
|
1924
|
+
blocked: `${org.slug}'s Files organisation is blocked, not planned: its own files host is not applied yet`,
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1781
1927
|
if (!filesReady) {
|
|
1782
1928
|
return {
|
|
1783
1929
|
step: null,
|
|
@@ -2150,14 +2296,32 @@ function planAgoraClient(org, byKey, claimed) {
|
|
|
2150
2296
|
* message naming each missing value, which `computePlanDetail` lists with the
|
|
2151
2297
|
* plan's non-step items, so every other step still applies.
|
|
2152
2298
|
*/
|
|
2153
|
-
function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
2299
|
+
function planAgora(org, byKey, claimed, recordedIds, reading, issuerSwitch) {
|
|
2154
2300
|
const key = resourceKey('coolify', 'agora', org.id);
|
|
2155
2301
|
const live = byKey.get(key);
|
|
2156
2302
|
if (live)
|
|
2157
2303
|
claimed.add(key);
|
|
2304
|
+
const ownSignIn = ownsServices(org);
|
|
2305
|
+
if (ownSignIn && !ownAuthApplied(org, recordedIds)) {
|
|
2306
|
+
return {
|
|
2307
|
+
step: null,
|
|
2308
|
+
blocked: `${org.slug}'s Agora is blocked, not planned: its own sign-in service is not applied yet`,
|
|
2309
|
+
issuerNote: null,
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
// The files host records its internal URL only while it applies, and Agora's
|
|
2313
|
+
// env takes its files upstream from it: planned in the same pass, the first
|
|
2314
|
+
// deploy would start without one.
|
|
2315
|
+
if (ownSignIn && !recordedIds[PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY]) {
|
|
2316
|
+
return {
|
|
2317
|
+
step: null,
|
|
2318
|
+
blocked: `${org.slug}'s Agora is blocked, not planned: its own files host is not applied yet`,
|
|
2319
|
+
issuerNote: null,
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2158
2322
|
const error = 'error' in reading ? reading.error : null;
|
|
2159
|
-
const
|
|
2160
|
-
const missing = agoraHostMissing(
|
|
2323
|
+
const platformHost = withRecordedMessagingProject('error' in reading ? NO_AGORA_HOST_VALUES : reading, recordedIds);
|
|
2324
|
+
const missing = agoraHostMissing(platformHost);
|
|
2161
2325
|
if (missing.length > 0) {
|
|
2162
2326
|
// A fixed text, never the port's own message: this line is part of the
|
|
2163
2327
|
// stale-plan comparison, and an error that varies per call would refuse
|
|
@@ -2166,8 +2330,41 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2166
2330
|
return {
|
|
2167
2331
|
step: null,
|
|
2168
2332
|
blocked: `${org.slug}'s Agora is blocked, not planned: the host supplies no ${missing.join(', ')}${why}`,
|
|
2333
|
+
issuerNote: null,
|
|
2169
2334
|
};
|
|
2170
2335
|
}
|
|
2336
|
+
// Agora signs in at the company's own login host, and refuses any issuer off
|
|
2337
|
+
// its own address's domain. Only this value changes: the calls token env,
|
|
2338
|
+
// chat and Boule keep reading the platform issuer from `reading`, and Agora
|
|
2339
|
+
// always plans. The company host is written only once the login host is
|
|
2340
|
+
// applied with Agora's client AND a live read said it serves discovery as
|
|
2341
|
+
// that issuer. An Agora already on the company host keeps it while the host
|
|
2342
|
+
// stays applied: a later failing read never flips it back (no oscillation).
|
|
2343
|
+
let host = platformHost;
|
|
2344
|
+
let issuerNote = null;
|
|
2345
|
+
const choice = agoraIssuerHostFor(org);
|
|
2346
|
+
if (choice.mismatch) {
|
|
2347
|
+
issuerNote = `${org.slug}'s Agora cannot sign in: its login host ${loginHostnameFor(org)} is not on its Agora address ${agoraHostnameFor(org)}; the login host must be on the org's domain`;
|
|
2348
|
+
}
|
|
2349
|
+
else if (choice.host) {
|
|
2350
|
+
const loginHost = choice.host;
|
|
2351
|
+
const target = `https://${loginHost}`;
|
|
2352
|
+
const applied = loginHostServesAgora(loginHost, org, recordedIds);
|
|
2353
|
+
if (applied && (liveAgoraIssuer(live) === target || issuerSwitch.servesDiscovery)) {
|
|
2354
|
+
host = { ...platformHost, issuer: target };
|
|
2355
|
+
}
|
|
2356
|
+
else if (ownSignIn) {
|
|
2357
|
+
// No platform issuer to fall back on: Agora waits for the company's own.
|
|
2358
|
+
return {
|
|
2359
|
+
step: null,
|
|
2360
|
+
blocked: `${org.slug}'s Agora is blocked, not planned: ${target} does not answer OIDC discovery yet`,
|
|
2361
|
+
issuerNote: null,
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
else {
|
|
2365
|
+
issuerNote = `${org.slug}'s Agora keeps the platform issuer; the switch to ${target} is blocked, not planned, until ${loginHost} is applied with Agora's client and answers OIDC discovery`;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2171
2368
|
// The executor writes exactly these host values, so what the plan showed
|
|
2172
2369
|
// is what gets applied; the ids only the apply itself records are read at
|
|
2173
2370
|
// run time.
|
|
@@ -2188,6 +2385,7 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2188
2385
|
},
|
|
2189
2386
|
},
|
|
2190
2387
|
blocked: null,
|
|
2388
|
+
issuerNote,
|
|
2191
2389
|
};
|
|
2192
2390
|
}
|
|
2193
2391
|
const liveEnv = (live.config.env ?? {});
|
|
@@ -2215,8 +2413,9 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2215
2413
|
// actually serving this exact `env`; while it disagrees, the step stays
|
|
2216
2414
|
// planned no matter what Coolify's env store already shows.
|
|
2217
2415
|
const activated = recordedIds[AGORA_ACTIVATED_ENV_SCOPE_KEY] === agoraEnvRevision(env);
|
|
2218
|
-
if (drift.length === 0 && activated && !portDrift)
|
|
2219
|
-
return { step: null, blocked: null };
|
|
2416
|
+
if (drift.length === 0 && activated && !portDrift) {
|
|
2417
|
+
return { step: null, blocked: null, issuerNote };
|
|
2418
|
+
}
|
|
2220
2419
|
const reasons = [
|
|
2221
2420
|
...(drift.length > 0 ? [`${drift.join(', ')} missing or different`] : []),
|
|
2222
2421
|
...(portDrift ? ["ports_exposes wrong -- Traefik can't reach it"] : []),
|
|
@@ -2233,8 +2432,22 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
2233
2432
|
},
|
|
2234
2433
|
},
|
|
2235
2434
|
blocked: null,
|
|
2435
|
+
issuerNote,
|
|
2236
2436
|
};
|
|
2237
2437
|
}
|
|
2438
|
+
/** The issuer a live Agora's `AGORA_CONFIG` names, or null when it has none or is not JSON. */
|
|
2439
|
+
function liveAgoraIssuer(live) {
|
|
2440
|
+
const raw = (live?.config.env ?? {}).AGORA_CONFIG;
|
|
2441
|
+
if (raw === undefined)
|
|
2442
|
+
return null;
|
|
2443
|
+
try {
|
|
2444
|
+
const issuer = JSON.parse(raw)?.issuer;
|
|
2445
|
+
return typeof issuer === 'string' ? issuer : null;
|
|
2446
|
+
}
|
|
2447
|
+
catch {
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2238
2451
|
/**
|
|
2239
2452
|
* #161 (Boule part): every org gets one fixed ZITADEL web client named
|
|
2240
2453
|
* `boule`, so its Boule (https://github.com/wtfalch/boule) can sign anyone in.
|
|
@@ -2486,7 +2699,6 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2486
2699
|
const blockedKey = (rest) => entryKey === APP_ENTRY_KEY ? `coolify:app:${rest}` : `coolify:app:${entryKey}:${rest}`;
|
|
2487
2700
|
const { marker: wantedEnv, unresolved, unresolvedSecrets, } = entry
|
|
2488
2701
|
? resolveAppEnv(org, entry, recordedIds, {
|
|
2489
|
-
platformMailKey: readiness.mailKeyHeld === true,
|
|
2490
2702
|
sharedSecret: readiness.canHoldSecrets === true,
|
|
2491
2703
|
})
|
|
2492
2704
|
: { marker: {}, unresolved: [], unresolvedSecrets: [] };
|
|
@@ -2502,9 +2714,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2502
2714
|
.concat(
|
|
2503
2715
|
// Never a value: the ref of the missing key, or what the host lacks.
|
|
2504
2716
|
unresolvedSecrets.map(([name, source]) => ({
|
|
2505
|
-
description: `${org.slug}'s ${label} env ${name} is blocked, not planned: ${source
|
|
2506
|
-
? `the platform's Lettermint key is not held yet (held-key ref ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF})`
|
|
2507
|
-
: `its source ${source} needs a held-key store that can put, and the host has none`}`,
|
|
2717
|
+
description: `${org.slug}'s ${label} env ${name} is blocked, not planned: ${`its source ${source} needs a held-key store that can put, and the host has none`}`,
|
|
2508
2718
|
key: blockedKey(`env:${name}`),
|
|
2509
2719
|
})))
|
|
2510
2720
|
: [];
|
|
@@ -2544,7 +2754,18 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2544
2754
|
key: blockedKey('repo'),
|
|
2545
2755
|
});
|
|
2546
2756
|
}
|
|
2547
|
-
|
|
2757
|
+
// A compose file can set `privileged`, host networking or a Docker socket mount, and its labels are
|
|
2758
|
+
// its own. `validateHosting` refuses the build pack on write; an entry stored before that rule is
|
|
2759
|
+
// held here too, and a live app of it is parked like one whose entry was removed.
|
|
2760
|
+
const composeBuilt = entry !== undefined && entry.buildPack === 'dockercompose';
|
|
2761
|
+
if (composeBuilt) {
|
|
2762
|
+
blocked.push({
|
|
2763
|
+
description: `${org.slug}'s ${label} is blocked, not planned: a compose-built app is refused until its compose file is checked`,
|
|
2764
|
+
key: blockedKey('compose'),
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
const refused = noRepo || composeBuilt;
|
|
2768
|
+
const toggled = planHostingToggle('coolify', 'app', entry !== undefined && !refused, org, byKey, claimed, () => `provision ${org.slug}'s ${label}`, () => `park ${org.slug}'s ${label} (stops serving; app data is retained, not deleted -- #7 corrections 1)`, stepKey);
|
|
2548
2769
|
// #447: only the live app's own `git_repository` decides, never the
|
|
2549
2770
|
// declared entry (a `repo: null` entry can sit over a bespoke app, which is
|
|
2550
2771
|
// parked as before). Only the `app` key ever held the default app, and an
|
|
@@ -2576,7 +2797,7 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2576
2797
|
}
|
|
2577
2798
|
if (toggled)
|
|
2578
2799
|
return { step: toggled, blocked };
|
|
2579
|
-
if (!entry ||
|
|
2800
|
+
if (!entry || refused)
|
|
2580
2801
|
return { step: null, blocked };
|
|
2581
2802
|
const live = byKey.get(resourceKey('coolify', 'app', stepKey));
|
|
2582
2803
|
if (!live)
|
|
@@ -2691,15 +2912,14 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2691
2912
|
blocked,
|
|
2692
2913
|
};
|
|
2693
2914
|
}
|
|
2694
|
-
//
|
|
2695
|
-
//
|
|
2696
|
-
if (
|
|
2697
|
-
JSON.stringify(appliedAppDomains(org, recordedIds, entryKey))) {
|
|
2915
|
+
// No entry carries Coolify `domains`: the company's tunnel routes its names straight to the container.
|
|
2916
|
+
// An app that still holds some (made before that) has them cleared and is redeployed.
|
|
2917
|
+
if (appliedAppDomains(org, recordedIds, entryKey).length > 0) {
|
|
2698
2918
|
return {
|
|
2699
2919
|
step: {
|
|
2700
2920
|
action,
|
|
2701
2921
|
step: {
|
|
2702
|
-
description: `
|
|
2922
|
+
description: `remove ${org.slug}'s ${label} Coolify domains (the tunnel routes it directly) and redeploy the app`,
|
|
2703
2923
|
destroys: false,
|
|
2704
2924
|
migrates: false,
|
|
2705
2925
|
},
|
|
@@ -2707,17 +2927,16 @@ function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authI
|
|
|
2707
2927
|
blocked,
|
|
2708
2928
|
};
|
|
2709
2929
|
}
|
|
2710
|
-
// A
|
|
2711
|
-
//
|
|
2712
|
-
//
|
|
2930
|
+
// A changed hostname, wildcard or port, or a container still open to Traefik: the label set
|
|
2931
|
+
// (`traefik.enable=false`) is patched and the app redeployed once the marker `coolifyApp`
|
|
2932
|
+
// recorded differs from the wanted one.
|
|
2713
2933
|
const appliedLabels = recordedIds[appLabelsAppliedScopeKey(entryKey)];
|
|
2714
|
-
|
|
2715
|
-
if (labelSpec && entryLabelHash(labelSpec) !== appliedLabels) {
|
|
2934
|
+
if (entryLabelHash(entryLabelSpec(org, entry)) !== appliedLabels) {
|
|
2716
2935
|
return {
|
|
2717
2936
|
step: {
|
|
2718
2937
|
action,
|
|
2719
2938
|
step: {
|
|
2720
|
-
description: `apply ${org.slug}'s ${label}
|
|
2939
|
+
description: `apply ${org.slug}'s ${label} routing (hostnames, port, no Traefik route) and redeploy the app`,
|
|
2721
2940
|
destroys: false,
|
|
2722
2941
|
migrates: false,
|
|
2723
2942
|
},
|
|
@@ -2783,12 +3002,11 @@ function planApps(org, byKey, claimed, recordedIds, plannedSteps, authIssuer, re
|
|
|
2783
3002
|
// The same availability the plan's marker used: the executor records its
|
|
2784
3003
|
// marker from this, never from its own port, so the two cannot disagree.
|
|
2785
3004
|
const usesSecrets = org.hosting.entries.some((e) => e.key === key &&
|
|
2786
|
-
Object.values(e.env).some((v) => typeof v !== 'string' &&
|
|
3005
|
+
Object.values(e.env).some((v) => typeof v !== 'string' && v.from === 'sharedSecret'));
|
|
2787
3006
|
if (usesSecrets && app.step && app.step.action.desired.enabled !== false) {
|
|
2788
3007
|
app.step.action.desired = {
|
|
2789
3008
|
...app.step.action.desired,
|
|
2790
3009
|
secretSources: {
|
|
2791
|
-
platformMailKey: readiness.mailKeyHeld === true,
|
|
2792
3010
|
sharedSecret: readiness.canHoldSecrets === true,
|
|
2793
3011
|
},
|
|
2794
3012
|
};
|
|
@@ -2871,6 +3089,16 @@ function planJobsRegistration(org, recordedIds, jobsAvailable, missingKeyRef = n
|
|
|
2871
3089
|
// The host says whether its executor has a scheduler to register with. A
|
|
2872
3090
|
// fixed text: it is part of the stale-plan comparison.
|
|
2873
3091
|
if (!jobsAvailable) {
|
|
3092
|
+
if (ownsServices(org)) {
|
|
3093
|
+
return {
|
|
3094
|
+
step: null,
|
|
3095
|
+
blocked: `${org.slug}'s jobs registration is blocked, not planned: ${addressMissing
|
|
3096
|
+
? `${org.slug}'s own jobs scheduler has not recorded its internal address yet; apply its own jobs step first`
|
|
3097
|
+
: missingKeyRef
|
|
3098
|
+
? `${org.slug}'s own jobs admin token ${missingKeyRef} is not held yet`
|
|
3099
|
+
: `${org.slug}'s own jobs scheduler is not applied yet`}`,
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
2874
3102
|
return {
|
|
2875
3103
|
step: null,
|
|
2876
3104
|
blocked: addressMissing
|
|
@@ -2943,11 +3171,13 @@ function sortedNames(names) {
|
|
|
2943
3171
|
* domain) plans no per-org tunnel for Agora: `<slug>-agora.oikos.host` rides
|
|
2944
3172
|
* the shared `*.oikos.host` wildcard tunnel (`scripts/shared-ingress.mjs`).
|
|
2945
3173
|
*/
|
|
2946
|
-
function planTunnel(org, byKey, claimed, recordedIds,
|
|
3174
|
+
function planTunnel(org, byKey, claimed, recordedIds, platformApex) {
|
|
2947
3175
|
const desired = (org.hosting.tunnel && org.hosting.usesDomain && Boolean(org.domain)) ||
|
|
2948
3176
|
(org.site.enabled && Boolean(org.domain)) ||
|
|
2949
3177
|
(org.calls.enabled && org.calls.usesDomain && Boolean(org.domain)) ||
|
|
2950
|
-
Boolean(org.domain)
|
|
3178
|
+
Boolean(org.domain) ||
|
|
3179
|
+
// A company without a domain still gets a tunnel of its own for an entry at `<slug>.oikos.host`.
|
|
3180
|
+
entryRoutes(org).length > 0;
|
|
2951
3181
|
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,
|
|
2952
3182
|
// Cloudflare's status, read against what is declared: a park cannot change
|
|
2953
3183
|
// it, so judging on/off by status alone would never let the plan empty.
|
|
@@ -2963,7 +3193,7 @@ function planTunnel(org, byKey, claimed, recordedIds, isPlatformOrg, platformApe
|
|
|
2963
3193
|
// #568: the company's own login host is routed by the step, so a first
|
|
2964
3194
|
// provision carries it too; a park does not. Without a known platform apex
|
|
2965
3195
|
// the platform's own org cannot be told apart, so none is planned.
|
|
2966
|
-
const loginHost = platformApex ? companyLoginTunnelHost(org
|
|
3196
|
+
const loginHost = platformApex ? companyLoginTunnelHost(org) : null;
|
|
2967
3197
|
if (toggled && toggled.action.desired.enabled !== false && loginHost) {
|
|
2968
3198
|
return {
|
|
2969
3199
|
...toggled,
|
|
@@ -2995,8 +3225,17 @@ function planTunnel(org, byKey, claimed, recordedIds, isPlatformOrg, platformApe
|
|
|
2995
3225
|
org.domain !== null &&
|
|
2996
3226
|
sortedNames(published.split(',')) !==
|
|
2997
3227
|
sortedNames(tunnelDnsHostnames(org, org.domain, loginHost));
|
|
2998
|
-
|
|
2999
|
-
|
|
3228
|
+
// The entry names in the platform's oikos.host zone are recorded apart from the company's own zone.
|
|
3229
|
+
const oikosStale = (recordedIds[CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY] ?? '') !==
|
|
3230
|
+
entryOikosHostnames(org).join(',');
|
|
3231
|
+
const dnsStale = (org.domain !== null && recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain) ||
|
|
3232
|
+
edgeChanged ||
|
|
3233
|
+
oikosStale;
|
|
3234
|
+
// An entry's hostname goes straight to its container, so a changed port, key or name, or an ingress
|
|
3235
|
+
// written before entries were routed that way, needs the ingress written again. Nothing recorded
|
|
3236
|
+
// equals no routes.
|
|
3237
|
+
const routesStale = (recordedIds[CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY] ?? '[]') !== entryRoutesMarker(org);
|
|
3238
|
+
if (!connectorMissing && !dnsStale && !routesStale)
|
|
3000
3239
|
return null;
|
|
3001
3240
|
return {
|
|
3002
3241
|
action: {
|
|
@@ -3008,7 +3247,9 @@ function planTunnel(org, byKey, claimed, recordedIds, isPlatformOrg, platformApe
|
|
|
3008
3247
|
step: {
|
|
3009
3248
|
description: connectorMissing
|
|
3010
3249
|
? `run a cloudflared connector for ${org.slug}'s Cloudflare tunnel`
|
|
3011
|
-
:
|
|
3250
|
+
: dnsStale
|
|
3251
|
+
? `point ${org.slug}'s hostnames at its Cloudflare tunnel`
|
|
3252
|
+
: `route ${org.slug}'s entry hostnames straight to their containers`,
|
|
3012
3253
|
destroys: false,
|
|
3013
3254
|
migrates: false,
|
|
3014
3255
|
},
|