@wtfalch/foundry 0.8.0 → 0.10.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/declare.js +1 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/migrations/0029_hosting_entries.sql +109 -0
- package/dist/migrations/0030_hosting_entry_hostnames.sql +24 -0
- package/dist/org.d.ts +87 -29
- package/dist/org.js +180 -27
- package/dist/plan/agora.d.ts +7 -0
- package/dist/plan/agora.js +9 -0
- package/dist/plan/broker-entries.d.ts +104 -0
- package/dist/plan/broker-entries.js +214 -0
- package/dist/plan/diff.d.ts +2 -0
- package/dist/plan/diff.js +274 -50
- package/dist/plan/executor.d.ts +89 -29
- package/dist/plan/executor.js +495 -233
- package/dist/plan/hostnames.d.ts +24 -0
- package/dist/plan/hostnames.js +56 -0
- package/dist/plan/index.d.ts +3 -1
- package/dist/plan/index.js +2 -1
- 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 +16 -0
- package/dist/plan/plan-apply.js +48 -11
- package/dist/plan/platform-diff.d.ts +69 -10
- package/dist/plan/platform-diff.js +133 -14
- package/dist/plan/platform-providers.d.ts +102 -2
- package/dist/plan/platform-providers.js +486 -40
- package/dist/plan/zitadel-system-api.d.ts +64 -0
- package/dist/plan/zitadel-system-api.js +125 -0
- package/dist/platform.d.ts +1 -1
- package/dist/store/postgres.js +12 -12
- package/dist/store/schema.d.ts +20 -1
- package/dist/store/schema.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeclaredOrg, HostingEntry } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* The full names an entry is served at. With `hosting.usesDomain` off, or no
|
|
4
|
+
* domain, `hostnames` is not read: the `app` entry is at `<slug>.oikos.host`
|
|
5
|
+
* and every other entry is internal. `'*'` names nothing here; the wildcard is
|
|
6
|
+
* acted on elsewhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare function entryHostnames(org: DeclaredOrg, entry: HostingEntry): string[];
|
|
9
|
+
/**
|
|
10
|
+
* The one primary name of the org's app: its first, or the name the app was
|
|
11
|
+
* served at before entries had hostnames. `APP_URL`, the ZITADEL redirect URI
|
|
12
|
+
* and the jobs registration read it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function appHostname(org: DeclaredOrg): string;
|
|
15
|
+
/** What `domains` held on an `app` entry's Coolify app before entries had hostnames. */
|
|
16
|
+
export declare function legacyAppHostname(org: DeclaredOrg): string;
|
|
17
|
+
/**
|
|
18
|
+
* The hostnames `cloudflareTunnel` routes to Coolify's proxy (#115, #145,
|
|
19
|
+
* #164): a site's two names, every entry's names, an own-domain calls name and
|
|
20
|
+
* Agora's. An org that does not host keeps the single name it always had.
|
|
21
|
+
*/
|
|
22
|
+
export declare function tunnelIngressHostnames(org: DeclaredOrg): string[];
|
|
23
|
+
/** The ingress names that get a DNS record: those under the org's own domain. */
|
|
24
|
+
export declare function tunnelDnsHostnames(org: DeclaredOrg, domain: string): string[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { APP_ENTRY_KEY } from '../org.js';
|
|
2
|
+
import { agoraHostnameFor } from './agora.js';
|
|
3
|
+
/**
|
|
4
|
+
* The full names an entry is served at. With `hosting.usesDomain` off, or no
|
|
5
|
+
* domain, `hostnames` is not read: the `app` entry is at `<slug>.oikos.host`
|
|
6
|
+
* and every other entry is internal. `'*'` names nothing here; the wildcard is
|
|
7
|
+
* acted on elsewhere.
|
|
8
|
+
*/
|
|
9
|
+
export function entryHostnames(org, entry) {
|
|
10
|
+
const { domain } = org;
|
|
11
|
+
if (!org.hosting.usesDomain || !domain) {
|
|
12
|
+
return entry.key === APP_ENTRY_KEY ? [`${org.slug}.oikos.host`] : [];
|
|
13
|
+
}
|
|
14
|
+
return entry.hostnames.flatMap((name) => name === '*' ? [] : name === '@' ? [domain] : [`${name}.${domain}`]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The one primary name of the org's app: its first, or the name the app was
|
|
18
|
+
* served at before entries had hostnames. `APP_URL`, the ZITADEL redirect URI
|
|
19
|
+
* and the jobs registration read it.
|
|
20
|
+
*/
|
|
21
|
+
export function appHostname(org) {
|
|
22
|
+
const entry = org.hosting.entries.find((e) => e.key === APP_ENTRY_KEY);
|
|
23
|
+
const first = entry ? entryHostnames(org, entry)[0] : undefined;
|
|
24
|
+
return first ?? legacyAppHostname(org);
|
|
25
|
+
}
|
|
26
|
+
/** What `domains` held on an `app` entry's Coolify app before entries had hostnames. */
|
|
27
|
+
export function legacyAppHostname(org) {
|
|
28
|
+
return org.hosting.usesDomain && org.domain ? org.domain : `${org.slug}.oikos.host`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The hostnames `cloudflareTunnel` routes to Coolify's proxy (#115, #145,
|
|
32
|
+
* #164): a site's two names, every entry's names, an own-domain calls name and
|
|
33
|
+
* Agora's. An org that does not host keeps the single name it always had.
|
|
34
|
+
*/
|
|
35
|
+
export function tunnelIngressHostnames(org) {
|
|
36
|
+
const hostnames = org.site.enabled && org.domain ? [org.domain, `cms.${org.domain}`] : [];
|
|
37
|
+
if (org.hosting.enabled) {
|
|
38
|
+
for (const entry of org.hosting.entries)
|
|
39
|
+
hostnames.push(...entryHostnames(org, entry));
|
|
40
|
+
}
|
|
41
|
+
else if (hostnames.length === 0) {
|
|
42
|
+
hostnames.push(legacyAppHostname(org));
|
|
43
|
+
}
|
|
44
|
+
if (org.calls.enabled && org.calls.usesDomain && org.domain) {
|
|
45
|
+
hostnames.push(`calls.${org.domain}`);
|
|
46
|
+
}
|
|
47
|
+
// #164: Agora always moves onto `agora.<domain>` once `org.domain` is set.
|
|
48
|
+
if (org.domain) {
|
|
49
|
+
hostnames.push(agoraHostnameFor(org));
|
|
50
|
+
}
|
|
51
|
+
return [...new Set(hostnames)];
|
|
52
|
+
}
|
|
53
|
+
/** The ingress names that get a DNS record: those under the org's own domain. */
|
|
54
|
+
export function tunnelDnsHostnames(org, domain) {
|
|
55
|
+
return tunnelIngressHostnames(org).filter((hostname) => hostname === domain || hostname.endsWith(`.${domain}`));
|
|
56
|
+
}
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, Confirmatio
|
|
|
13
13
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
|
|
14
14
|
export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
|
|
15
15
|
export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
|
|
16
|
-
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
|
|
16
|
+
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
17
17
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
18
18
|
export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './platform-providers.js';
|
|
19
19
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
@@ -24,6 +24,8 @@ export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCop
|
|
|
24
24
|
export type { ScopeResolver, RecordedIds } from './providers-readers.js';
|
|
25
25
|
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
26
26
|
export type { AgoraHostPort, AgoraHostValues } from './agora.js';
|
|
27
|
+
export type { BrokerPort } from './broker-entries.js';
|
|
28
|
+
export { PlatformBrokerPort } from './broker-entries.js';
|
|
27
29
|
export type { LoginHostBinding, LoginHostBindingsPort } from './login-host.js';
|
|
28
30
|
export { loginHostnameFor } from './login-host.js';
|
|
29
31
|
export type { MailIdentityApp, MailIdentityAudienceCheck } from './mail-identity.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -6,12 +6,13 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.
|
|
|
6
6
|
export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
|
|
7
7
|
export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
|
|
8
8
|
// #171: how a host signs files management requests with the delivered key.
|
|
9
|
-
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
|
|
9
|
+
export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
|
|
10
10
|
export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
|
|
11
11
|
export { createZitadelAdminMinter } from './zitadel-machine-user.js';
|
|
12
12
|
export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
13
13
|
export { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, MissingMailCopyAccountsError, MissingMailCopySourceTokenError, MissingMailCopyStalwartTokenError, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
14
14
|
export { ProvidersCloudflareReader, ProvidersCmsReader, ProvidersCoolifyReader, ProvidersZitadelReader, storeScopeResolver, storeRecordedIds, } from './providers-readers.js';
|
|
15
|
+
export { PlatformBrokerPort } from './broker-entries.js';
|
|
15
16
|
export { loginHostnameFor } from './login-host.js';
|
|
16
17
|
export { checkMailIdentityAudience } from './mail-identity.js';
|
|
17
18
|
export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
/**
|
|
3
|
+
* The names under the org's domain that foundry owns, so no hosting entry may
|
|
4
|
+
* take one: Agora, the cms studio when the site is on, calls when it uses the
|
|
5
|
+
* domain, the sign-in host when the issuer does, and every platform service
|
|
6
|
+
* hostname that falls under the domain. Empty for an org with no domain.
|
|
7
|
+
*/
|
|
8
|
+
export declare function lockedHostnames(org: DeclaredOrg, platformHostnames: readonly string[]): Set<string>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { agoraHostnameFor } from './agora.js';
|
|
2
|
+
import { loginHostnameFor } from './login-host.js';
|
|
3
|
+
/**
|
|
4
|
+
* The names under the org's domain that foundry owns, so no hosting entry may
|
|
5
|
+
* take one: Agora, the cms studio when the site is on, calls when it uses the
|
|
6
|
+
* domain, the sign-in host when the issuer does, and every platform service
|
|
7
|
+
* hostname that falls under the domain. Empty for an org with no domain.
|
|
8
|
+
*/
|
|
9
|
+
export function lockedHostnames(org, platformHostnames) {
|
|
10
|
+
const { domain } = org;
|
|
11
|
+
const locked = new Set();
|
|
12
|
+
if (!domain)
|
|
13
|
+
return locked;
|
|
14
|
+
locked.add(agoraHostnameFor(org));
|
|
15
|
+
if (org.site.enabled)
|
|
16
|
+
locked.add(`cms.${domain}`);
|
|
17
|
+
if (org.calls.enabled && org.calls.usesDomain)
|
|
18
|
+
locked.add(`calls.${domain}`);
|
|
19
|
+
if (org.issuer.usesDomain)
|
|
20
|
+
locked.add(loginHostnameFor(org));
|
|
21
|
+
for (const hostname of platformHostnames) {
|
|
22
|
+
if (hostname.endsWith(`.${domain}`))
|
|
23
|
+
locked.add(hostname);
|
|
24
|
+
}
|
|
25
|
+
return locked;
|
|
26
|
+
}
|
|
@@ -66,7 +66,7 @@ export interface LoginHostBindingsPort {
|
|
|
66
66
|
* on the next run.
|
|
67
67
|
*/
|
|
68
68
|
export declare const LOGIN_BINDING_SCOPE_KEY = "login_host_binding";
|
|
69
|
-
/** The org's recorded app client ids (hosted app, cms, agora), sorted. */
|
|
69
|
+
/** The org's recorded app client ids (hosted app, each entry's own, cms, agora), sorted. */
|
|
70
70
|
export declare function loginHostClientIds(recorded: Readonly<Record<string, string>>): string[];
|
|
71
71
|
/** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
|
|
72
72
|
export declare function loginBindingState(host: string, clientIds: readonly string[]): string;
|
package/dist/plan/login-host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, } from './agora.js';
|
|
1
|
+
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, } from './agora.js';
|
|
2
2
|
/**
|
|
3
3
|
* #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
|
|
4
4
|
* need to agree on for an org's own company login host -- the hostname and
|
|
@@ -36,12 +36,15 @@ export function loginHostnameFor(org) {
|
|
|
36
36
|
* on the next run.
|
|
37
37
|
*/
|
|
38
38
|
export const LOGIN_BINDING_SCOPE_KEY = 'login_host_binding';
|
|
39
|
-
/** The org's recorded app client ids (hosted app, cms, agora), sorted. */
|
|
39
|
+
/** The org's recorded app client ids (hosted app, each entry's own, cms, agora), sorted. */
|
|
40
40
|
export function loginHostClientIds(recorded) {
|
|
41
41
|
return [
|
|
42
42
|
recorded[APP_CLIENT_ID_SCOPE_KEY],
|
|
43
43
|
recorded[CMS_CLIENT_ID_SCOPE_KEY],
|
|
44
44
|
recorded[AGORA_CLIENT_ID_SCOPE_KEY],
|
|
45
|
+
...Object.entries(recorded)
|
|
46
|
+
.filter(([key]) => key.startsWith(ENTRY_CLIENT_ID_SCOPE_PREFIX))
|
|
47
|
+
.map(([, id]) => id),
|
|
45
48
|
]
|
|
46
49
|
.filter((id) => Boolean(id))
|
|
47
50
|
.sort();
|
|
@@ -202,6 +202,9 @@ export interface PlatformStepExecutor {
|
|
|
202
202
|
driftedSteps?(platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<{
|
|
203
203
|
archon?: boolean;
|
|
204
204
|
auth?: boolean;
|
|
205
|
+
backupStorage?: boolean;
|
|
206
|
+
/** #257 fix round 4: see `PlanPlatformOptions.drifted.backupStorageBlocked` (`platform-diff.ts`). */
|
|
207
|
+
backupStorageBlocked?: string;
|
|
205
208
|
}>;
|
|
206
209
|
execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
|
|
207
210
|
resourceId?: string;
|
|
@@ -295,6 +298,12 @@ export interface FoundryPlanApplyOptions {
|
|
|
295
298
|
* before zitadel-login's binding can be written.
|
|
296
299
|
*/
|
|
297
300
|
loginHostBindings?: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* F5: true when the host's executor was built with a `broker` port.
|
|
303
|
+
* Optional, and false without it: each entry's broker entry is then
|
|
304
|
+
* reported blocked instead of planned.
|
|
305
|
+
*/
|
|
306
|
+
broker?: boolean;
|
|
298
307
|
/**
|
|
299
308
|
* #177: true when the host's executor was built with its own `filesOrganisation`
|
|
300
309
|
* / `aiOrganisation` port. Without one, the org step needs the platform's
|
|
@@ -349,6 +358,7 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
349
358
|
private readonly agora;
|
|
350
359
|
private readonly jobsScheduler;
|
|
351
360
|
private readonly loginHostBindings;
|
|
361
|
+
private readonly broker;
|
|
352
362
|
private readonly hostFiles;
|
|
353
363
|
private readonly hostAi;
|
|
354
364
|
private readonly now;
|
|
@@ -388,6 +398,12 @@ export declare class FoundryPlanApply implements PlanApply {
|
|
|
388
398
|
private refuseDestroy;
|
|
389
399
|
private runSteps;
|
|
390
400
|
private computeFreshPlan;
|
|
401
|
+
/**
|
|
402
|
+
* An entry may not take a name foundry owns (`locked-hostnames.ts`). The
|
|
403
|
+
* store checks the names it knows; this adds the platform's own hostnames,
|
|
404
|
+
* which only the plan can read.
|
|
405
|
+
*/
|
|
406
|
+
private invalidHosting;
|
|
391
407
|
/**
|
|
392
408
|
* #177: an org's plan reads the shared platform's own addresses and
|
|
393
409
|
* readiness through the very `platform` deps `planPlatform`/`applyPlatform`
|
package/dist/plan/plan-apply.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
|
|
3
|
+
import { validateHosting } from '../org.js';
|
|
4
|
+
import { PLATFORM_SERVICE_KEYS } from '../platform.js';
|
|
3
5
|
import { NO_AGORA_HOST_VALUES, readAgoraHost, } from './agora.js';
|
|
4
6
|
import { AI_KEY_UNDELIVERED_SCOPE_KEY, JOBS_REGISTRATION_UNDELIVERED_SCOPE_KEY, computePlanDetail, toPlan, } from './diff.js';
|
|
5
7
|
import { readLivePlatformResources, readLiveResources, } from './live.js';
|
|
8
|
+
import { lockedHostnames } from './locked-hostnames.js';
|
|
6
9
|
import { planPlatform as computePlatformPlanDetail, toPlatformPlan, } from './platform-diff.js';
|
|
7
10
|
import { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesFromPlatform, heldPlatformKeyRefs, orgPlatformReadinessFrom, } from './platform-org.js';
|
|
8
11
|
export class DeclaredOrgNotFoundError extends Error {
|
|
@@ -63,6 +66,15 @@ export class PlatformConfirmationRequiredError extends Error {
|
|
|
63
66
|
this.name = 'PlatformConfirmationRequiredError';
|
|
64
67
|
}
|
|
65
68
|
}
|
|
69
|
+
/** The steps that read an entry's hostnames: `computeFreshPlan` drops them for an org whose hosting is invalid. */
|
|
70
|
+
const HOSTNAME_STEPS = new Set([
|
|
71
|
+
'coolify:app',
|
|
72
|
+
'coolify:broker-entry',
|
|
73
|
+
'coolify:database',
|
|
74
|
+
'cloudflare:tunnel',
|
|
75
|
+
'cloudflare:access',
|
|
76
|
+
'jobs:registration',
|
|
77
|
+
]);
|
|
66
78
|
/**
|
|
67
79
|
* The #19 implementation of {@link PlanApply}: computes a plan against live
|
|
68
80
|
* state, and refuses to apply one that has gone stale, one that touches a
|
|
@@ -80,6 +92,7 @@ export class FoundryPlanApply {
|
|
|
80
92
|
agora;
|
|
81
93
|
jobsScheduler;
|
|
82
94
|
loginHostBindings;
|
|
95
|
+
broker;
|
|
83
96
|
hostFiles;
|
|
84
97
|
hostAi;
|
|
85
98
|
now;
|
|
@@ -95,6 +108,7 @@ export class FoundryPlanApply {
|
|
|
95
108
|
this.agora = options.agora;
|
|
96
109
|
this.jobsScheduler = options.jobsScheduler ?? false;
|
|
97
110
|
this.loginHostBindings = options.loginHostBindings ?? false;
|
|
111
|
+
this.broker = options.broker ?? false;
|
|
98
112
|
this.hostFiles = options.filesOrganisation ?? false;
|
|
99
113
|
this.hostAi = options.aiOrganisation ?? false;
|
|
100
114
|
this.now = options.now ?? (() => new Date());
|
|
@@ -354,9 +368,34 @@ export class FoundryPlanApply {
|
|
|
354
368
|
this.orgPlatformReadiness(),
|
|
355
369
|
this.orgAgoraHost(id),
|
|
356
370
|
]);
|
|
357
|
-
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, { loginHostBindings: this.loginHostBindings });
|
|
371
|
+
const detail = computePlanDetail(org, live, applied?.serviceIds ?? {}, agoraHost, this.jobsScheduler, platformReadiness, { loginHostBindings: this.loginHostBindings, broker: this.broker });
|
|
372
|
+
const invalid = await this.invalidHosting(org);
|
|
373
|
+
if (invalid) {
|
|
374
|
+
// Rule 2: reported in `unmanaged`, not thrown. Steps that read the
|
|
375
|
+
// hostnames are dropped, so `apply` does not act on them.
|
|
376
|
+
detail.plannedSteps = detail.plannedSteps.filter(({ action }) => !HOSTNAME_STEPS.has(`${action.system}:${action.kind}`));
|
|
377
|
+
detail.unmanaged.push({
|
|
378
|
+
description: `${org.slug}'s hosting is blocked, not planned: ${invalid}`,
|
|
379
|
+
key: 'hosting:invalid',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
358
382
|
return { org, plan: toPlan(detail), detail };
|
|
359
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* An entry may not take a name foundry owns (`locked-hostnames.ts`). The
|
|
386
|
+
* store checks the names it knows; this adds the platform's own hostnames,
|
|
387
|
+
* which only the plan can read.
|
|
388
|
+
*/
|
|
389
|
+
async invalidHosting(org) {
|
|
390
|
+
const platform = await this.platformDeps?.store.get();
|
|
391
|
+
const platformHostnames = platform
|
|
392
|
+
? PLATFORM_SERVICE_KEYS.map((k) => platform[k].hostname)
|
|
393
|
+
: [];
|
|
394
|
+
return validateHosting(org.hosting, {
|
|
395
|
+
domain: org.domain,
|
|
396
|
+
locked: lockedHostnames(org, platformHostnames),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
360
399
|
/**
|
|
361
400
|
* #177: an org's plan reads the shared platform's own addresses and
|
|
362
401
|
* readiness through the very `platform` deps `planPlatform`/`applyPlatform`
|
|
@@ -604,7 +643,8 @@ function destroyServiceIdKey(target) {
|
|
|
604
643
|
const STEP_FIELDS = {
|
|
605
644
|
'zitadel:issuer': 'issuer.usesDomain',
|
|
606
645
|
'zitadel:owner': 'owner',
|
|
607
|
-
'zitadel:customers': 'hosting.
|
|
646
|
+
'zitadel:customers': 'hosting.entries.uses.auth',
|
|
647
|
+
'coolify:broker-entry': 'hosting.entries.uses.auth',
|
|
608
648
|
'coolify:mail': 'mail',
|
|
609
649
|
'coolify:chat': 'chat.usesDomain',
|
|
610
650
|
'coolify:calls': 'calls.enabled',
|
|
@@ -633,13 +673,12 @@ function stepField(action) {
|
|
|
633
673
|
*
|
|
634
674
|
* Three sources, in this order:
|
|
635
675
|
* - fields no planner step exists for yet, so no apply can make them true:
|
|
636
|
-
* SSO, guests and solo when turned on,
|
|
637
|
-
* (#
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
* reads it (#240), so it converges with that step like any other field;
|
|
676
|
+
* SSO, guests and solo when turned on, and calls or hosting on a domain
|
|
677
|
+
* the org has not recorded (#145: calls on the org's own domain now has a
|
|
678
|
+
* planner step, the same way hosting's own domain does, so it only fails
|
|
679
|
+
* for that one missing-domain reason, not unconditionally). An app repo is
|
|
680
|
+
* not one of these: `coolify:app` reads the `app` entry's `repo`, so it
|
|
681
|
+
* converges with that step like any other field;
|
|
643
682
|
* - chat's reverse re-key, which the planner surfaces but which is not built
|
|
644
683
|
* (#7 amendment 3), so it counts as failed even when no step threw;
|
|
645
684
|
* - on a failed apply, the field of every planned step from the one that
|
|
@@ -663,8 +702,6 @@ function failedFields(org, detail, completed, serviceIds) {
|
|
|
663
702
|
failed.add('guests.enabled');
|
|
664
703
|
if (org.solo.enabled)
|
|
665
704
|
failed.add('solo.enabled');
|
|
666
|
-
if (org.adoptedApp?.form === 'repo')
|
|
667
|
-
failed.add('adoptedApp');
|
|
668
705
|
// `planCallsService`/`planTunnel`/`planZone` all share this same domain
|
|
669
706
|
// requirement (#145): a declared `usesDomain: true` with no recorded
|
|
670
707
|
// domain plans no step for any of them either.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeclaredOrg, OrgId } from '../org.js';
|
|
1
|
+
import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
|
|
2
2
|
import { type Plan, type UnmanagedItem, type VersionChangeItem } from '../plan.js';
|
|
3
3
|
import type { DeclaredPlatform } from '../platform.js';
|
|
4
4
|
import type { PlannedStep } from './diff.js';
|
|
@@ -118,6 +118,10 @@ export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_
|
|
|
118
118
|
export declare const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = "archon_gate_closed";
|
|
119
119
|
export declare const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = "zitadel_platform_admin_user_id";
|
|
120
120
|
export declare const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = "zitadel_platform_admin_user_undelivered";
|
|
121
|
+
/** #283: the created instance's id, recorded once its owner token is delivered. */
|
|
122
|
+
export declare const PLATFORM_AUTH_INSTANCE_SCOPE_KEY = "zitadel_platform_instance_id";
|
|
123
|
+
/** #283: an instance that may exist without a delivered owner token; blocks creating another. */
|
|
124
|
+
export declare const PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY = "zitadel_platform_instance_undelivered";
|
|
121
125
|
/**
|
|
122
126
|
* #216 (building on #209/#215): the `IAM_LOGIN_CLIENT` machine user minted
|
|
123
127
|
* for the login app and the broker, mirroring
|
|
@@ -312,24 +316,67 @@ export declare function platformMailMxHost(apex: string): string;
|
|
|
312
316
|
export declare const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = "mail_applied_apex";
|
|
313
317
|
export declare const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = "mail_applied_host";
|
|
314
318
|
/**
|
|
315
|
-
*
|
|
319
|
+
* The key of a hosting entry in a `coolify:app` or `cloudflare:access` step
|
|
320
|
+
* and in the ids recorded for it: `<orgId>:<entry key>`, so a second entry
|
|
321
|
+
* cannot collide with the first.
|
|
322
|
+
*/
|
|
323
|
+
export declare function appStepKey(org: DeclaredOrg, entryKey?: string): string;
|
|
324
|
+
/**
|
|
325
|
+
* The Coolify name of a hosting entry's app: `<slug>` for the `app` entry, so
|
|
326
|
+
* the move to a list finds the app that runs today, `<slug>-<key>` for any
|
|
327
|
+
* other.
|
|
328
|
+
*/
|
|
329
|
+
export declare function appNameFor(org: DeclaredOrg, entry: HostingEntry): string;
|
|
330
|
+
/**
|
|
331
|
+
* #243: the entry's `env` values the last `coolify:app` apply pushed,
|
|
316
332
|
* recorded as one JSON object (env name to value; ids and a domain, never a
|
|
317
333
|
* secret). `planApp` plans the step again when {@link resolveAppEnv} differs
|
|
318
|
-
* from it, so a changed map reaches an app that is already live.
|
|
334
|
+
* from it, so a changed map reaches an app that is already live. The `.<key>`
|
|
335
|
+
* suffix is the entry key; a scope key holds no `:`, so `storeRecordedIds`
|
|
336
|
+
* still filters it out.
|
|
319
337
|
*/
|
|
320
|
-
export declare
|
|
338
|
+
export declare function appEnvAppliedScopeKey(entryKey: string): string;
|
|
321
339
|
/**
|
|
322
340
|
* #239: set to '1' once an app has been restarted after its database URLs
|
|
323
341
|
* were pushed. Not a URL. Without it a crash between the push and the restart
|
|
324
342
|
* would leave a stale container that no plan notices.
|
|
325
343
|
*/
|
|
326
|
-
export declare
|
|
344
|
+
export declare function dbEnvAppliedScopeKey(entryKey: string): string;
|
|
345
|
+
/**
|
|
346
|
+
* The settings `coolifyApp` owns on an entry's app (`folder`, `port`,
|
|
347
|
+
* `autoDeploy`) as the JSON the marker below holds. `coolifyApp` records it
|
|
348
|
+
* on create and after it patches the app, and `planApp` plans the step again
|
|
349
|
+
* when the declared settings differ from it.
|
|
350
|
+
*/
|
|
351
|
+
export interface AppSettings {
|
|
352
|
+
folder: string | null;
|
|
353
|
+
port: number | null;
|
|
354
|
+
autoDeploy: boolean;
|
|
355
|
+
}
|
|
356
|
+
export declare function appSettingsOf(entry: HostingEntry): AppSettings;
|
|
357
|
+
/** What an app that predates the marker has: Coolify's own defaults, so it plans no change. */
|
|
358
|
+
export declare const DEFAULT_APP_SETTINGS: AppSettings;
|
|
359
|
+
export declare function appSettingsAppliedScopeKey(entryKey: string): string;
|
|
360
|
+
/** The settings the app has: the marker, or the defaults when there is none. */
|
|
361
|
+
export declare function appliedAppSettings(recorded: Readonly<Record<string, string>>, entryKey: string): AppSettings;
|
|
362
|
+
/**
|
|
363
|
+
* The names `coolifyApp` last put in the entry's Coolify `domains`, as a JSON
|
|
364
|
+
* array (`[]` for an internal entry). `planApp` plans the step again, with a
|
|
365
|
+
* redeploy, when {@link entryHostnames} differs from it.
|
|
366
|
+
*/
|
|
367
|
+
export declare function appDomainsAppliedScopeKey(entryKey: string): string;
|
|
368
|
+
/**
|
|
369
|
+
* The names the app has: the marker, or for an app that predates it the one
|
|
370
|
+
* name it was created with (the `app` entry's address, nothing for any other
|
|
371
|
+
* entry), so an app that runs today plans no change.
|
|
372
|
+
*/
|
|
373
|
+
export declare function appliedAppDomains(org: DeclaredOrg, recorded: Readonly<Record<string, string>>, entryKey: string): string[];
|
|
327
374
|
/**
|
|
328
|
-
* The `
|
|
329
|
-
*
|
|
330
|
-
*
|
|
375
|
+
* The entry's `env` values that have a value, in env-name order, and the env
|
|
376
|
+
* names whose source has none yet. Shared by `planApp` and the executor's
|
|
377
|
+
* `pushAppEnv` so both read the same recorded ids.
|
|
331
378
|
*/
|
|
332
|
-
export declare function resolveAppEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): {
|
|
379
|
+
export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): {
|
|
333
380
|
values: Record<string, string>;
|
|
334
381
|
unresolved: Array<[string, string]>;
|
|
335
382
|
};
|
|
@@ -498,11 +545,23 @@ export interface PlanPlatformOptions {
|
|
|
498
545
|
* include list is not the declared one. `auth` (#216 round 4): the
|
|
499
546
|
* login-client PAT recorded under
|
|
500
547
|
* {@link PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY} is no longer the sole
|
|
501
|
-
* live PAT on that ZITADEL machine user.
|
|
548
|
+
* live PAT on that ZITADEL machine user. `backupStorage` (#257 fix round
|
|
549
|
+
* 2): the registered R2 storage's live fingerprint no longer matches the
|
|
550
|
+
* host's configured bucket/endpoint/key/secret, or the recorded storage
|
|
551
|
+
* uuid is no longer in Coolify's live list -- see `planBackups`'s own doc
|
|
552
|
+
* comment for why this is the one drift flag that can fire even once
|
|
553
|
+
* every per-service backup step is claimed. `backupStorageBlocked` (#257
|
|
554
|
+
* fix round 4, point 4): set instead of `backupStorage` when the liveness
|
|
555
|
+
* check itself failed (Coolify unreachable, credentials revoked) -- the
|
|
556
|
+
* plan is never failed over it, but it is reported in `unmanaged` rather
|
|
557
|
+
* than only logged, so a persistent failure is visible instead of silently
|
|
558
|
+
* retried forever.
|
|
502
559
|
*/
|
|
503
560
|
drifted?: {
|
|
504
561
|
archon?: boolean;
|
|
505
562
|
auth?: boolean;
|
|
563
|
+
backupStorage?: boolean;
|
|
564
|
+
backupStorageBlocked?: string;
|
|
506
565
|
};
|
|
507
566
|
}
|
|
508
567
|
/**
|