@wtfalch/foundry 0.9.0 → 0.11.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/apply.d.ts +16 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/migrations/0030_hosting_entry_hostnames.sql +24 -0
- package/dist/migrations/0031_adoptions.sql +37 -0
- package/dist/org.d.ts +49 -4
- package/dist/org.js +134 -1
- package/dist/plan/adopt.d.ts +112 -0
- package/dist/plan/adopt.js +252 -0
- package/dist/plan/adoption-vault.d.ts +9 -0
- package/dist/plan/adoption-vault.js +1 -0
- package/dist/plan/agora.d.ts +7 -0
- package/dist/plan/agora.js +9 -0
- package/dist/plan/broker-entries.d.ts +108 -0
- package/dist/plan/broker-entries.js +214 -0
- package/dist/plan/cloudflared-connector.d.ts +10 -2
- package/dist/plan/cloudflared-connector.js +24 -16
- package/dist/plan/diff.d.ts +14 -0
- package/dist/plan/diff.js +488 -47
- package/dist/plan/executor.d.ts +141 -3
- package/dist/plan/executor.js +1100 -149
- package/dist/plan/hostnames.d.ts +24 -0
- package/dist/plan/hostnames.js +56 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/locked-hostnames.d.ts +8 -0
- package/dist/plan/locked-hostnames.js +26 -0
- package/dist/plan/login-host.d.ts +1 -1
- package/dist/plan/login-host.js +5 -2
- package/dist/plan/plan-apply.d.ts +63 -3
- package/dist/plan/plan-apply.js +163 -5
- package/dist/plan/platform-diff.d.ts +66 -13
- package/dist/plan/platform-diff.js +117 -15
- package/dist/plan/platform-providers.d.ts +95 -2
- package/dist/plan/platform-providers.js +393 -27
- package/dist/plan/providers-readers.d.ts +8 -1
- package/dist/plan/providers-readers.js +58 -4
- package/dist/plan.d.ts +6 -0
- package/dist/store/postgres.d.ts +6 -1
- package/dist/store/postgres.js +80 -5
- package/dist/store/schema.d.ts +162 -0
- package/dist/store/schema.js +19 -1
- package/dist/store.d.ts +37 -0
- package/package.json +1 -1
package/dist/plan/diff.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
2
|
-
import {
|
|
2
|
+
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
|
+
import { adoptedConnectorKey, adoptedResources, adoptedTunnelKey, declaredAdoptUuids, isActiveSnapshot, protectsKey, } from './adopt.js';
|
|
3
4
|
import { NO_AGORA_HOST_VALUES, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraHostMissing, desiredAgoraEnv, } from './agora.js';
|
|
5
|
+
import { brokerEntryAppliedScopeKey, brokerEntryWarningsScopeKey, entryAuthEnv, entryAuthEnvAppliedScopeKey, wantedBrokerEntry, } from './broker-entries.js';
|
|
6
|
+
import { appHostname, entryHostnames, tunnelDnsHostnames } from './hostnames.js';
|
|
4
7
|
import { LOGIN_BINDING_SCOPE_KEY, loginBindingState, loginHostClientIds, loginHostnameFor, pendingRemovalHosts, } from './login-host.js';
|
|
5
8
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
6
|
-
import {
|
|
9
|
+
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, appDeployPendingScopeKey, appEnvAppliedScopeKey, appSettingsOf, appStepKey, appliedAppDomains, appliedAppSettings, dbEnvAppliedScopeKey, resolveAppEnv, versionNotAppliedLine, } from './platform-diff.js';
|
|
7
10
|
import { DEFAULT_ORG_PLATFORM_READINESS } from './platform-org.js';
|
|
8
11
|
/** The `Plan` (#19's public contract) a `PlanDetail` carries. */
|
|
9
12
|
export function toPlan(detail) {
|
|
@@ -164,25 +167,110 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
164
167
|
// `AUTH_CLIENT_ID`, which only exists once that app's own `zitadel:app`
|
|
165
168
|
// step has run -- `plan-apply.ts`'s `runSteps` executes `plannedSteps` in
|
|
166
169
|
// this array order, one at a time.
|
|
170
|
+
// Customers before the entries that sign them in: `pushAppEnv` and the
|
|
171
|
+
// broker entry read the customers organisation's id.
|
|
172
|
+
const customers = planCustomers(org, live, claimed, recordedIds);
|
|
173
|
+
if (customers)
|
|
174
|
+
plannedSteps.push(customers);
|
|
167
175
|
plannedSteps.push(...planIssuerApps(org, byKey, claimed));
|
|
176
|
+
plannedSteps.push(...planEntryClients(org, byKey, claimed));
|
|
177
|
+
// #311: the snapshot of each adopted resource, before the database and app
|
|
178
|
+
// steps that first write to them.
|
|
179
|
+
const adoption = planAdoptions(org, options.adoption);
|
|
180
|
+
plannedSteps.push(...adoption.steps);
|
|
168
181
|
// The Access gate before the database, app, tunnel and zone (#84): on a
|
|
169
182
|
// first apply the hostname is gated before anything answers on it.
|
|
170
|
-
|
|
171
|
-
if (access)
|
|
172
|
-
plannedSteps.push(access);
|
|
183
|
+
plannedSteps.push(...planAccess(org, recordedIds));
|
|
173
184
|
// Database before app (hosting-wiring campaign): `plan-apply.ts`'s
|
|
174
185
|
// `runSteps` executes `plannedSteps` in this array order, and the app's own
|
|
175
186
|
// step (`executor.ts`'s `coolifyApp`) pushes the database's connection
|
|
176
187
|
// string into the app's environment once the database exists -- it can
|
|
177
188
|
// only do that if the database step already ran and recorded the
|
|
178
189
|
// database's resource id.
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
// A park of an adopted resource is reported, never planned (#311).
|
|
191
|
+
const parks = [];
|
|
192
|
+
const notAdoptedPark = (s) => {
|
|
193
|
+
const held = adoptedPark(org, options.adoption?.adopted ?? [], recordedIds, s);
|
|
194
|
+
if (held)
|
|
195
|
+
parks.push(held);
|
|
196
|
+
return !held;
|
|
197
|
+
};
|
|
198
|
+
// An adopt step plans its resource's step after it, whatever the markers
|
|
199
|
+
// say: the adopted resource has not been configured by foundry yet.
|
|
200
|
+
const adopting = new Set(adoption.steps.map((s) => s.action.key));
|
|
201
|
+
const database = planDatabase(org, byKey, claimed) ??
|
|
202
|
+
(adopting.has(`coolify:database:${org.id}`)
|
|
203
|
+
? {
|
|
204
|
+
action: {
|
|
205
|
+
system: 'coolify',
|
|
206
|
+
kind: 'database',
|
|
207
|
+
key: org.id,
|
|
208
|
+
desired: { enabled: true },
|
|
209
|
+
},
|
|
210
|
+
step: {
|
|
211
|
+
description: `record ${org.slug}'s adopted database`,
|
|
212
|
+
destroys: false,
|
|
213
|
+
migrates: false,
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
: null);
|
|
217
|
+
if (database &&
|
|
218
|
+
notAdoptedPark(database) &&
|
|
219
|
+
!adoption.withheld.has(`coolify:database:${org.id}`)) {
|
|
181
220
|
plannedSteps.push(database);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
221
|
+
}
|
|
222
|
+
const authIssuer = 'error' in agoraHost ? null : agoraHost.issuer;
|
|
223
|
+
const app = planApps(org, byKey, claimed, recordedIds, plannedSteps, adopting, authIssuer);
|
|
224
|
+
plannedSteps.push(...app.steps.filter((s) => notAdoptedPark(s) && !adoption.withheld.has(`coolify:app:${s.action.key}`)));
|
|
225
|
+
// After the apps: the broker entry's key is minted into each app's env.
|
|
226
|
+
const brokerEntries = planBrokerEntries(org, recordedIds, plannedSteps, options.broker ?? false);
|
|
227
|
+
plannedSteps.push(...brokerEntries.steps);
|
|
228
|
+
// An adopt step of the tunnel or its connector plans the tunnel step after
|
|
229
|
+
// it, whatever the markers say: foundry has not merged its routes yet (#312).
|
|
230
|
+
const tunnelKey = adoptedTunnelKey(org);
|
|
231
|
+
const connectorKey = adoptedConnectorKey(org);
|
|
232
|
+
const tunnelHeld = adoption.withheld.has(tunnelKey) || adoption.withheld.has(connectorKey);
|
|
233
|
+
const planned = planTunnel(org, byKey, claimed, recordedIds);
|
|
234
|
+
const merging = org.hosting.adopt?.tunnel !== undefined &&
|
|
235
|
+
(planned !== null || adopting.has(tunnelKey) || adopting.has(connectorKey));
|
|
236
|
+
const tunnelPlanned = merging && planned?.action.desired.enabled !== false
|
|
237
|
+
? {
|
|
238
|
+
action: {
|
|
239
|
+
system: 'cloudflare',
|
|
240
|
+
kind: 'tunnel',
|
|
241
|
+
key: org.id,
|
|
242
|
+
desired: { enabled: true },
|
|
243
|
+
},
|
|
244
|
+
step: {
|
|
245
|
+
description: `merge ${org.slug}'s routes into its adopted Cloudflare tunnel and keep the others`,
|
|
246
|
+
destroys: false,
|
|
247
|
+
migrates: false,
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
: planned;
|
|
251
|
+
// An adopted tunnel with no active snapshot and no adopt step in this plan
|
|
252
|
+
// (hosting off plans none) cannot be written to: reported blocked, no tunnel
|
|
253
|
+
// step, or the step would throw on every apply (rule 2, #312).
|
|
254
|
+
const adoptedRows = options.adoption?.adopted ?? [];
|
|
255
|
+
const activeRow = (key, uuid) => isActiveSnapshot(adoptedRows.find((r) => r.resourceKey === key), uuid ?? '');
|
|
256
|
+
const adoptable = org.hosting.adopt?.tunnel !== undefined &&
|
|
257
|
+
!tunnelHeld &&
|
|
258
|
+
!adopting.has(tunnelKey) &&
|
|
259
|
+
!adopting.has(connectorKey) &&
|
|
260
|
+
(!org.hosting.enabled ||
|
|
261
|
+
!(activeRow(tunnelKey, org.hosting.adopt.tunnel) &&
|
|
262
|
+
activeRow(connectorKey, org.hosting.adopt.connectorApp)));
|
|
263
|
+
if (adoptable) {
|
|
264
|
+
adoption.blocked.push({
|
|
265
|
+
description: `${org.slug}'s ${tunnelKey} is blocked, not planned: hosting is off, so its routes are not merged into the adopted tunnel`,
|
|
266
|
+
key: `coolify:adopt:${tunnelKey}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// Neither a snapshot-less adopted tunnel nor an undeclared one is written to
|
|
270
|
+
// or parked (#312); a blocked adoption plans no tunnel step (rule 2).
|
|
271
|
+
const tunnel = tunnelPlanned && notAdoptedPark(tunnelPlanned) && !tunnelHeld && !adoptable
|
|
272
|
+
? tunnelPlanned
|
|
273
|
+
: null;
|
|
186
274
|
// #238: a tunnel that is being provisioned waits for an active zone; parking
|
|
187
275
|
// one never does.
|
|
188
276
|
if (tunnel && zoneWaiting && tunnel.action.desired.enabled !== false) {
|
|
@@ -209,11 +297,6 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
209
297
|
else if (rateLimit) {
|
|
210
298
|
plannedSteps.push(rateLimit);
|
|
211
299
|
}
|
|
212
|
-
// After the apps: the customers org is granted the project the first app
|
|
213
|
-
// step creates.
|
|
214
|
-
const customers = planCustomers(org, live, claimed, recordedIds);
|
|
215
|
-
if (customers)
|
|
216
|
-
plannedSteps.push(customers);
|
|
217
300
|
// #164: every org's own Agora -- always on, no toggle. Last, so nothing
|
|
218
301
|
// else in the org waits on it: `runSteps` stops at the first failing step.
|
|
219
302
|
// Its fixed ZITADEL client before the Coolify app it signs into:
|
|
@@ -288,6 +371,8 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
288
371
|
unmanaged.push(...zoneBlocked);
|
|
289
372
|
// #243: an appEnv source nothing will record is reported, not silently skipped.
|
|
290
373
|
unmanaged.push(...app.blocked);
|
|
374
|
+
unmanaged.push(...adoption.blocked, ...parks);
|
|
375
|
+
unmanaged.push(...brokerEntries.blocked);
|
|
291
376
|
return { orgId: org.id, plannedSteps, unmanaged, versionChanges };
|
|
292
377
|
}
|
|
293
378
|
/**
|
|
@@ -524,6 +609,7 @@ const MAIL_ROLE_MAILBOXES_SCOPE_KEY = 'mail_role_mailboxes';
|
|
|
524
609
|
const CLOUDFLARE_TUNNEL_CONNECTOR_APP_SCOPE_KEY = 'cloudflare_tunnel_connector_app';
|
|
525
610
|
/** #236: restated from `executor.ts`, the same rule: the domain the org tunnel's DNS was last published under. */
|
|
526
611
|
const CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY = 'cloudflare_tunnel_dns_domain';
|
|
612
|
+
const CLOUDFLARE_TUNNEL_DNS_HOSTNAMES_SCOPE_KEY = 'cloudflare_tunnel_dns_hostnames';
|
|
527
613
|
/** Restated from `executor.ts`, the same rule: the mail domain the last apply resolved. */
|
|
528
614
|
const MAIL_DOMAIN_SCOPE_KEY = 'mail_domain';
|
|
529
615
|
/** Restated from `executor.ts`, the same rule: the id of an ai key minted but not delivered. */
|
|
@@ -1325,6 +1411,117 @@ function planAgora(org, byKey, claimed, recordedIds, reading) {
|
|
|
1325
1411
|
blocked: null,
|
|
1326
1412
|
};
|
|
1327
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* #311: one `coolify:adopt` step per adopted resource that has no adoption row
|
|
1416
|
+
* yet. It `migrates`, so apply needs the org's name typed. With no vault and
|
|
1417
|
+
* store wired, each is reported blocked instead, and `withheld` names the
|
|
1418
|
+
* resources whose own step must not plan either: it would write to a live
|
|
1419
|
+
* resource nothing has snapshotted (rule 2).
|
|
1420
|
+
*/
|
|
1421
|
+
function planAdoptions(org, adoption) {
|
|
1422
|
+
const steps = [];
|
|
1423
|
+
const blocked = [];
|
|
1424
|
+
const withheld = new Set();
|
|
1425
|
+
for (const resource of adoptedResources(org)) {
|
|
1426
|
+
const what = resource.kind === 'app'
|
|
1427
|
+
? `${resource.entry.key} app`
|
|
1428
|
+
: resource.kind === 'connector'
|
|
1429
|
+
? 'cloudflared connector'
|
|
1430
|
+
: resource.kind;
|
|
1431
|
+
if (!adoption?.wired) {
|
|
1432
|
+
withheld.add(resource.resourceKey);
|
|
1433
|
+
blocked.push({
|
|
1434
|
+
description: `${org.slug}'s adoption of its ${what} is blocked, not planned: no adoption vault is wired`,
|
|
1435
|
+
key: `coolify:adopt:${resource.resourceKey}`,
|
|
1436
|
+
});
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const row = adoption.adopted.find((r) => r.resourceKey === resource.resourceKey);
|
|
1440
|
+
// A new snapshot of another uuid would record foundry's own earlier writes.
|
|
1441
|
+
if (row?.state === 'adopted' && row.settings.uuid !== resource.uuid) {
|
|
1442
|
+
withheld.add(resource.resourceKey);
|
|
1443
|
+
blocked.push({
|
|
1444
|
+
description: `${org.slug}'s ${resource.resourceKey} is adopted as ${String(row.settings.uuid)}, not ${resource.uuid}: restore the adopted ${resource.kind} first, not planned`,
|
|
1445
|
+
key: `coolify:adopt:${resource.resourceKey}`,
|
|
1446
|
+
});
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (isActiveSnapshot(row, resource.uuid))
|
|
1450
|
+
continue;
|
|
1451
|
+
// A snapshot of a uuid another key holds would record foundry's own writes.
|
|
1452
|
+
const holder = adoption.adopted.find((r) => r.resourceKey !== resource.resourceKey &&
|
|
1453
|
+
protectsKey(r) &&
|
|
1454
|
+
r.settings.uuid === resource.uuid);
|
|
1455
|
+
if (holder) {
|
|
1456
|
+
withheld.add(resource.resourceKey);
|
|
1457
|
+
blocked.push({
|
|
1458
|
+
description: `${org.slug}'s ${resource.resourceKey} declares ${resource.uuid}, which ${holder.resourceKey} already adopted: declare it under one key, not planned`,
|
|
1459
|
+
key: `coolify:adopt:${resource.resourceKey}`,
|
|
1460
|
+
});
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
steps.push({
|
|
1464
|
+
action: {
|
|
1465
|
+
system: 'coolify',
|
|
1466
|
+
kind: 'adopt',
|
|
1467
|
+
key: resource.resourceKey,
|
|
1468
|
+
desired: { uuid: resource.uuid },
|
|
1469
|
+
},
|
|
1470
|
+
step: {
|
|
1471
|
+
description: `adopt ${org.slug}'s ${what} (Coolify ${resource.uuid}): snapshot it, then change only what the org declares`,
|
|
1472
|
+
destroys: false,
|
|
1473
|
+
migrates: true,
|
|
1474
|
+
},
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
// An entry that was adopted and no longer declares `adopt` is not managed
|
|
1478
|
+
// as foundry's own app or database (rule 2), restored or not.
|
|
1479
|
+
const held = (key) => adoption?.adopted.some((r) => r.resourceKey === key && protectsKey(r));
|
|
1480
|
+
const undeclared = [
|
|
1481
|
+
...(org.hosting.enabled
|
|
1482
|
+
? [
|
|
1483
|
+
...(org.hosting.adopt?.database ? [] : [`coolify:database:${org.id}`]),
|
|
1484
|
+
...org.hosting.entries
|
|
1485
|
+
.filter((entry) => !entry.adopt)
|
|
1486
|
+
.map((entry) => `coolify:app:${appStepKey(org, entry.key)}`),
|
|
1487
|
+
]
|
|
1488
|
+
: []),
|
|
1489
|
+
// The tunnel and its connector are held with hosting off too (#312).
|
|
1490
|
+
...(org.hosting.adopt?.tunnel ? [] : [adoptedTunnelKey(org)]),
|
|
1491
|
+
...(org.hosting.adopt?.connectorApp ? [] : [adoptedConnectorKey(org)]),
|
|
1492
|
+
];
|
|
1493
|
+
for (const key of undeclared.filter(held)) {
|
|
1494
|
+
withheld.add(key);
|
|
1495
|
+
blocked.push({
|
|
1496
|
+
description: `${org.slug}'s ${key} was adopted, so foundry does not manage it as its own: declare adopt again, or run restore to put it back as it was, not planned`,
|
|
1497
|
+
key: `coolify:adopt:${key}`,
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
return { steps, blocked, withheld };
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* The blocked item for a park step on a resource foundry adopted: its key has
|
|
1504
|
+
* a row that protects it (`adopted`, `confirmed` or `restored`), or its recorded id is a uuid the org
|
|
1505
|
+
* declares as adopted. `null` for any other step (#311).
|
|
1506
|
+
*/
|
|
1507
|
+
function adoptedPark(org, rows, recordedIds, { action }) {
|
|
1508
|
+
if (action.desired.enabled !== false)
|
|
1509
|
+
return null;
|
|
1510
|
+
const parkable = action.system === 'coolify'
|
|
1511
|
+
? action.kind === 'app' || action.kind === 'database'
|
|
1512
|
+
: action.system === 'cloudflare' && action.kind === 'tunnel';
|
|
1513
|
+
if (!parkable)
|
|
1514
|
+
return null;
|
|
1515
|
+
const key = `${action.system}:${action.kind}:${action.key}`;
|
|
1516
|
+
const id = recordedIds[key];
|
|
1517
|
+
const held = rows.some((r) => r.resourceKey === key && protectsKey(r));
|
|
1518
|
+
if (!held && !(id && declaredAdoptUuids(org).has(id)))
|
|
1519
|
+
return null;
|
|
1520
|
+
return {
|
|
1521
|
+
description: `${org.slug}'s ${key} is adopted, not stopped: foundry never stops a resource it adopted`,
|
|
1522
|
+
key: `coolify:adopt:${key}`,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1328
1525
|
/**
|
|
1329
1526
|
* The shared on/off shape behind {@link planApp}, {@link planDatabase} and
|
|
1330
1527
|
* {@link planTunnel} (#71): a per-org singleton resource, keyed by the org
|
|
@@ -1387,7 +1584,7 @@ function planHostingToggle(system, kind, desiredEnabled, org, byKey, claimed, de
|
|
|
1387
1584
|
* resource's `enabled` field -- a live resource nothing has actually
|
|
1388
1585
|
* checked yet is assumed fine rather than replanned every time.
|
|
1389
1586
|
*/
|
|
1390
|
-
function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
1587
|
+
function planApp(org, entryKey, byKey, claimed, recordedIds, plannedSteps, authIssuer) {
|
|
1391
1588
|
// #243: an `appEnv` source with no recorded value and no step planned to
|
|
1392
1589
|
// record one this run has nothing to push, now or after this apply.
|
|
1393
1590
|
const sourceStep = {
|
|
@@ -1395,21 +1592,27 @@ function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
|
1395
1592
|
aiOrganisationId: ['ai', 'organisation'],
|
|
1396
1593
|
mailDomain: ['coolify', 'mail'],
|
|
1397
1594
|
};
|
|
1398
|
-
const entry = org.hosting.enabled
|
|
1595
|
+
const entry = org.hosting.enabled
|
|
1596
|
+
? org.hosting.entries.find((e) => e.key === entryKey)
|
|
1597
|
+
: undefined;
|
|
1598
|
+
// Descriptions and unmanaged keys of the `app` entry stay as they were; any
|
|
1599
|
+
// other entry is named in them.
|
|
1600
|
+
const label = entryKey === APP_ENTRY_KEY ? 'app' : `${entryKey} app`;
|
|
1601
|
+
const blockedKey = (rest) => entryKey === APP_ENTRY_KEY ? `coolify:app:${rest}` : `coolify:app:${entryKey}:${rest}`;
|
|
1399
1602
|
const { values: wantedEnv, unresolved } = entry
|
|
1400
1603
|
? resolveAppEnv(org, entry, recordedIds)
|
|
1401
1604
|
: { values: {}, unresolved: [] };
|
|
1402
|
-
const stepKey = appStepKey(org);
|
|
1605
|
+
const stepKey = appStepKey(org, entryKey);
|
|
1403
1606
|
const blocked = entry
|
|
1404
1607
|
? unresolved
|
|
1405
1608
|
.filter(([, source]) => !plannedSteps.some((p) => p.action.system === sourceStep[source][0] &&
|
|
1406
1609
|
p.action.kind === sourceStep[source][1]))
|
|
1407
1610
|
.map(([name, source]) => ({
|
|
1408
|
-
description: `${org.slug}'s
|
|
1409
|
-
key: `
|
|
1611
|
+
description: `${org.slug}'s ${label} env ${name} is blocked, not planned: its source ${source} has no recorded value yet`,
|
|
1612
|
+
key: blockedKey(`env:${name}`),
|
|
1410
1613
|
}))
|
|
1411
1614
|
: [];
|
|
1412
|
-
const toggled = planHostingToggle('coolify', 'app', entry !== undefined, org, byKey, claimed, () => `provision ${org.slug}'s
|
|
1615
|
+
const toggled = planHostingToggle('coolify', 'app', entry !== undefined, 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);
|
|
1413
1616
|
if (toggled)
|
|
1414
1617
|
return { step: toggled, blocked };
|
|
1415
1618
|
if (!entry)
|
|
@@ -1423,12 +1626,29 @@ function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
|
1423
1626
|
key: stepKey,
|
|
1424
1627
|
desired: { enabled: true },
|
|
1425
1628
|
};
|
|
1426
|
-
|
|
1629
|
+
// An adopted app patched but not yet deployed: replanned whatever the
|
|
1630
|
+
// markers say (#311).
|
|
1631
|
+
if (entry.adopt && recordedIds[appDeployPendingScopeKey(entryKey)]) {
|
|
1427
1632
|
return {
|
|
1428
1633
|
step: {
|
|
1429
1634
|
action,
|
|
1430
1635
|
step: {
|
|
1431
|
-
description: `
|
|
1636
|
+
description: `deploy ${org.slug}'s ${label} settings to the adopted app`,
|
|
1637
|
+
destroys: false,
|
|
1638
|
+
migrates: false,
|
|
1639
|
+
},
|
|
1640
|
+
},
|
|
1641
|
+
blocked,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
// An adopted app gets no volume (`executor.ts`'s `coolifyApp`), so a missing
|
|
1645
|
+
// one is not a drift (#311).
|
|
1646
|
+
if (live.config.hasLogVolume === false && !entry.adopt) {
|
|
1647
|
+
return {
|
|
1648
|
+
step: {
|
|
1649
|
+
action,
|
|
1650
|
+
step: {
|
|
1651
|
+
description: `add a durable log directory to ${org.slug}'s ${label} (persistent volume at /app/logs + LOG_DIR) so a database outage does not erase the evidence (#75)`,
|
|
1432
1652
|
destroys: false,
|
|
1433
1653
|
migrates: false,
|
|
1434
1654
|
},
|
|
@@ -1442,13 +1662,17 @@ function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
|
1442
1662
|
// names (`envNames` absent) reads as compliant, like `hasLogVolume`.
|
|
1443
1663
|
const envNames = live.config.envNames;
|
|
1444
1664
|
const dbLive = byKey.get(resourceKey('coolify', 'database', org.id));
|
|
1445
|
-
|
|
1665
|
+
// An adopted app or database keeps the addresses it has (#311).
|
|
1666
|
+
const dbReady = entry.database &&
|
|
1667
|
+
!entry.adopt &&
|
|
1668
|
+
!org.hosting.adopt?.database &&
|
|
1669
|
+
Boolean(recordedIds[`coolify:database:${org.id}`]);
|
|
1446
1670
|
if (dbReady && dbLive?.config.hasInternalUrl === false) {
|
|
1447
1671
|
// Rule 2: the push cannot succeed yet, so report it rather than replan
|
|
1448
1672
|
// forever.
|
|
1449
1673
|
blocked.push({
|
|
1450
|
-
description: `${org.slug}'s
|
|
1451
|
-
key: '
|
|
1674
|
+
description: `${org.slug}'s ${label} database URLs are blocked, not planned: the database reports no internal connection URL yet`,
|
|
1675
|
+
key: blockedKey('database-url'),
|
|
1452
1676
|
});
|
|
1453
1677
|
}
|
|
1454
1678
|
else if (dbReady &&
|
|
@@ -1458,12 +1682,12 @@ function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
|
1458
1682
|
((Array.isArray(envNames) && !envNames.includes('DATABASE_URL_OWNER')) ||
|
|
1459
1683
|
// A crash between the push and the restart leaves the env right and no
|
|
1460
1684
|
// marker: replan once more to restart and set it.
|
|
1461
|
-
!recordedIds[
|
|
1685
|
+
!recordedIds[dbEnvAppliedScopeKey(entryKey)])) {
|
|
1462
1686
|
return {
|
|
1463
1687
|
step: {
|
|
1464
1688
|
action,
|
|
1465
1689
|
step: {
|
|
1466
|
-
description: `give ${org.slug}'s
|
|
1690
|
+
description: `give ${org.slug}'s ${label} its owner and runtime database URLs and restart it (#239)`,
|
|
1467
1691
|
destroys: false,
|
|
1468
1692
|
migrates: false,
|
|
1469
1693
|
},
|
|
@@ -1474,12 +1698,71 @@ function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
|
1474
1698
|
// #243 (rule 3): a changed entry `env` reaches an app that is already
|
|
1475
1699
|
// live. `pushAppEnv` records what it pushed, so this plans the same step
|
|
1476
1700
|
// again, with a redeploy, only once the resolved map differs from that.
|
|
1477
|
-
if (JSON.stringify(wantedEnv) !== (recordedIds[
|
|
1701
|
+
if (JSON.stringify(wantedEnv) !== (recordedIds[appEnvAppliedScopeKey(entryKey)] ?? '{}')) {
|
|
1702
|
+
return {
|
|
1703
|
+
step: {
|
|
1704
|
+
action,
|
|
1705
|
+
step: {
|
|
1706
|
+
description: `push ${org.slug}'s changed ${label} env (appEnv) and redeploy the app`,
|
|
1707
|
+
destroys: false,
|
|
1708
|
+
migrates: false,
|
|
1709
|
+
},
|
|
1710
|
+
},
|
|
1711
|
+
blocked,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
// F5 (rule 3): who signs in, which client and which issuer reach an app
|
|
1715
|
+
// that is already live. `pushAppEnv` records what it pushed.
|
|
1716
|
+
if (entry.uses?.auth &&
|
|
1717
|
+
JSON.stringify(entryAuthEnv(org, entry, recordedIds, authIssuer)) !==
|
|
1718
|
+
(recordedIds[entryAuthEnvAppliedScopeKey(entryKey)] ?? '{}')) {
|
|
1719
|
+
return {
|
|
1720
|
+
step: {
|
|
1721
|
+
action,
|
|
1722
|
+
step: {
|
|
1723
|
+
description: `push ${org.slug}'s ${label} sign-in env (organisation, client, issuer) and redeploy the app`,
|
|
1724
|
+
destroys: false,
|
|
1725
|
+
migrates: false,
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
blocked,
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
// A changed hostname reaches an app that is already live: its domains are
|
|
1732
|
+
// patched and it is redeployed.
|
|
1733
|
+
if (JSON.stringify(entryHostnames(org, entry)) !==
|
|
1734
|
+
JSON.stringify(appliedAppDomains(org, recordedIds, entryKey))) {
|
|
1478
1735
|
return {
|
|
1479
1736
|
step: {
|
|
1480
1737
|
action,
|
|
1481
1738
|
step: {
|
|
1482
|
-
description: `
|
|
1739
|
+
description: `apply ${org.slug}'s ${label} hostnames and redeploy the app`,
|
|
1740
|
+
destroys: false,
|
|
1741
|
+
migrates: false,
|
|
1742
|
+
},
|
|
1743
|
+
},
|
|
1744
|
+
blocked,
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
// A changed `folder`, `port` or `autoDeploy` reaches an app that is already live.
|
|
1748
|
+
const want = appSettingsOf(entry);
|
|
1749
|
+
const have = appliedAppSettings(recordedIds, entryKey);
|
|
1750
|
+
// Coolify has no value that resets a port to its default, so a port taken
|
|
1751
|
+
// back to null cannot be applied (rule 2): reported, not planned.
|
|
1752
|
+
if (want.port === null && have.port !== null) {
|
|
1753
|
+
blocked.push({
|
|
1754
|
+
description: `${org.slug}'s ${label} port is blocked, not planned: Coolify cannot reset it to its default, so set a port`,
|
|
1755
|
+
key: blockedKey('port'),
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
if (want.folder !== have.folder ||
|
|
1759
|
+
want.autoDeploy !== have.autoDeploy ||
|
|
1760
|
+
(want.port !== null && want.port !== have.port)) {
|
|
1761
|
+
return {
|
|
1762
|
+
step: {
|
|
1763
|
+
action,
|
|
1764
|
+
step: {
|
|
1765
|
+
description: `apply ${org.slug}'s ${label} folder, port and auto-deploy settings`,
|
|
1483
1766
|
destroys: false,
|
|
1484
1767
|
migrates: false,
|
|
1485
1768
|
},
|
|
@@ -1489,6 +1772,55 @@ function planApp(org, byKey, claimed, recordedIds, plannedSteps) {
|
|
|
1489
1772
|
}
|
|
1490
1773
|
return { step: null, blocked };
|
|
1491
1774
|
}
|
|
1775
|
+
/**
|
|
1776
|
+
* {@link planApp} once per hosting entry. A key with no declared entry still
|
|
1777
|
+
* runs for `app`, so an org that turns hosting off parks the app it has live.
|
|
1778
|
+
*/
|
|
1779
|
+
function planApps(org, byKey, claimed, recordedIds, plannedSteps, adopting, authIssuer) {
|
|
1780
|
+
const steps = [];
|
|
1781
|
+
const blocked = [];
|
|
1782
|
+
// An entry removed from the list while its app is recorded is parked like a
|
|
1783
|
+
// removed `app`: `planApp` finds no entry for its key.
|
|
1784
|
+
const recordedKeys = Object.keys(recordedIds)
|
|
1785
|
+
.filter((id) => id.startsWith(`coolify:app:${org.id}:`))
|
|
1786
|
+
.map((id) => id.slice(`coolify:app:${org.id}:`.length));
|
|
1787
|
+
const declared = new Set([APP_ENTRY_KEY, ...org.hosting.entries.map((e) => e.key)]);
|
|
1788
|
+
const keys = new Set([...declared, ...recordedKeys]);
|
|
1789
|
+
for (const key of keys) {
|
|
1790
|
+
if (!declared.has(key)) {
|
|
1791
|
+
// The reader gives a parked app no `enabled` field, so only a running
|
|
1792
|
+
// app is parked; a stopped one plans nothing and is still claimed.
|
|
1793
|
+
const resource = byKey.get(resourceKey('coolify', 'app', appStepKey(org, key)));
|
|
1794
|
+
const status = resource?.config.status;
|
|
1795
|
+
if (resource)
|
|
1796
|
+
claimed.add(resourceKey('coolify', 'app', appStepKey(org, key)));
|
|
1797
|
+
if (!resource || (typeof status === 'string' && /^(exited|stopped)/.test(status)))
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
const app = planApp(org, key, byKey, claimed, recordedIds, plannedSteps, authIssuer);
|
|
1801
|
+
const stepKey = appStepKey(org, key);
|
|
1802
|
+
if (!app.step && adopting.has(`coolify:app:${stepKey}`)) {
|
|
1803
|
+
app.step = {
|
|
1804
|
+
action: { system: 'coolify', kind: 'app', key: stepKey, desired: { enabled: true } },
|
|
1805
|
+
step: {
|
|
1806
|
+
description: `apply ${org.slug}'s ${key === APP_ENTRY_KEY ? 'app' : `${key} app`} settings, hostnames and env to the adopted app`,
|
|
1807
|
+
destroys: false,
|
|
1808
|
+
migrates: false,
|
|
1809
|
+
},
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
// The executor pushes the issuer the plan read, the way Agora gets its host values.
|
|
1813
|
+
if (app.step && app.step.action.desired.enabled !== false) {
|
|
1814
|
+
const signsIn = org.hosting.entries.some((e) => e.key === key && e.uses?.auth);
|
|
1815
|
+
if (signsIn)
|
|
1816
|
+
app.step.action.desired = { ...app.step.action.desired, authIssuer };
|
|
1817
|
+
}
|
|
1818
|
+
if (app.step)
|
|
1819
|
+
steps.push(app.step);
|
|
1820
|
+
blocked.push(...app.blocked);
|
|
1821
|
+
}
|
|
1822
|
+
return { steps, blocked };
|
|
1823
|
+
}
|
|
1492
1824
|
/**
|
|
1493
1825
|
* #173: register the org's own app with the platform's jobs scheduler
|
|
1494
1826
|
* (https://github.com/wtfalch/jobs's admin API, jobs#62), so the scheduler
|
|
@@ -1537,7 +1869,7 @@ function planJobsRegistration(org, recordedIds, jobsAvailable, missingKeyRef = n
|
|
|
1537
1869
|
: `the platform's jobs admin token ${missingKeyRef} is not held yet`}`,
|
|
1538
1870
|
};
|
|
1539
1871
|
}
|
|
1540
|
-
const hostname = org
|
|
1872
|
+
const hostname = appHostname(org);
|
|
1541
1873
|
return {
|
|
1542
1874
|
blocked: null,
|
|
1543
1875
|
step: {
|
|
@@ -1562,7 +1894,10 @@ function planJobsRegistration(org, recordedIds, jobsAvailable, missingKeyRef = n
|
|
|
1562
1894
|
* {@link planApp}.
|
|
1563
1895
|
*/
|
|
1564
1896
|
function planDatabase(org, byKey, claimed) {
|
|
1565
|
-
return planHostingToggle('coolify', 'database',
|
|
1897
|
+
return planHostingToggle('coolify', 'database', org.hosting.entries.some((entry) => entry.database), org, byKey, claimed, () => `provision a database for ${org.slug}'s app`, () => `park ${org.slug}'s database (data is retained, not deleted -- #7 corrections 1)`);
|
|
1898
|
+
}
|
|
1899
|
+
function sortedNames(names) {
|
|
1900
|
+
return names.filter(Boolean).sort().join(',');
|
|
1566
1901
|
}
|
|
1567
1902
|
/**
|
|
1568
1903
|
* A Cloudflare tunnel reaching the org's app (`org.ts`'s
|
|
@@ -1618,7 +1953,14 @@ function planTunnel(org, byKey, claimed, recordedIds) {
|
|
|
1618
1953
|
if (!live)
|
|
1619
1954
|
return null;
|
|
1620
1955
|
const connectorMissing = !recordedIds[CLOUDFLARE_TUNNEL_CONNECTOR_APP_SCOPE_KEY];
|
|
1621
|
-
|
|
1956
|
+
// A hostname added or dropped on an entry: the names published last time
|
|
1957
|
+
// differ from the names now declared. An org that never recorded a list
|
|
1958
|
+
// has nothing to compare, so only the domain check above applies to it.
|
|
1959
|
+
const published = recordedIds[CLOUDFLARE_TUNNEL_DNS_HOSTNAMES_SCOPE_KEY];
|
|
1960
|
+
const namesChanged = published !== undefined &&
|
|
1961
|
+
org.domain !== null &&
|
|
1962
|
+
sortedNames(published.split(',')) !== sortedNames(tunnelDnsHostnames(org, org.domain));
|
|
1963
|
+
const dnsStale = recordedIds[CLOUDFLARE_TUNNEL_DNS_DOMAIN_SCOPE_KEY] !== org.domain || namesChanged;
|
|
1622
1964
|
if (!connectorMissing && !dnsStale)
|
|
1623
1965
|
return null;
|
|
1624
1966
|
return {
|
|
@@ -1762,16 +2104,108 @@ function planIssuerApps(org, byKey, claimed) {
|
|
|
1762
2104
|
}
|
|
1763
2105
|
return steps;
|
|
1764
2106
|
}
|
|
2107
|
+
/**
|
|
2108
|
+
* One ZITADEL web client per entry with `uses.auth`, named by the entry key,
|
|
2109
|
+
* in the org's project. Planned once; its redirect addresses name the entry's
|
|
2110
|
+
* exact hostnames when it is created (`executor.ts`'s `zitadelApp`).
|
|
2111
|
+
*/
|
|
2112
|
+
function planEntryClients(org, byKey, claimed) {
|
|
2113
|
+
const steps = [];
|
|
2114
|
+
for (const entry of org.hosting.entries) {
|
|
2115
|
+
if (!entry.uses?.auth)
|
|
2116
|
+
continue;
|
|
2117
|
+
const key = resourceKey('zitadel', 'app', entry.key);
|
|
2118
|
+
if (byKey.has(key)) {
|
|
2119
|
+
claimed.add(key);
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
steps.push({
|
|
2123
|
+
action: { system: 'zitadel', kind: 'app', key: entry.key, desired: { appType: 'web' } },
|
|
2124
|
+
step: {
|
|
2125
|
+
description: `provision the sign-in client '${entry.key}' for ${org.slug}`,
|
|
2126
|
+
destroys: false,
|
|
2127
|
+
migrates: false,
|
|
2128
|
+
},
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
return steps;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* F5: one broker `AUTH_APPS` entry per entry with `uses.auth`. Idempotent on
|
|
2135
|
+
* the recorded marker of what the last write said (ids, names, origins; never
|
|
2136
|
+
* the key), so a rerun plans nothing. The values it needs come from steps
|
|
2137
|
+
* that run earlier; one that nothing records and no step in this plan will
|
|
2138
|
+
* record blocks it (rule 2). A foreign broker entry the last write found
|
|
2139
|
+
* invalid is listed as a warning: the broker drops it, foundry keeps it.
|
|
2140
|
+
*/
|
|
2141
|
+
function planBrokerEntries(org, recordedIds, plannedSteps, brokerWired) {
|
|
2142
|
+
const steps = [];
|
|
2143
|
+
const blocked = [];
|
|
2144
|
+
for (const entry of org.hosting.entries) {
|
|
2145
|
+
const auth = entry.uses?.auth;
|
|
2146
|
+
if (!auth)
|
|
2147
|
+
continue;
|
|
2148
|
+
const at = `${org.slug}'s ${entry.key} broker entry`;
|
|
2149
|
+
const key = `coolify:broker-entry:${entry.key}`;
|
|
2150
|
+
const warnings = recordedIds[brokerEntryWarningsScopeKey(entry.key)];
|
|
2151
|
+
for (const name of warnings ? JSON.parse(warnings) : []) {
|
|
2152
|
+
blocked.push({
|
|
2153
|
+
description: `warning: the broker's AUTH_APPS entry '${name}' is invalid and the broker drops it; foundry leaves it as it is`,
|
|
2154
|
+
key: `${key}:invalid:${name}`,
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
if (!brokerWired) {
|
|
2158
|
+
blocked.push({
|
|
2159
|
+
description: `${at} is blocked, not planned: the host has not wired the broker port`,
|
|
2160
|
+
key,
|
|
2161
|
+
});
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
const wanted = wantedBrokerEntry(org, entry, recordedIds);
|
|
2165
|
+
if ('want' in wanted) {
|
|
2166
|
+
if (recordedIds[brokerEntryAppliedScopeKey(entry.key)] === JSON.stringify(wanted.want)) {
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
else {
|
|
2171
|
+
const unplanned = wanted.missing.filter(({ producedBy: [system, kind] }) => !plannedSteps.some((p) => p.action.system === system && p.action.kind === kind));
|
|
2172
|
+
if (unplanned.length > 0) {
|
|
2173
|
+
blocked.push({
|
|
2174
|
+
description: `${at} is blocked, not planned: ${unplanned.map((m) => m.what).join(', ')} not recorded yet`,
|
|
2175
|
+
key,
|
|
2176
|
+
});
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
const replaces = auth.replacesBrokerEntry
|
|
2181
|
+
? ` and replace the hand-written entry '${auth.replacesBrokerEntry}'`
|
|
2182
|
+
: '';
|
|
2183
|
+
steps.push({
|
|
2184
|
+
action: {
|
|
2185
|
+
system: 'coolify',
|
|
2186
|
+
kind: 'broker-entry',
|
|
2187
|
+
key: appStepKey(org, entry.key),
|
|
2188
|
+
desired: {},
|
|
2189
|
+
},
|
|
2190
|
+
step: {
|
|
2191
|
+
description: `write ${at} beside the others${replaces}`,
|
|
2192
|
+
destroys: false,
|
|
2193
|
+
migrates: false,
|
|
2194
|
+
},
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
return { steps, blocked };
|
|
2198
|
+
}
|
|
1765
2199
|
/**
|
|
1766
2200
|
* Plan gap A9: an org's customers sign in through a second ZITADEL org with
|
|
1767
|
-
* self-registration on, granted the org's app project. Planned once
|
|
1768
|
-
*
|
|
2201
|
+
* self-registration on, granted the org's app project. Planned once an entry
|
|
2202
|
+
* signs in its customers (`uses.auth.users: 'customers'`).
|
|
1769
2203
|
* Idempotent on the recorded customers org id, like `planOwner`. Once
|
|
1770
2204
|
* recorded, the live project grant to that org is claimed so it is not
|
|
1771
2205
|
* listed as unmanaged.
|
|
1772
2206
|
*/
|
|
1773
2207
|
function planCustomers(org, live, claimed, recordedIds) {
|
|
1774
|
-
if (org.hosting.
|
|
2208
|
+
if (!org.hosting.entries.some((entry) => entry.uses?.auth?.users === 'customers'))
|
|
1775
2209
|
return null;
|
|
1776
2210
|
const customersOrgId = recordedIds[resourceKey('zitadel', 'customers', org.id)];
|
|
1777
2211
|
if (customersOrgId) {
|
|
@@ -1795,24 +2229,31 @@ function planCustomers(org, live, claimed, recordedIds) {
|
|
|
1795
2229
|
}
|
|
1796
2230
|
/**
|
|
1797
2231
|
* #84: a Cloudflare Access gate on the org's app hostname, letting only
|
|
1798
|
-
*
|
|
2232
|
+
* each entry's `accessEmails` through. Idempotent on the recorded Access app id,
|
|
1799
2233
|
* like `planOwner`: Cloudflare's live listing has no Access apps. A later
|
|
1800
2234
|
* change to the email list is not reconciled yet.
|
|
1801
2235
|
*/
|
|
1802
2236
|
function planAccess(org, recordedIds) {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2237
|
+
if (!org.hosting.enabled)
|
|
2238
|
+
return [];
|
|
2239
|
+
// Only the `app` entry has a public name, and so only it can sit behind
|
|
2240
|
+
// Access; per-entry hostnames are issue 309.
|
|
2241
|
+
return org.hosting.entries
|
|
2242
|
+
.filter((entry) => entry.key === APP_ENTRY_KEY && entry.accessEmails.length > 0)
|
|
2243
|
+
.filter((entry) => !recordedIds[resourceKey('cloudflare', 'access', appStepKey(org, entry.key))])
|
|
2244
|
+
.map((entry) => ({
|
|
2245
|
+
action: {
|
|
2246
|
+
system: 'cloudflare',
|
|
2247
|
+
kind: 'access',
|
|
2248
|
+
key: appStepKey(org, entry.key),
|
|
2249
|
+
desired: {},
|
|
2250
|
+
},
|
|
1810
2251
|
step: {
|
|
1811
|
-
description: `put ${org.slug}'s app behind Cloudflare Access`,
|
|
2252
|
+
description: `put ${org.slug}'s ${entry.key === APP_ENTRY_KEY ? 'app' : `${entry.key} app`} behind Cloudflare Access`,
|
|
1812
2253
|
destroys: false,
|
|
1813
2254
|
migrates: false,
|
|
1814
2255
|
},
|
|
1815
|
-
};
|
|
2256
|
+
}));
|
|
1816
2257
|
}
|
|
1817
2258
|
/**
|
|
1818
2259
|
* https://github.com/wtfalch/foundry/issues/43: one Cloudflare rate-limiting
|