@wtfalch/foundry 0.38.0 → 0.39.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/versions.d.ts +3 -2
- package/dist/plan/backup-gate.d.ts +16 -0
- package/dist/plan/backup-gate.js +40 -0
- package/dist/plan/company-backups.js +15 -0
- package/dist/plan/diff.d.ts +4 -0
- package/dist/plan/diff.js +15 -1
- package/dist/plan/executor.d.ts +13 -0
- package/dist/plan/executor.js +49 -12
- package/dist/plan/own-diff.d.ts +26 -0
- package/dist/plan/own-diff.js +59 -2
- package/dist/plan/own-services.d.ts +10 -0
- package/dist/plan/own-services.js +10 -0
- package/dist/plan/plan-apply.d.ts +4 -0
- package/dist/plan/plan-apply.js +23 -1
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +82 -28
- package/dist/plan/signin-db-backup.d.ts +105 -0
- package/dist/plan/signin-db-backup.js +193 -0
- package/dist/plan/signin-db-move.d.ts +68 -0
- package/dist/plan/signin-db-move.js +338 -0
- package/dist/plan/signin-db-tls.d.ts +69 -0
- package/dist/plan/signin-db-tls.js +255 -0
- package/dist/plan/signin-db.d.ts +114 -0
- package/dist/plan/signin-db.js +255 -0
- package/package.json +1 -1
- package/versions.digests.json +2 -1
- package/versions.json +2 -1
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* The estate's pinned third-party image/release refs for the platform
|
|
3
3
|
* services foundry provisions (#247). One JSON file, so Renovate can read
|
|
4
4
|
* and later propose bumps against it, instead of four separate string
|
|
5
|
-
* constants it cannot see.
|
|
6
|
-
* pins a postgres image tag today, so there is no ref to record yet.
|
|
5
|
+
* constants it cannot see.
|
|
7
6
|
*/
|
|
8
7
|
export interface PinnedVersions {
|
|
9
8
|
zitadel: string;
|
|
@@ -17,6 +16,8 @@ export interface PinnedVersions {
|
|
|
17
16
|
alpine: string;
|
|
18
17
|
/** A static busybox, copied into the MAS image so its health check has an HTTP client. */
|
|
19
18
|
busybox: string;
|
|
19
|
+
/** The sign-in database's image (#746). Its major version is the data directory's format: a bump past 16 needs a dump and restore, not an image change. */
|
|
20
|
+
postgres: string;
|
|
20
21
|
}
|
|
21
22
|
export declare function loadVersions(): PinnedVersions;
|
|
22
23
|
export declare const VERSIONS: PinnedVersions;
|
|
@@ -28,6 +28,15 @@ export declare class NetworkMoveBackupMissingError extends Error {
|
|
|
28
28
|
readonly reason: 'no-gate' | 'no-backup' | 'by-hand';
|
|
29
29
|
constructor(reason: 'no-gate' | 'no-backup' | 'by-hand');
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The move of a company's sign-in database to the TLS image (#746) is refused before any step runs when
|
|
33
|
+
* the old database has no finished backup from the last {@link BACKUP_MAX_AGE_HOURS} hours, or the host
|
|
34
|
+
* has no backup gate. The message names no database, uuid or URL. There is no override.
|
|
35
|
+
*/
|
|
36
|
+
export declare class SigninDbMoveBackupMissingError extends Error {
|
|
37
|
+
readonly reason: 'no-gate' | 'no-backup';
|
|
38
|
+
constructor(reason: 'no-gate' | 'no-backup');
|
|
39
|
+
}
|
|
31
40
|
export interface BackupGatePorts {
|
|
32
41
|
/** The company's recorded ids (`AppliedState.serviceIds`): database uuids and `backup_<db>` schedule uuids. */
|
|
33
42
|
recordedIds: Readonly<Record<string, string>>;
|
|
@@ -67,6 +76,13 @@ export declare function requireFreshBackupsForUpgrades(plan: {
|
|
|
67
76
|
}, confirmed: readonly string[] | undefined, ports: BackupGatePorts): Promise<{
|
|
68
77
|
backupId: string | null;
|
|
69
78
|
}>;
|
|
79
|
+
/**
|
|
80
|
+
* The sign-in database move's gate (#746): the company's recorded `auth` database needs a schedule and a
|
|
81
|
+
* `success` execution not older than {@link BACKUP_MAX_AGE_HOURS} hours, read from Coolify's executions.
|
|
82
|
+
* A schedule alone proves no dump ran, so a Coolify with no executions route refuses too. Returns the
|
|
83
|
+
* uuid of the execution that passed.
|
|
84
|
+
*/
|
|
85
|
+
export declare function requireFreshBackupForSigninMove(ports: BackupGatePorts): Promise<string>;
|
|
70
86
|
export interface MoveGatePorts {
|
|
71
87
|
/** The company's Coolify project (`coolify_project`). */
|
|
72
88
|
projectUuid: string | undefined;
|
package/dist/plan/backup-gate.js
CHANGED
|
@@ -44,6 +44,21 @@ export class NetworkMoveBackupMissingError extends Error {
|
|
|
44
44
|
this.name = 'NetworkMoveBackupMissingError';
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The move of a company's sign-in database to the TLS image (#746) is refused before any step runs when
|
|
49
|
+
* the old database has no finished backup from the last {@link BACKUP_MAX_AGE_HOURS} hours, or the host
|
|
50
|
+
* has no backup gate. The message names no database, uuid or URL. There is no override.
|
|
51
|
+
*/
|
|
52
|
+
export class SigninDbMoveBackupMissingError extends Error {
|
|
53
|
+
reason;
|
|
54
|
+
constructor(reason) {
|
|
55
|
+
super(reason === 'no-gate'
|
|
56
|
+
? 'sign-in database move refused: this host has no backup gate, so a fresh backup cannot be checked'
|
|
57
|
+
: `sign-in database move refused: the old sign-in database has no finished backup from the last ${BACKUP_MAX_AGE_HOURS} hours, checked in Coolify; take one now and apply again`);
|
|
58
|
+
this.reason = reason;
|
|
59
|
+
this.name = 'SigninDbMoveBackupMissingError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
47
62
|
async function withinTimeout(call) {
|
|
48
63
|
let timer;
|
|
49
64
|
try {
|
|
@@ -184,6 +199,31 @@ export async function requireFreshBackupsForUpgrades(plan, confirmed, ports) {
|
|
|
184
199
|
}
|
|
185
200
|
return { backupId };
|
|
186
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* The sign-in database move's gate (#746): the company's recorded `auth` database needs a schedule and a
|
|
204
|
+
* `success` execution not older than {@link BACKUP_MAX_AGE_HOURS} hours, read from Coolify's executions.
|
|
205
|
+
* A schedule alone proves no dump ran, so a Coolify with no executions route refuses too. Returns the
|
|
206
|
+
* uuid of the execution that passed.
|
|
207
|
+
*/
|
|
208
|
+
export async function requireFreshBackupForSigninMove(ports) {
|
|
209
|
+
const now = (ports.now ?? Date.now)();
|
|
210
|
+
const databaseUuid = ports.recordedIds[companyDatabaseKeys({ id: ports.orgId }).auth];
|
|
211
|
+
const backupUuid = ports.recordedIds[backupScopeKey('auth')];
|
|
212
|
+
if (!databaseUuid || !backupUuid)
|
|
213
|
+
throw new SigninDbMoveBackupMissingError('no-backup');
|
|
214
|
+
let executions;
|
|
215
|
+
try {
|
|
216
|
+
executions = await readExecutions(ports.coolify, databaseUuid, backupUuid);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// An unreadable list is not a fresh backup. The cause stays out: it can carry a response body.
|
|
220
|
+
throw new SigninDbMoveBackupMissingError('no-backup');
|
|
221
|
+
}
|
|
222
|
+
const fresh = executions ? freshSuccess(executions, now) : null;
|
|
223
|
+
if (!fresh)
|
|
224
|
+
throw new SigninDbMoveBackupMissingError('no-backup');
|
|
225
|
+
return fresh.uuid;
|
|
226
|
+
}
|
|
187
227
|
/**
|
|
188
228
|
* The gate for a move to the company's own network, which deletes every database of the project. It
|
|
189
229
|
* fails closed and is stricter than the upgrade gate: it reads the project itself, not foundry's records.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BOULE_DATABASE_SCOPE_KEY } from './boule.js';
|
|
2
2
|
import { CHAT_REKEY_MAS_DB_SCOPE_KEY, CHAT_REKEY_SYNAPSE_DB_SCOPE_KEY } from './executor.js';
|
|
3
3
|
import { MAIL_DATABASE_NAME, MAIL_DATABASE_SCOPE_KEY } from './mail-store.js';
|
|
4
|
+
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
5
|
+
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db-tls.js';
|
|
4
6
|
/**
|
|
5
7
|
* F6: the Postgres databases of a company that get a nightly backup, and the
|
|
6
8
|
* recorded id each one's uuid sits under. The keys are second, documented
|
|
@@ -101,5 +103,18 @@ export function planCompanyBackups(org, recordedIds, storageUuid) {
|
|
|
101
103
|
},
|
|
102
104
|
});
|
|
103
105
|
}
|
|
106
|
+
const oldDatabase = recordedIds[companyDatabaseKeys(org).auth];
|
|
107
|
+
if (recordedIds[SIGNIN_DB_TLS_SCOPE_KEY] && oldDatabase) {
|
|
108
|
+
blocked.push({
|
|
109
|
+
description: `${org.slug}'s old Coolify sign-in database (${oldDatabase}) is no longer used; delete it by hand in Coolify when you no longer want it as a fallback, foundry never deletes it`,
|
|
110
|
+
key: 'signin-db-old-database',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (signinDbBackupMissing(recordedIds)) {
|
|
114
|
+
blocked.push({
|
|
115
|
+
description: `${org.slug}'s sign-in database runs as foundry's own image, which Coolify's database backups do not cover: its nightly dump is not scheduled yet, so no backup covers it`,
|
|
116
|
+
key: 'backup-not-covered:signin-db',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
104
119
|
return { steps, blocked };
|
|
105
120
|
}
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -69,6 +69,10 @@ export interface PlanOrgOptions {
|
|
|
69
69
|
ownMailKeyHeld?: boolean;
|
|
70
70
|
/** The executor can mint an own org's Lettermint key (`FoundryStepExecutor.ownMailKeyMintable`). */
|
|
71
71
|
ownMailKeyMintable?: boolean;
|
|
72
|
+
/** The executor can store the sign-in database's CA and certificate (`FoundryStepExecutor.ownSigninDbKeysPuttable`). Absent: true. */
|
|
73
|
+
ownSigninDbKeysPuttable?: boolean;
|
|
74
|
+
/** The host supplies the backup bucket the sign-in database dumps into (`FoundryStepExecutor.ownSigninDbBackupConfigured`). Absent: true. */
|
|
75
|
+
ownSigninDbBackupConfigured?: boolean;
|
|
72
76
|
/** The own jobs admin token's held-key ref holds a value. */
|
|
73
77
|
ownJobsTokenHeld?: boolean;
|
|
74
78
|
/** The own media server's key pair ref holds a value. */
|
package/dist/plan/diff.js
CHANGED
|
@@ -16,7 +16,7 @@ import { LOGIN_BINDING_SCOPE_KEY, agoraIssuerHostFor, companyLoginTunnelHost, lo
|
|
|
16
16
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
|
|
17
17
|
import { mailStoreSwitchChange } from './mail-store.js';
|
|
18
18
|
import { hasAdoptedResources, networkMoveNeeded, planNetworkMove } from './network-move.js';
|
|
19
|
-
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 { 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, OWN_SIGNIN_DB_MOVE_BLOCKED_KEY, planOwnAuth, planOwnFiles, planOwnFilesR2, planOwnJobs, planOwnMail, planOwnSfu, planSigninDbMove, } from './own-diff.js';
|
|
20
20
|
import { ownDnsHostnames } from './own-dns.js';
|
|
21
21
|
import { ownHostnames } from './own-hostnames.js';
|
|
22
22
|
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';
|
|
@@ -197,9 +197,20 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
197
197
|
oikosZoneReady: options.oikosZoneReady ?? false,
|
|
198
198
|
mailKeyHeld: options.ownMailKeyHeld ?? false,
|
|
199
199
|
mailKeyMintable: options.ownMailKeyMintable ?? false,
|
|
200
|
+
now: (options.now ?? Date.now)(),
|
|
201
|
+
signinDbKeysPuttable: options.ownSigninDbKeysPuttable ?? true,
|
|
202
|
+
signinDbBackupConfigured: options.ownSigninDbBackupConfigured ?? true,
|
|
200
203
|
});
|
|
201
204
|
if (ownAuth.step)
|
|
202
205
|
plannedSteps.push(ownAuth.step);
|
|
206
|
+
const signinDbMove = planSigninDbMove({
|
|
207
|
+
org,
|
|
208
|
+
recordedIds,
|
|
209
|
+
signinDbKeysPuttable: options.ownSigninDbKeysPuttable ?? true,
|
|
210
|
+
signinDbBackupConfigured: options.ownSigninDbBackupConfigured ?? true,
|
|
211
|
+
});
|
|
212
|
+
if (signinDbMove.step)
|
|
213
|
+
plannedSteps.push(signinDbMove.step);
|
|
203
214
|
// An own org's ZITADEL is the company's own instance: nothing that needs it plans before it exists.
|
|
204
215
|
const ownSignInPending = ownsServices(org) && !ownAuthApplied(org, recordedIds);
|
|
205
216
|
const issuer = planIssuer(org, byKey, claimed, platformReadiness.authReady);
|
|
@@ -610,6 +621,9 @@ export function computePlanDetail(org, live, recordedIds = {}, agoraHost = NO_AG
|
|
|
610
621
|
unmanaged.push(...edgeReservedLabels.blocked);
|
|
611
622
|
if (ownAuth.blocked)
|
|
612
623
|
unmanaged.push({ description: ownAuth.blocked, key: OWN_AUTH_BLOCKED_KEY });
|
|
624
|
+
if (signinDbMove.blocked) {
|
|
625
|
+
unmanaged.push({ description: signinDbMove.blocked, key: OWN_SIGNIN_DB_MOVE_BLOCKED_KEY });
|
|
626
|
+
}
|
|
613
627
|
if (ownJobs.blocked)
|
|
614
628
|
unmanaged.push({ description: ownJobs.blocked, key: OWN_JOBS_BLOCKED_KEY });
|
|
615
629
|
if (ownMail.blocked)
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -325,6 +325,8 @@ export interface CoolifyPort {
|
|
|
325
325
|
description?: string;
|
|
326
326
|
instant_deploy?: boolean;
|
|
327
327
|
custom_docker_run_options?: string;
|
|
328
|
+
/** `false`: no domain is made for the app. Coolify makes one by default, which would route to the app's port. */
|
|
329
|
+
autogenerate_domain?: boolean;
|
|
328
330
|
}): Promise<{
|
|
329
331
|
uuid: string;
|
|
330
332
|
}>;
|
|
@@ -1828,6 +1830,10 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
1828
1830
|
private executeScoped;
|
|
1829
1831
|
/** Whether `ensureOwnAuthMailToken` can mint: the same two inputs it blocks on. */
|
|
1830
1832
|
ownMailKeyMintable(): boolean;
|
|
1833
|
+
/** Whether the held keys can store a new company's sign-in database CA and certificate (#746). */
|
|
1834
|
+
ownSigninDbKeysPuttable(): boolean;
|
|
1835
|
+
/** Whether the host supplies the backup bucket the sign-in database dumps into (#746). */
|
|
1836
|
+
ownSigninDbBackupConfigured(): boolean;
|
|
1831
1837
|
/** What the executor's own files dependencies supply, for the planner's blocked lines. */
|
|
1832
1838
|
ownFilesDeps(): {
|
|
1833
1839
|
keys: boolean;
|
|
@@ -2927,6 +2933,13 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2927
2933
|
* declared auth ref patches both git apps' branch and redeploys them; the
|
|
2928
2934
|
* applied marker comes last, so a run that died halfway is planned again.
|
|
2929
2935
|
*/
|
|
2936
|
+
/**
|
|
2937
|
+
* The sign-in ports of one company's steps: its own network and limits, a SMTP target without the
|
|
2938
|
+
* platform's password, and every secret the step reads registered for redaction.
|
|
2939
|
+
*/
|
|
2940
|
+
private ownAuthDeps;
|
|
2941
|
+
/** `plan/signin-db-move.ts`: copies a company's sign-in data from its Coolify database to the TLS database. */
|
|
2942
|
+
private ownAuthDbMove;
|
|
2930
2943
|
private ownAuth;
|
|
2931
2944
|
/**
|
|
2932
2945
|
* `plan/own-diff.ts`'s `planOwnMail`: the org's own Stalwart in the org's own
|
package/dist/plan/executor.js
CHANGED
|
@@ -44,6 +44,7 @@ import { CALLS_MAS_APPLIED_IMAGE_TAG_SCOPE_KEY, CALLS_SYNAPSE_APPLIED_IMAGE_TAG_
|
|
|
44
44
|
import { AUTH_LOGIN_PORT, PLATFORM_AUTH_BROKER_MAIL_KEY_REF, PLATFORM_AUTH_LOGIN_INTERNAL_NAME, PLATFORM_AUTH_LOGIN_SCOPE_KEY, PLATFORM_MAIL_SERVER_SCOPE_KEY, defaultStalwartForInstance, filesDepsInvalid, filesShareDomains, provisionAuthStack, provisionDatabaseBackup, provisionFilesHost, provisionJobsHost, provisionMailServer, resolveOwnFilesKeys, } from './platform-providers.js';
|
|
45
45
|
import { CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY, CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY, ENTRY_CONTAINER_LABELS, appLabelsAppliedScopeKey, entryLabelHash, entryLabelSpec, entryOikosHostnames, entryProxyLabels, entryRoutes, entryRoutesMarker, ownsOikosName, routesWithoutToken, sameLabelSets, } from './proxy-labels.js';
|
|
46
46
|
import { ZITADEL_ORG_DOMAIN_SCOPE_KEY, ZITADEL_ORG_REGISTRATION_CLOSED_SCOPE_KEY, closeSignInOrgRegistration, ensureSignInOrg, inviteOwnerWithLink, registerSignInDomainOrBlock, signInOrgDomain, } from './sign-in-org.js';
|
|
47
|
+
import { moveSigninDatabase } from './signin-db-move.js';
|
|
47
48
|
import { entryWorkerAppliedScopeKey, entryWorkerExclusionsScopeKey, entryWorkerRetiredScopeKey, entryWorkerRouteIdScopeKey, entryWorkerRoutePatternScopeKey, entryWorkerRoutePendingScopeKey, entryWorkerRouteZoneScopeKey, entryWorkerUploadedScopeKey, } from './worker.js';
|
|
48
49
|
/**
|
|
49
50
|
* The real {@link StepExecutor} (#20 "provision Company Factory end to
|
|
@@ -940,6 +941,15 @@ export class FoundryStepExecutor {
|
|
|
940
941
|
ownMailKeyMintable() {
|
|
941
942
|
return Boolean(this.ports.lettermint && this.ports.ownServices?.provision.auth.heldKeys);
|
|
942
943
|
}
|
|
944
|
+
/** Whether the held keys can store a new company's sign-in database CA and certificate (#746). */
|
|
945
|
+
ownSigninDbKeysPuttable() {
|
|
946
|
+
return Boolean(this.ports.ownServices?.provision.auth.heldKeys?.put);
|
|
947
|
+
}
|
|
948
|
+
/** Whether the host supplies the backup bucket the sign-in database dumps into (#746). */
|
|
949
|
+
ownSigninDbBackupConfigured() {
|
|
950
|
+
const files = this.ports.ownServices?.provision.files;
|
|
951
|
+
return Boolean(files?.r2Buckets && files.r2Keys);
|
|
952
|
+
}
|
|
943
953
|
/** What the executor's own files dependencies supply, for the planner's blocked lines. */
|
|
944
954
|
ownFilesDeps() {
|
|
945
955
|
const files = this.ports.ownServices?.provision.files;
|
|
@@ -1132,6 +1142,8 @@ export class FoundryStepExecutor {
|
|
|
1132
1142
|
return this.jobsRegistration(action, org, context);
|
|
1133
1143
|
case 'coolify:own-auth':
|
|
1134
1144
|
return this.ownAuth(action, org, context);
|
|
1145
|
+
case 'coolify:own-auth-db-move':
|
|
1146
|
+
return this.ownAuthDbMove(org, context);
|
|
1135
1147
|
case 'coolify:own-jobs':
|
|
1136
1148
|
return this.ownJobs(action, org, context);
|
|
1137
1149
|
case 'coolify:own-mail':
|
|
@@ -5709,30 +5721,55 @@ export class FoundryStepExecutor {
|
|
|
5709
5721
|
* declared auth ref patches both git apps' branch and redeploys them; the
|
|
5710
5722
|
* applied marker comes last, so a run that died halfway is planned again.
|
|
5711
5723
|
*/
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
const gitRef = action.desired.gitRef;
|
|
5718
|
-
const hn = ownHostnames(org);
|
|
5719
|
-
const scope = scopeFromOrg(context, {});
|
|
5724
|
+
/**
|
|
5725
|
+
* The sign-in ports of one company's steps: its own network and limits, a SMTP target without the
|
|
5726
|
+
* platform's password, and every secret the step reads registered for redaction.
|
|
5727
|
+
*/
|
|
5728
|
+
ownAuthDeps(auth, org, context) {
|
|
5720
5729
|
// The target (host, port, tls, user) is not secret; the platform's SMTP
|
|
5721
5730
|
// password is, so the company's own token is the password instead.
|
|
5722
|
-
const { smtp, ...authDeps } =
|
|
5723
|
-
const
|
|
5731
|
+
const { smtp, signinDbBackup: _platformBackup, ...authDeps } = auth;
|
|
5732
|
+
const files = this.ports.ownServices?.provision.files;
|
|
5733
|
+
return {
|
|
5724
5734
|
...authDeps,
|
|
5735
|
+
// The company's own bucket and key, through the files host's ports; never a platform-wide backup key.
|
|
5736
|
+
signinDbBackup: files?.r2Buckets && files.r2Keys
|
|
5737
|
+
? { r2Buckets: files.r2Buckets, r2Keys: files.r2Keys }
|
|
5738
|
+
: undefined,
|
|
5725
5739
|
...(smtp
|
|
5726
5740
|
? { smtp: { host: smtp.host, port: smtp.port, tls: smtp.tls, user: smtp.user } }
|
|
5727
5741
|
: {}),
|
|
5728
|
-
coolify: limited(this.placedForCompany(
|
|
5742
|
+
coolify: limited(this.placedForCompany(auth.coolify, org, context), LIMITS.ownAuth),
|
|
5729
5743
|
runtime: {
|
|
5730
|
-
...
|
|
5744
|
+
...auth.runtime,
|
|
5731
5745
|
onSecret: (value) => {
|
|
5732
5746
|
this.handledSecrets.add(value);
|
|
5733
5747
|
},
|
|
5734
5748
|
},
|
|
5735
5749
|
};
|
|
5750
|
+
}
|
|
5751
|
+
/** `plan/signin-db-move.ts`: copies a company's sign-in data from its Coolify database to the TLS database. */
|
|
5752
|
+
async ownAuthDbMove(org, context) {
|
|
5753
|
+
const own = this.ports.ownServices;
|
|
5754
|
+
if (!own)
|
|
5755
|
+
throw new Error(OWN_SERVICES_PORT_ABSENT);
|
|
5756
|
+
const deps = this.ownAuthDeps(own.provision.auth, org, context);
|
|
5757
|
+
const placed = this.placedForCompany(own.provision.auth.coolify, org, context);
|
|
5758
|
+
await moveSigninDatabase(scopeFromOrg(context, {}), { ...deps, readOldDatabase: (uuid) => placed.getDatabase(uuid) }, orgTarget(org), {
|
|
5759
|
+
project: { ensure: () => this.ensureCoolifyProject(org, context) },
|
|
5760
|
+
label: `${org.slug}:auth`,
|
|
5761
|
+
});
|
|
5762
|
+
return {};
|
|
5763
|
+
}
|
|
5764
|
+
async ownAuth(action, org, context) {
|
|
5765
|
+
const own = this.ports.ownServices;
|
|
5766
|
+
if (!own)
|
|
5767
|
+
throw new Error(OWN_SERVICES_PORT_ABSENT);
|
|
5768
|
+
const hostname = action.desired.hostname;
|
|
5769
|
+
const gitRef = action.desired.gitRef;
|
|
5770
|
+
const hn = ownHostnames(org);
|
|
5771
|
+
const scope = scopeFromOrg(context, {});
|
|
5772
|
+
const deps = this.ownAuthDeps(own.provision.auth, org, context);
|
|
5736
5773
|
const before = scope.recordedIds();
|
|
5737
5774
|
const refChanged = before[OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== undefined &&
|
|
5738
5775
|
before[OWN_AUTH_APPLIED_GIT_REF_SCOPE_KEY] !== gitRef;
|
package/dist/plan/own-diff.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface PlanOwnAuthOptions {
|
|
|
45
45
|
mailKeyHeld: boolean;
|
|
46
46
|
/** The host can mint that key itself (a Lettermint port and a held-key reader): the step plans without it. */
|
|
47
47
|
mailKeyMintable?: boolean;
|
|
48
|
+
/** The clock for the sign-in database certificate's renewal window (#746). Default `Date.now`. */
|
|
49
|
+
now?: number;
|
|
50
|
+
/** The held keys can store the sign-in database's CA and certificate. Default true. */
|
|
51
|
+
signinDbKeysPuttable?: boolean;
|
|
52
|
+
/** The host supplies the backup bucket the sign-in database dumps into. Default true. */
|
|
53
|
+
signinDbBackupConfigured?: boolean;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* An own org (`services.own`) runs its own sign-in: ZITADEL, the login app
|
|
@@ -59,6 +65,26 @@ export declare function planOwnAuth(options: PlanOwnAuthOptions): {
|
|
|
59
65
|
step: DraftStep | null;
|
|
60
66
|
blocked: string | null;
|
|
61
67
|
};
|
|
68
|
+
/** `unmanaged` key of the one line `planSigninDbMove` reports. */
|
|
69
|
+
export declare const OWN_SIGNIN_DB_MOVE_BLOCKED_KEY = "own-signin-db-move";
|
|
70
|
+
export interface PlanSigninDbMoveOptions {
|
|
71
|
+
org: DeclaredOrg;
|
|
72
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
73
|
+
/** The held keys can store the sign-in database's CA and certificate. Default true. */
|
|
74
|
+
signinDbKeysPuttable?: boolean;
|
|
75
|
+
/** The host supplies the backup bucket the sign-in database dumps into. Default true. */
|
|
76
|
+
signinDbBackupConfigured?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One step for an own org whose sign-in is applied and whose ZITADEL still uses the Coolify database
|
|
80
|
+
* (#746): `signin-db-move.ts` copies it to foundry's TLS image. Never planned for a company on the TLS
|
|
81
|
+
* database, a new one, or one whose sign-in is not applied. The apply refuses it without a fresh
|
|
82
|
+
* backup of the old database (`backup-gate.ts`).
|
|
83
|
+
*/
|
|
84
|
+
export declare function planSigninDbMove(options: PlanSigninDbMoveOptions): {
|
|
85
|
+
step: DraftStep | null;
|
|
86
|
+
blocked: string | null;
|
|
87
|
+
};
|
|
62
88
|
/** `unmanaged` key of the one line `planOwnMail` reports. */
|
|
63
89
|
export declare const OWN_MAIL_BLOCKED_KEY = "own-mail";
|
|
64
90
|
export interface PlanOwnMailOptions {
|
package/dist/plan/own-diff.js
CHANGED
|
@@ -12,7 +12,10 @@ import { OWN_FILES_R2_SCOPE_KEY, OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget, } fr
|
|
|
12
12
|
import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_SLOT_SCOPE_KEY, OWN_SFU_APPLIED_VERSION_SCOPE_KEY, OWN_SFU_DEPLOY_PENDING_SCOPE_KEY, OWN_SFU_KEYS_UNDELIVERED_SCOPE_KEY, SFU_INTERNAL_URL_SCOPE_KEY, SFU_PUBLIC_URL_SCOPE_KEY, } from './own-sfu.js';
|
|
13
13
|
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
|
|
14
14
|
import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
|
|
15
|
-
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
15
|
+
import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
|
|
16
|
+
import { signinDbBackupMissing } from './signin-db-backup.js';
|
|
17
|
+
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, signinDbCertDue } from './signin-db-tls.js';
|
|
18
|
+
import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db.js';
|
|
16
19
|
/** `unmanaged` key of the one line `planOwnJobs` reports. */
|
|
17
20
|
export const OWN_JOBS_BLOCKED_KEY = 'own-jobs';
|
|
18
21
|
/**
|
|
@@ -130,6 +133,15 @@ export function planOwnAuth(options) {
|
|
|
130
133
|
if (!options.mailKeyHeld && !options.mailKeyMintable) {
|
|
131
134
|
return blocked(`the held key ${orgTarget(org).refs.brokerMailKey} is not held yet`);
|
|
132
135
|
}
|
|
136
|
+
// A company still on the Coolify database keeps running without the keys until it moves.
|
|
137
|
+
if (!recordedIds[PLATFORM_AUTH_DB_SCOPE_KEY]) {
|
|
138
|
+
if (options.signinDbKeysPuttable === false && !recordedIds[SIGNIN_DB_TLS_SCOPE_KEY]) {
|
|
139
|
+
return blocked('the host cannot store the sign-in database certificate (held keys have no put)');
|
|
140
|
+
}
|
|
141
|
+
if (options.signinDbBackupConfigured === false) {
|
|
142
|
+
return blocked('the host supplies no backup bucket for the sign-in database');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
133
145
|
const hostname = names.auth;
|
|
134
146
|
// The broker starts only on a valid `AUTH_APPS`, and the login client is minted only once it does:
|
|
135
147
|
// the first broker entry is what lets this step finish, as the platform's first registration does.
|
|
@@ -149,7 +161,11 @@ export function planOwnAuth(options) {
|
|
|
149
161
|
recordedIds[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY] &&
|
|
150
162
|
recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
|
|
151
163
|
recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
|
|
152
|
-
(Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry)
|
|
164
|
+
(Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry) &&
|
|
165
|
+
!signinDbCertDue(recordedIds, options.now ?? Date.now()) &&
|
|
166
|
+
!signinDbBackupMissing(recordedIds) &&
|
|
167
|
+
// ZITADEL was started on the TLS database and never seen healthy there: the step starts it again.
|
|
168
|
+
!recordedIds[SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY]) {
|
|
153
169
|
return { step: null, blocked: null };
|
|
154
170
|
}
|
|
155
171
|
return {
|
|
@@ -169,6 +185,47 @@ export function planOwnAuth(options) {
|
|
|
169
185
|
},
|
|
170
186
|
};
|
|
171
187
|
}
|
|
188
|
+
/** `unmanaged` key of the one line `planSigninDbMove` reports. */
|
|
189
|
+
export const OWN_SIGNIN_DB_MOVE_BLOCKED_KEY = 'own-signin-db-move';
|
|
190
|
+
/**
|
|
191
|
+
* One step for an own org whose sign-in is applied and whose ZITADEL still uses the Coolify database
|
|
192
|
+
* (#746): `signin-db-move.ts` copies it to foundry's TLS image. Never planned for a company on the TLS
|
|
193
|
+
* database, a new one, or one whose sign-in is not applied. The apply refuses it without a fresh
|
|
194
|
+
* backup of the old database (`backup-gate.ts`).
|
|
195
|
+
*/
|
|
196
|
+
export function planSigninDbMove(options) {
|
|
197
|
+
const { org, recordedIds } = options;
|
|
198
|
+
if (!ownsServices(org) ||
|
|
199
|
+
!ownAuthApplied(org, recordedIds) ||
|
|
200
|
+
!recordedIds[PLATFORM_AUTH_DB_SCOPE_KEY] ||
|
|
201
|
+
!recordedIds[PLATFORM_AUTH_ZITADEL_SCOPE_KEY] ||
|
|
202
|
+
recordedIds[SIGNIN_DB_TLS_SCOPE_KEY]) {
|
|
203
|
+
return { step: null, blocked: null };
|
|
204
|
+
}
|
|
205
|
+
if (options.signinDbKeysPuttable === false) {
|
|
206
|
+
return {
|
|
207
|
+
step: null,
|
|
208
|
+
blocked: `${org.slug}'s sign-in database move is blocked, not planned: the host cannot store the sign-in database certificate (held keys have no put)`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (options.signinDbBackupConfigured === false) {
|
|
212
|
+
return {
|
|
213
|
+
step: null,
|
|
214
|
+
blocked: `${org.slug}'s sign-in database move is blocked, not planned: the host supplies no backup bucket for the sign-in database`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
blocked: null,
|
|
219
|
+
step: {
|
|
220
|
+
action: { system: 'coolify', kind: 'own-auth-db-move', key: org.id, desired: {} },
|
|
221
|
+
step: {
|
|
222
|
+
description: `move ${org.slug}'s sign-in database to foundry's TLS image: ZITADEL is stopped while the data is copied, the old database is kept`,
|
|
223
|
+
destroys: false,
|
|
224
|
+
migrates: true,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
172
229
|
/** `unmanaged` key of the one line `planOwnMail` reports. */
|
|
173
230
|
export const OWN_MAIL_BLOCKED_KEY = 'own-mail';
|
|
174
231
|
/**
|
|
@@ -20,6 +20,10 @@ export interface ServiceTarget {
|
|
|
20
20
|
readonly kind: 'platform' | 'org';
|
|
21
21
|
readonly names: {
|
|
22
22
|
authDb: string;
|
|
23
|
+
/** The sign-in database as foundry's own TLS image (#746). Only an org uses it; the platform's value is never read. */
|
|
24
|
+
signinDb: string;
|
|
25
|
+
/** The one-shot job that copies the old database into it (#746). */
|
|
26
|
+
signinDbMove: string;
|
|
23
27
|
authZitadel: string;
|
|
24
28
|
authLogin: string;
|
|
25
29
|
authBroker: string;
|
|
@@ -34,6 +38,8 @@ export interface ServiceTarget {
|
|
|
34
38
|
filesRunner: string;
|
|
35
39
|
/** An own org's R2 bucket. The platform's is host-supplied (`PlatformFilesDeps.r2`), so its value here is never read. */
|
|
36
40
|
filesBucket: string;
|
|
41
|
+
/** An own org's sign-in database backup bucket (#746): its own bucket, never the files bucket. */
|
|
42
|
+
signinBackupBucket: string;
|
|
37
43
|
jobsApp: string;
|
|
38
44
|
jobsDb: string;
|
|
39
45
|
sfuApp: string;
|
|
@@ -41,6 +47,8 @@ export interface ServiceTarget {
|
|
|
41
47
|
sfuCloudflared: string;
|
|
42
48
|
};
|
|
43
49
|
readonly internal: {
|
|
50
|
+
/** The hostname ZITADEL connects to, and the one name the database's certificate carries. */
|
|
51
|
+
signinDb: string;
|
|
44
52
|
authZitadel: string;
|
|
45
53
|
authLogin: string;
|
|
46
54
|
authBroker: string;
|
|
@@ -62,6 +70,8 @@ export interface ServiceTarget {
|
|
|
62
70
|
keysCredential: string;
|
|
63
71
|
/** The public half the host verifies with (`KEYS_VERIFY_KEYS`). */
|
|
64
72
|
keysVerifyKeys: string;
|
|
73
|
+
/** The sign-in database's server certificate and key, held as JSON `{ cert, key }` (#746). */
|
|
74
|
+
signinDbServer: string;
|
|
65
75
|
};
|
|
66
76
|
}
|
|
67
77
|
/** The platform's shared services, with the literals the platform steps use today. */
|
|
@@ -11,6 +11,8 @@ export function platformTarget() {
|
|
|
11
11
|
kind: 'platform',
|
|
12
12
|
names: {
|
|
13
13
|
authDb: 'platform-auth-db',
|
|
14
|
+
signinDb: 'platform-signin-db',
|
|
15
|
+
signinDbMove: 'platform-signin-db-move',
|
|
14
16
|
authZitadel: 'platform-auth-zitadel',
|
|
15
17
|
authLogin: 'platform-auth-login',
|
|
16
18
|
authBroker: 'platform-auth-broker',
|
|
@@ -24,6 +26,7 @@ export function platformTarget() {
|
|
|
24
26
|
filesMigrate: 'files-migrate',
|
|
25
27
|
filesRunner: 'files',
|
|
26
28
|
filesBucket: 'platform-files',
|
|
29
|
+
signinBackupBucket: 'platform-signin-backups',
|
|
27
30
|
jobsApp: 'jobs',
|
|
28
31
|
jobsDb: 'jobs-scheduler-db',
|
|
29
32
|
sfuApp: 'platform-sfu',
|
|
@@ -31,6 +34,7 @@ export function platformTarget() {
|
|
|
31
34
|
sfuCloudflared: 'platform-sfu-cloudflared',
|
|
32
35
|
},
|
|
33
36
|
internal: {
|
|
37
|
+
signinDb: 'platform-signin-db',
|
|
34
38
|
authZitadel: 'platform-auth-zitadel',
|
|
35
39
|
authLogin: 'platform-auth-login',
|
|
36
40
|
authBroker: 'platform-auth-broker',
|
|
@@ -50,6 +54,7 @@ export function platformTarget() {
|
|
|
50
54
|
livekitKeys: 'livekit:platform:keys',
|
|
51
55
|
keysCredential: 'keys:platform:credential',
|
|
52
56
|
keysVerifyKeys: 'keys:platform:verifyKeys',
|
|
57
|
+
signinDbServer: 'signin-db:platform:server',
|
|
53
58
|
},
|
|
54
59
|
};
|
|
55
60
|
}
|
|
@@ -61,6 +66,8 @@ export function orgTarget(org) {
|
|
|
61
66
|
kind: 'org',
|
|
62
67
|
names: {
|
|
63
68
|
authDb: `${s}-auth-db`,
|
|
69
|
+
signinDb: `${s}-signin-db`,
|
|
70
|
+
signinDbMove: `${s}-signin-db-move`,
|
|
64
71
|
authZitadel: `${s}-auth-zitadel`,
|
|
65
72
|
authLogin: `${s}-auth-login`,
|
|
66
73
|
authBroker: `${s}-auth-broker`,
|
|
@@ -74,6 +81,7 @@ export function orgTarget(org) {
|
|
|
74
81
|
filesMigrate: `${s}-files-migrate`,
|
|
75
82
|
filesRunner: `${s}-files`,
|
|
76
83
|
filesBucket: `${s}-files`,
|
|
84
|
+
signinBackupBucket: `${s}-signin-backups`,
|
|
77
85
|
jobsApp: `${s}-jobs`,
|
|
78
86
|
jobsDb: `${s}-jobs-db`,
|
|
79
87
|
sfuApp: `${s}-sfu`,
|
|
@@ -81,6 +89,7 @@ export function orgTarget(org) {
|
|
|
81
89
|
sfuCloudflared: `${s}-sfu-cloudflared`,
|
|
82
90
|
},
|
|
83
91
|
internal: {
|
|
92
|
+
signinDb: `${s}-signin-db`,
|
|
84
93
|
authZitadel: `${s}-auth-zitadel`,
|
|
85
94
|
authLogin: `${s}-auth-login`,
|
|
86
95
|
authBroker: `${s}-auth-broker`,
|
|
@@ -100,6 +109,7 @@ export function orgTarget(org) {
|
|
|
100
109
|
livekitKeys: `livekit:org:${s}:keys`,
|
|
101
110
|
keysCredential: `keys:org:${s}:credential`,
|
|
102
111
|
keysVerifyKeys: `keys:org:${s}:verifyKeys`,
|
|
112
|
+
signinDbServer: `signin-db:org:${s}:server`,
|
|
103
113
|
},
|
|
104
114
|
};
|
|
105
115
|
}
|
|
@@ -199,6 +199,10 @@ export interface StepExecutor {
|
|
|
199
199
|
}>;
|
|
200
200
|
/** Whether the executor can mint an own org's Lettermint key: a Lettermint port and a reader for the held ref. */
|
|
201
201
|
ownMailKeyMintable?(): boolean;
|
|
202
|
+
/** Whether the executor's held keys can store the sign-in database's CA and certificate (#746). Absent: true. */
|
|
203
|
+
ownSigninDbKeysPuttable?(): boolean;
|
|
204
|
+
/** Whether the host supplies the backup bucket the sign-in database dumps into (#746). Absent: true. */
|
|
205
|
+
ownSigninDbBackupConfigured?(): boolean;
|
|
202
206
|
/**
|
|
203
207
|
* Whether the host's scoped ports for this own org include a files-organisation
|
|
204
208
|
* port. The planner plans no `files:organisation` step for an own org while
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -7,7 +7,7 @@ import { AlreadyRunningError, StepFailedError } from '../runner/errors.js';
|
|
|
7
7
|
import { StaleLeaseError, } from '../store.js';
|
|
8
8
|
import { AdoptRefusedError, adoptCandidates, adoptedKeysUnreadable, adoptedScopeKey, parseAdoptSnapshot, planAdoptFrom, } from './adopt.js';
|
|
9
9
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
10
|
-
import { NetworkMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
10
|
+
import { NetworkMoveBackupMissingError, SigninDbMoveBackupMissingError, UpgradeBackupMissingError, platformDatabasesForService, requireFreshBackupForSigninMove, requireFreshBackupsForMove, requireFreshBackupsForUpgrades, } from './backup-gate.js';
|
|
11
11
|
import { readLiveBuilds } from './build-record.js';
|
|
12
12
|
import { unconfirmedUpgrades } from './confirm-upgrade.js';
|
|
13
13
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
@@ -443,6 +443,25 @@ export class FoundryPlanApply {
|
|
|
443
443
|
throw e;
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
|
+
// The move of a sign-in database reads the old one, and the old one stays; a fresh dump is the way back.
|
|
447
|
+
if (detail.plannedSteps.some((s) => s.action.system === 'coolify' && s.action.kind === 'own-auth-db-move')) {
|
|
448
|
+
try {
|
|
449
|
+
if (!this.backupGate)
|
|
450
|
+
throw new SigninDbMoveBackupMissingError('no-gate');
|
|
451
|
+
await requireFreshBackupForSigninMove({
|
|
452
|
+
coolify: this.backupGate.coolify,
|
|
453
|
+
orgId,
|
|
454
|
+
recordedIds: (await this.store.getAppliedState(orgId))?.serviceIds ?? {},
|
|
455
|
+
now: () => this.now().getTime(),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
if (e instanceof SigninDbMoveBackupMissingError) {
|
|
460
|
+
await this.refuse(plan, 'sign-in database move without a fresh backup');
|
|
461
|
+
}
|
|
462
|
+
throw e;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
446
465
|
if (planNeedsConfirmation(plan.steps) &&
|
|
447
466
|
confirmedOrgName.trim() !== org.displayName.trim()) {
|
|
448
467
|
await this.refuse(plan, "destructive or migrating step without the org's name typed correctly");
|
|
@@ -1044,6 +1063,8 @@ export class FoundryPlanApply {
|
|
|
1044
1063
|
oikosZoneReady: platformReadiness.oikosIngressApplied === true,
|
|
1045
1064
|
ownMailKeyHeld: await this.ownMailKeyHeld(org),
|
|
1046
1065
|
ownMailKeyMintable: this.executor.ownMailKeyMintable?.() ?? false,
|
|
1066
|
+
ownSigninDbKeysPuttable: this.executor.ownSigninDbKeysPuttable?.() ?? true,
|
|
1067
|
+
ownSigninDbBackupConfigured: this.executor.ownSigninDbBackupConfigured?.() ?? true,
|
|
1047
1068
|
...(this.backupGate
|
|
1048
1069
|
? {
|
|
1049
1070
|
backupStorageUuid: (await this.platformDeps?.store.getServiceIds())?.[PLATFORM_BACKUP_S3_STORAGE_SCOPE_KEY] || null,
|
|
@@ -1690,6 +1711,7 @@ const STEP_FIELDS = {
|
|
|
1690
1711
|
'coolify:mail': 'mail',
|
|
1691
1712
|
'coolify:chat': 'chat.usesDomain',
|
|
1692
1713
|
'coolify:own-auth': 'services.own',
|
|
1714
|
+
'coolify:own-auth-db-move': 'services.own',
|
|
1693
1715
|
'coolify:own-files': 'services.own',
|
|
1694
1716
|
'coolify:own-files-r2': 'services.own',
|
|
1695
1717
|
'coolify:own-jobs': 'services.own',
|