@wtfalch/foundry 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/bootstrap.d.ts +12 -8
- package/dist/bootstrap/bootstrap.js +4 -12
- package/dist/bootstrap/declare.js +1 -5
- package/dist/bootstrap/versions.d.ts +8 -0
- package/dist/bootstrap/versions.js +31 -0
- package/dist/calls-sfu-deploy-support.d.ts +1 -1
- package/dist/calls-sfu-deploy-support.js +1 -1
- package/dist/calls-sfu-ssh-adapter.d.ts +1 -1
- package/dist/calls-sfu-ssh-adapter.js +1 -1
- package/dist/calls-sfu.d.ts +5 -4
- package/dist/calls-sfu.js +5 -4
- package/dist/index.d.ts +4 -5
- package/dist/index.js +6 -3
- package/dist/migrations/0024_audit_events.sql +1 -1
- package/dist/migrations/0025_audit_display.sql +1 -1
- package/dist/migrations/0026_audit_erase_email_guard.sql +1 -1
- package/dist/migrations/0027_audit_chain.sql +1 -1
- package/dist/migrations/0028_audit_rls.sql +2 -2
- package/dist/migrations/0029_hosting_entries.sql +109 -0
- package/dist/migrations-dir.d.ts +9 -0
- package/dist/migrations-dir.js +10 -0
- package/dist/org.d.ts +59 -27
- package/dist/org.js +98 -26
- package/dist/plan/calls-matrix.d.ts +2 -0
- package/dist/plan/calls-matrix.js +2 -0
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +100 -32
- package/dist/plan/executor.d.ts +324 -19
- package/dist/plan/executor.js +764 -34
- package/dist/plan/index.d.ts +1 -1
- package/dist/plan/index.js +1 -1
- package/dist/plan/login-host.d.ts +24 -0
- package/dist/plan/login-host.js +21 -0
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/plan-apply.js +26 -10
- package/dist/plan/platform-diff.d.ts +102 -12
- package/dist/plan/platform-diff.js +236 -15
- package/dist/plan/platform-providers.d.ts +43 -1
- package/dist/plan/platform-providers.js +245 -33
- package/dist/plan/zitadel-system-api.d.ts +64 -0
- package/dist/plan/zitadel-system-api.js +125 -0
- package/dist/plan.d.ts +43 -0
- package/dist/plan.js +16 -1
- package/dist/platform.d.ts +1 -1
- package/dist/store/audit.d.ts +13 -17
- package/dist/store/audit.js +14 -18
- package/dist/store/postgres.d.ts +14 -0
- package/dist/store/postgres.js +15 -18
- package/dist/store/schema.d.ts +20 -1
- package/dist/store/schema.js +7 -1
- package/package.json +6 -3
- package/versions.json +1 -1
- package/dist/migrate.d.ts +0 -38
- package/dist/migrate.js +0 -47
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { VERSIONS } from '../bootstrap/versions.js';
|
|
2
|
+
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
|
+
import { VERSION_CHANGE_KIND, } from '../plan.js';
|
|
1
4
|
/**
|
|
2
5
|
* The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
|
|
3
6
|
* mirroring `diff.ts`'s `toPlan`. There is no separate platform-shaped `Plan`
|
|
@@ -12,6 +15,7 @@ export function toPlatformPlan(detail, ownerOrgId) {
|
|
|
12
15
|
orgId: ownerOrgId,
|
|
13
16
|
steps: detail.plannedSteps.map((p) => p.step),
|
|
14
17
|
unmanaged: detail.unmanaged,
|
|
18
|
+
versionChanges: detail.versionChanges,
|
|
15
19
|
};
|
|
16
20
|
}
|
|
17
21
|
function resourceKey(system, kind, key) {
|
|
@@ -25,6 +29,72 @@ function resourceKey(system, kind, key) {
|
|
|
25
29
|
* an org's own (which is keyed by that org's id).
|
|
26
30
|
*/
|
|
27
31
|
export const PLATFORM_AUTH_KEY = 'platform:auth';
|
|
32
|
+
/**
|
|
33
|
+
* #251: the ZITADEL image tag the last successful auth run actually set
|
|
34
|
+
* live. `provisionPlatformAuth` records this only when it set the tag --
|
|
35
|
+
* today that is creation alone, `findOrCreatePlatformApp`'s own
|
|
36
|
+
* `justCreated`, since nothing yet patches `docker_registry_image_tag` on
|
|
37
|
+
* an app found already running (unlike mail, which does). Recording it on
|
|
38
|
+
* every run regardless, the way the app id itself is recorded, would make
|
|
39
|
+
* {@link versionNotAppliedLine} compare the pinned version against a value
|
|
40
|
+
* that was never actually applied.
|
|
41
|
+
*/
|
|
42
|
+
export const PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY = 'auth_zitadel_applied_image_tag';
|
|
43
|
+
/**
|
|
44
|
+
* #251: the Synapse/MAS image tags an org's calls Matrix stack last
|
|
45
|
+
* successful apply actually set live. `coolifyCalls` (`executor.ts`)
|
|
46
|
+
* records these only at creation, for the same reason
|
|
47
|
+
* {@link PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY} is: nothing yet
|
|
48
|
+
* patches either tag on an app `findOrCreateCallsApp` found already
|
|
49
|
+
* running.
|
|
50
|
+
*/
|
|
51
|
+
export const CALLS_SYNAPSE_APPLIED_IMAGE_TAG_SCOPE_KEY = 'calls_synapse_applied_image_tag';
|
|
52
|
+
export const CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY = 'calls_mas_applied_image_tag';
|
|
53
|
+
/**
|
|
54
|
+
* One line for a service whose own step genuinely redeploys when its
|
|
55
|
+
* recorded applied ref differs from the declared or pinned one (mail,
|
|
56
|
+
* archon, ai, files, integrations) -- `from` undefined (nothing applied
|
|
57
|
+
* yet, a first-ever apply) or already equal to `to` means nothing to
|
|
58
|
+
* report.
|
|
59
|
+
*/
|
|
60
|
+
export function versionChangeLine(service, from, to, note) {
|
|
61
|
+
if (!from || from === to)
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
kind: VERSION_CHANGE_KIND,
|
|
65
|
+
service,
|
|
66
|
+
description: `${service}: ${from} -> ${to} (${note})`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One line for a service whose own step never patches a running app's
|
|
71
|
+
* image tag once created -- zitadel, synapse, mas. #251 round 3: a prior
|
|
72
|
+
* attempt worded this line the same as {@link versionChangeLine}'s
|
|
73
|
+
* "X -> Y (image, redeploy)", which promised a redeploy `apply` can never
|
|
74
|
+
* actually perform for these three. `appLive` false means nothing live yet
|
|
75
|
+
* to compare against (no line, the same first-apply case
|
|
76
|
+
* {@link versionChangeLine} has); a live app with no recorded `applied`
|
|
77
|
+
* value predates this tracking and is reported as `running tag unknown`
|
|
78
|
+
* rather than silently showing nothing.
|
|
79
|
+
*/
|
|
80
|
+
export function versionNotAppliedLine(service, appLive, applied, pinned) {
|
|
81
|
+
if (!appLive)
|
|
82
|
+
return null;
|
|
83
|
+
if (!applied) {
|
|
84
|
+
return {
|
|
85
|
+
kind: VERSION_CHANGE_KIND,
|
|
86
|
+
service,
|
|
87
|
+
description: `${service}: pinned ${pinned}, running tag unknown`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (applied === pinned)
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
kind: VERSION_CHANGE_KIND,
|
|
94
|
+
service,
|
|
95
|
+
description: `${service}: pinned ${pinned}, running ${applied} (not applied by foundry)`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
28
98
|
/**
|
|
29
99
|
* Scope keys (colon-free, so never mistaken for a `system:kind:key` resource
|
|
30
100
|
* id) for foundry's ZITADEL admin machine user. The delivered key is the guard
|
|
@@ -67,6 +137,10 @@ export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_a
|
|
|
67
137
|
export const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = 'archon_gate_closed';
|
|
68
138
|
export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
|
|
69
139
|
export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
|
|
140
|
+
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
141
|
+
export const PLATFORM_AUTH_INSTANCE_SCOPE_KEY = 'zitadel_platform_instance_id';
|
|
142
|
+
/** #283: an instance that may exist without a delivered owner token; blocks creating another. */
|
|
143
|
+
export const PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_instance_undelivered';
|
|
70
144
|
/**
|
|
71
145
|
* #216 (building on #209/#215): the `IAM_LOGIN_CLIENT` machine user minted
|
|
72
146
|
* for the login app and the broker, mirroring
|
|
@@ -274,24 +348,34 @@ export function platformMailMxHost(apex) {
|
|
|
274
348
|
export const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = 'mail_applied_apex';
|
|
275
349
|
export const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = 'mail_applied_host';
|
|
276
350
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
|
|
351
|
+
* The key of an org's `app` hosting entry in a `coolify:app` or
|
|
352
|
+
* `cloudflare:access` step and in the ids recorded for it:
|
|
353
|
+
* `<orgId>:<entry key>`, so a second entry cannot collide with the first.
|
|
354
|
+
*/
|
|
355
|
+
export function appStepKey(org) {
|
|
356
|
+
return `${org.id}:${APP_ENTRY_KEY}`;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* #243: the org's `app` entry's `env` values the last `coolify:app` apply
|
|
360
|
+
* pushed, recorded as one JSON object (env name to value; ids and a domain,
|
|
361
|
+
* never a secret). `planApp` plans the step again when {@link resolveAppEnv}
|
|
362
|
+
* differs from it, so a changed map reaches an app that is already live. The
|
|
363
|
+
* `.app` suffix is the entry key; a scope key holds no `:`, so
|
|
364
|
+
* `storeRecordedIds` still filters it out.
|
|
281
365
|
*/
|
|
282
|
-
export const APP_ENV_APPLIED_SCOPE_KEY =
|
|
366
|
+
export const APP_ENV_APPLIED_SCOPE_KEY = `app_env_applied.${APP_ENTRY_KEY}`;
|
|
283
367
|
/**
|
|
284
368
|
* #239: set to '1' once an app has been restarted after its database URLs
|
|
285
369
|
* were pushed. Not a URL. Without it a crash between the push and the restart
|
|
286
370
|
* would leave a stale container that no plan notices.
|
|
287
371
|
*/
|
|
288
|
-
export const DB_ENV_APPLIED_SCOPE_KEY =
|
|
372
|
+
export const DB_ENV_APPLIED_SCOPE_KEY = `db_env_applied.${APP_ENTRY_KEY}`;
|
|
289
373
|
/**
|
|
290
|
-
* The `
|
|
291
|
-
*
|
|
292
|
-
*
|
|
374
|
+
* The entry's `env` values that have a value, in env-name order, and the env
|
|
375
|
+
* names whose source has none yet. Shared by `planApp` and the executor's
|
|
376
|
+
* `pushAppEnv` so both read the same recorded ids.
|
|
293
377
|
*/
|
|
294
|
-
export function resolveAppEnv(org, recorded) {
|
|
378
|
+
export function resolveAppEnv(org, entry, recorded) {
|
|
295
379
|
const sources = {
|
|
296
380
|
filesOrganisationId: recorded[`files:organisation:${org.id}`],
|
|
297
381
|
aiOrganisationId: recorded[`ai:organisation:${org.id}`],
|
|
@@ -299,12 +383,12 @@ export function resolveAppEnv(org, recorded) {
|
|
|
299
383
|
};
|
|
300
384
|
const values = {};
|
|
301
385
|
const unresolved = [];
|
|
302
|
-
for (const [name,
|
|
303
|
-
const value = sources[
|
|
386
|
+
for (const [name, declared] of Object.entries(entry.env).sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
387
|
+
const value = typeof declared === 'string' ? declared : sources[declared.from];
|
|
304
388
|
if (value)
|
|
305
389
|
values[name] = value;
|
|
306
|
-
else
|
|
307
|
-
unresolved.push([name,
|
|
390
|
+
else if (typeof declared !== 'string')
|
|
391
|
+
unresolved.push([name, declared.from]);
|
|
308
392
|
}
|
|
309
393
|
return { values, unresolved };
|
|
310
394
|
}
|
|
@@ -490,6 +574,40 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
490
574
|
const archon = planArchon(platform, recordedIds, blocked.archon, authReady, drifted.archon);
|
|
491
575
|
if (archon.step)
|
|
492
576
|
plannedSteps.push(archon.step);
|
|
577
|
+
// #257: after every service above, so a database any of them just
|
|
578
|
+
// recorded is already visible to `recordedIds` on this same plan.
|
|
579
|
+
const backups = planBackups(byKey, claimed, recordedIds, blocked.backupExecutorMissing ?? false);
|
|
580
|
+
plannedSteps.push(...backups.steps);
|
|
581
|
+
// #251: one line per service whose recorded applied ref no longer matches
|
|
582
|
+
// what the next apply would use (or, for zitadel, what is actually live).
|
|
583
|
+
// Never pushed into `plannedSteps` -- see `VersionChangeItem`'s own doc
|
|
584
|
+
// comment (`plan.ts`) for why. Withheld exactly where that service's own
|
|
585
|
+
// step is blocked: advertising a version it cannot run yet, or cannot be
|
|
586
|
+
// read for, is premature, not informational.
|
|
587
|
+
const versionChanges = [];
|
|
588
|
+
for (const change of [
|
|
589
|
+
mail.blocked
|
|
590
|
+
? null
|
|
591
|
+
: versionChangeLine('mail', recordedIds[PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY], platform.mail.gitRef, 'image, redeploy'),
|
|
592
|
+
archon.blocked
|
|
593
|
+
? null
|
|
594
|
+
: versionChangeLine('archon', recordedIds[PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY], platform.archon.gitRef, 'git ref, redeploy'),
|
|
595
|
+
ai.blocked
|
|
596
|
+
? null
|
|
597
|
+
: versionChangeLine('ai', recordedIds[PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY], platform.ai.gitRef, 'git ref, redeploy'),
|
|
598
|
+
files.blocked
|
|
599
|
+
? null
|
|
600
|
+
: versionChangeLine('files', recordedIds[PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY], platform.files.gitRef, 'git ref, redeploy'),
|
|
601
|
+
integrations.blocked
|
|
602
|
+
? null
|
|
603
|
+
: versionChangeLine('integrations', recordedIds[PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY], platform.integrations.gitRef, 'git ref, redeploy'),
|
|
604
|
+
auth.blocked
|
|
605
|
+
? null
|
|
606
|
+
: versionNotAppliedLine('zitadel', Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_AUTH_KEY)]), recordedIds[PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY], VERSIONS.zitadel),
|
|
607
|
+
]) {
|
|
608
|
+
if (change)
|
|
609
|
+
versionChanges.push(change);
|
|
610
|
+
}
|
|
493
611
|
const unmanaged = live
|
|
494
612
|
.filter((r) => !claimed.has(resourceKey(r.system, r.kind, r.key)))
|
|
495
613
|
.map((r) => ({
|
|
@@ -516,7 +634,8 @@ export function planPlatform(platform, live, options = {}) {
|
|
|
516
634
|
unmanaged.push({ description: ai.blocked, key: 'ai' });
|
|
517
635
|
if (archon.blocked)
|
|
518
636
|
unmanaged.push({ description: archon.blocked, key: 'archon' });
|
|
519
|
-
|
|
637
|
+
unmanaged.push(...backups.blocked);
|
|
638
|
+
return { plannedSteps, unmanaged, versionChanges };
|
|
520
639
|
}
|
|
521
640
|
/**
|
|
522
641
|
* The platform's own Cloudflare zone for its shared apex (`platform.ts`'s
|
|
@@ -610,6 +729,98 @@ function planJobs(platform, byKey, claimed, observable) {
|
|
|
610
729
|
},
|
|
611
730
|
};
|
|
612
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* #257: the six platform databases foundry itself creates, each keyed by
|
|
734
|
+
* the scope id `platform-providers.ts`'s own `createPostgresDatabase` call
|
|
735
|
+
* site records once that service's database exists --
|
|
736
|
+
* `JOBS_DATABASE_SCOPE_KEY`, `PLATFORM_AUTH_DB_SCOPE_KEY`,
|
|
737
|
+
* `PLATFORM_ARCHON_DB_SCOPE_KEY`, `PLATFORM_AI_DB_SCOPE_KEY`,
|
|
738
|
+
* `PLATFORM_FILES_DB_SCOPE_KEY`, `INTEGRATIONS_DATABASE_SCOPE_KEY`. Those
|
|
739
|
+
* constants stay module-private in `platform-providers.ts`, grouped with
|
|
740
|
+
* each service's other scope keys; the string values here are a second,
|
|
741
|
+
* documented copy rather than an import, so `planBackups` below never
|
|
742
|
+
* reaches into that file and `platform-providers.ts` keeps importing *from*
|
|
743
|
+
* this one, never the other way (every other scope key constant already
|
|
744
|
+
* flows that direction). `platform-providers.test.ts`'s own
|
|
745
|
+
* `coolify:backup` suite exercises both the executor's real constants and
|
|
746
|
+
* this planner end to end, so a renamed key on either side shows up as a
|
|
747
|
+
* database the planner can no longer see -- reported blocked, per rule 2,
|
|
748
|
+
* never a wrong step.
|
|
749
|
+
*/
|
|
750
|
+
export const PLATFORM_BACKUP_DATABASE_SCOPE_KEYS = {
|
|
751
|
+
jobs: 'coolify_jobs_database',
|
|
752
|
+
auth: 'coolify_platform_auth_database',
|
|
753
|
+
archon: 'coolify_platform_archon_database',
|
|
754
|
+
ai: 'coolify_platform_ai_database',
|
|
755
|
+
files: 'coolify_platform_files_database',
|
|
756
|
+
integrations: 'coolify_integrations_database',
|
|
757
|
+
};
|
|
758
|
+
export const PLATFORM_BACKUP_SERVICE_KEYS = [
|
|
759
|
+
'jobs',
|
|
760
|
+
'auth',
|
|
761
|
+
'archon',
|
|
762
|
+
'ai',
|
|
763
|
+
'files',
|
|
764
|
+
'integrations',
|
|
765
|
+
];
|
|
766
|
+
/**
|
|
767
|
+
* #257: a nightly backup for each platform database foundry itself creates,
|
|
768
|
+
* one `coolify:backup:<service>` step per service -- unconditional, the
|
|
769
|
+
* same "no enabled toggle, always wanted" shape {@link planApexZone} has,
|
|
770
|
+
* since a database that exists always wants a backup whether or not its own
|
|
771
|
+
* service is currently `enabled` (platform-steps rule 2, this repo's
|
|
772
|
+
* CLAUDE.md: blocked, not failing). A service with no database recorded yet
|
|
773
|
+
* has nothing to back up, so it is reported blocked rather than planned --
|
|
774
|
+
* the same way `planJobs` blocks rather than plans with no Coolify reader --
|
|
775
|
+
* and so is every service once a database exists but `executorMissing` says
|
|
776
|
+
* the host built the platform executor without backup dependencies (the
|
|
777
|
+
* R2/S3 credentials `FoundryPlatformStepExecutorOptions.backup` carries).
|
|
778
|
+
* Once a step's own `coolify:backup:<service>` resource is live, it is
|
|
779
|
+
* claimed and never re-planned: like every other provision-only step here,
|
|
780
|
+
* there is no park branch for a backup already scheduled.
|
|
781
|
+
*/
|
|
782
|
+
function planBackups(byKey, claimed, recordedIds, executorMissing) {
|
|
783
|
+
const steps = [];
|
|
784
|
+
const blocked = [];
|
|
785
|
+
for (const service of PLATFORM_BACKUP_SERVICE_KEYS) {
|
|
786
|
+
const key = resourceKey('coolify', 'backup', service);
|
|
787
|
+
const live = byKey.get(key);
|
|
788
|
+
if (live) {
|
|
789
|
+
claimed.add(key);
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
// No database yet is not reported: unlike a Coolify reader or a backup
|
|
793
|
+
// executor missing (below), there is nothing here for a host to fix --
|
|
794
|
+
// the database arrives on its own once that service's own step is
|
|
795
|
+
// enabled and applied, and this step simply follows next time. Reporting
|
|
796
|
+
// it would make every platform with a service not yet applied carry a
|
|
797
|
+
// standing "blocked" note nobody can act on.
|
|
798
|
+
const databaseId = recordedIds[PLATFORM_BACKUP_DATABASE_SCOPE_KEYS[service]];
|
|
799
|
+
if (!databaseId)
|
|
800
|
+
continue;
|
|
801
|
+
if (executorMissing) {
|
|
802
|
+
blocked.push({
|
|
803
|
+
description: `platform:backup:${service} is blocked, not planned: the host built the platform executor without its backup dependencies`,
|
|
804
|
+
key: `backup:${service}`,
|
|
805
|
+
});
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
steps.push({
|
|
809
|
+
action: {
|
|
810
|
+
system: 'coolify',
|
|
811
|
+
kind: 'backup',
|
|
812
|
+
key: service,
|
|
813
|
+
desired: { databaseId },
|
|
814
|
+
},
|
|
815
|
+
step: {
|
|
816
|
+
description: `schedule a nightly R2 backup of the platform's ${service} database`,
|
|
817
|
+
destroys: false,
|
|
818
|
+
migrates: false,
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
return { steps, blocked };
|
|
823
|
+
}
|
|
613
824
|
/**
|
|
614
825
|
* The platform's shared calls SFU (#170), keyed `sfu:livekit:platform` --
|
|
615
826
|
* there is exactly one, so `key` names the platform itself rather than a
|
|
@@ -803,6 +1014,16 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
|
|
|
803
1014
|
blocked: `the platform's sign-in is blocked, not planned: the admin key of ZITADEL machine user ${undelivered} was minted but never delivered and cannot be shown again; delete that user in ZITADEL and remove the recorded key ${PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY} to mint a new one`,
|
|
804
1015
|
};
|
|
805
1016
|
}
|
|
1017
|
+
// #283: an instance may exist whose owner token never reached the host.
|
|
1018
|
+
const instanceLost = recordedIds[PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY];
|
|
1019
|
+
if (instanceLost &&
|
|
1020
|
+
!recordedIds[PLATFORM_AUTH_INSTANCE_SCOPE_KEY] &&
|
|
1021
|
+
!recordedIds[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY]) {
|
|
1022
|
+
return {
|
|
1023
|
+
step: null,
|
|
1024
|
+
blocked: `the platform's sign-in is blocked, not planned: a ZITADEL instance (${instanceLost}) may exist without a delivered owner token; delete it with the system API (DELETE /system/v1/instances/{id}) or redeploy the ZITADEL database, then remove the recorded key ${PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY}`,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
806
1027
|
// #211 round 6: the executor itself is unusable on this host -- nothing to
|
|
807
1028
|
// plan, whatever the broker's recorded state says.
|
|
808
1029
|
if (executorMissing)
|
|
@@ -102,6 +102,13 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
102
102
|
* -- optional for the same reason as {@link auth}.
|
|
103
103
|
*/
|
|
104
104
|
archon?: PlatformArchonDeps;
|
|
105
|
+
/**
|
|
106
|
+
* #257: dependencies for every `coolify:backup:<service>` step -- optional
|
|
107
|
+
* for the same reason as `auth`; a service whose database already exists
|
|
108
|
+
* without this set has its backup step reported blocked rather than
|
|
109
|
+
* planned.
|
|
110
|
+
*/
|
|
111
|
+
backup?: PlatformBackupDeps;
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
107
114
|
* The real {@link PlatformStepExecutor} for the two steps built so far:
|
|
@@ -159,6 +166,10 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
159
166
|
* #175 adds `coolify:app` keyed {@link PLATFORM_ARCHON_KEY}, Archon itself
|
|
160
167
|
* ({@link provisionPlatformArchon}), once a host passes
|
|
161
168
|
* {@link PlatformArchonDeps}.
|
|
169
|
+
*
|
|
170
|
+
* #257 adds `coolify:backup:<service>`, a nightly R2 backup of each
|
|
171
|
+
* platform database ({@link provisionPlatformBackup}), once a host passes
|
|
172
|
+
* {@link PlatformBackupDeps}.
|
|
162
173
|
*/
|
|
163
174
|
export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor {
|
|
164
175
|
private readonly cloudflare;
|
|
@@ -172,6 +183,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
172
183
|
private readonly files?;
|
|
173
184
|
private readonly ai?;
|
|
174
185
|
private readonly archon?;
|
|
186
|
+
private readonly backup?;
|
|
175
187
|
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list'>, options?: FoundryPlatformStepExecutorOptions);
|
|
176
188
|
blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
|
|
177
189
|
auth?: string;
|
|
@@ -182,6 +194,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
182
194
|
ai?: string;
|
|
183
195
|
archon?: string;
|
|
184
196
|
mail?: string;
|
|
197
|
+
backupExecutorMissing?: boolean;
|
|
185
198
|
}>;
|
|
186
199
|
/**
|
|
187
200
|
* Whether Archon's live Access policy lets through anyone but the declared
|
|
@@ -210,6 +223,13 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
210
223
|
private executeSfu;
|
|
211
224
|
private coolifyJobs;
|
|
212
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
|
|
228
|
+
* delivered under, once. A host reads it back to build `zitadelForInstance`
|
|
229
|
+
* (see {@link provisionPlatformAuth}'s doc comment). Distinct from the admin
|
|
230
|
+
* key delivered under `zitadel:platform:admin`.
|
|
231
|
+
*/
|
|
232
|
+
export declare const PLATFORM_AUTH_INSTANCE_BEARER_REF = "zitadel:platform:instanceBearer";
|
|
213
233
|
/**
|
|
214
234
|
* The fixed description Stalwart's admin API key is minted under
|
|
215
235
|
* (`StalwartClient.createApiKey`'s own signature returns only the secret,
|
|
@@ -580,6 +600,28 @@ export declare const INTEGRATIONS_MANAGEMENT_ISSUER = "foundry";
|
|
|
580
600
|
* its signed request; fixed, since there is one key until rotation is built.
|
|
581
601
|
*/
|
|
582
602
|
export declare const INTEGRATIONS_MANAGEMENT_KEY_ID = "foundry-1";
|
|
603
|
+
/**
|
|
604
|
+
* #257: everything {@link provisionPlatformBackup} needs beyond
|
|
605
|
+
* {@link PlatformStepExecutionContext} -- the narrow Coolify slice for the
|
|
606
|
+
* backup calls themselves, and the R2 (or any S3-compatible) bucket every
|
|
607
|
+
* platform database's dump lands in. A person creates or confirms the
|
|
608
|
+
* bucket and these credentials by hand (the issue's own "Human steps"); this
|
|
609
|
+
* package only ever registers the bucket with Coolify and schedules the
|
|
610
|
+
* dump, never creates or reads the bucket itself.
|
|
611
|
+
*/
|
|
612
|
+
export interface PlatformBackupDeps {
|
|
613
|
+
coolify: Pick<CoolifyPort, 'listS3Storages' | 'createS3Storage' | 'listDatabaseBackups' | 'createDatabaseBackup'>;
|
|
614
|
+
r2: {
|
|
615
|
+
endpoint: string;
|
|
616
|
+
bucket: string;
|
|
617
|
+
accessKeyId: string;
|
|
618
|
+
secretAccessKey: string;
|
|
619
|
+
/** R2 takes `'auto'` (`CoolifyPort.createS3Storage`'s own doc comment). Default `'auto'`. */
|
|
620
|
+
region?: string;
|
|
621
|
+
};
|
|
622
|
+
/** A cron string or one of Coolify's names (`daily`, `weekly`, ...). Default `'daily'`. */
|
|
623
|
+
frequency?: string;
|
|
624
|
+
}
|
|
583
625
|
/**
|
|
584
626
|
* The real {@link PlatformCloudflareReader}: the platform's apex zone as
|
|
585
627
|
* `planApexZone` expects to see it, and nothing else. The zone's DNS records
|
|
@@ -609,7 +651,7 @@ export declare class ProvidersPlatformCloudflareReader implements PlatformCloudf
|
|
|
609
651
|
export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyReader {
|
|
610
652
|
private readonly client;
|
|
611
653
|
private readonly platformStore;
|
|
612
|
-
constructor(client: Pick<CoolifyPort, 'getApplication'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
|
|
654
|
+
constructor(client: Pick<CoolifyPort, 'getApplication' | 'listDatabaseBackups'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
|
|
613
655
|
listResources(): Promise<LiveResource[]>;
|
|
614
656
|
}
|
|
615
657
|
export {};
|