@wtfalch/foundry 0.26.0 → 0.26.2
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/plan/diff.d.ts +9 -0
- package/dist/plan/diff.js +1 -1
- package/dist/plan/executor.d.ts +8 -1
- package/dist/plan/executor.js +42 -19
- package/dist/plan/health-targets.d.ts +3 -1
- package/dist/plan/health-targets.js +23 -9
- package/package.json +1 -1
package/dist/plan/diff.d.ts
CHANGED
|
@@ -117,6 +117,15 @@ export declare function toPlan(detail: PlanDetail): Plan;
|
|
|
117
117
|
* against this seam.
|
|
118
118
|
*/
|
|
119
119
|
export declare function computePlanDetail(org: DeclaredOrg, live: LiveResource[], recordedIds?: Readonly<Record<string, string>>, agoraHost?: AgoraHostReading, jobsAvailable?: boolean, platformReadiness?: OrgPlatformReadiness, options?: PlanOrgOptions): PlanDetail;
|
|
120
|
+
/**
|
|
121
|
+
* #488: the bare hostname out of an issuer URL (`https://<hostname>`, the
|
|
122
|
+
* only shape {@link agoraHostValuesFromPlatform} or a host-supplied
|
|
123
|
+
* `AgoraHostPort` ever hands back), or `null` when there is no issuer to
|
|
124
|
+
* read one from. What {@link loginHostnameFor} compares the org's own
|
|
125
|
+
* computed `auth.<domain>` against, so the platform's own org never plans
|
|
126
|
+
* its login host equal to the shared ZITADEL instance's own.
|
|
127
|
+
*/
|
|
128
|
+
export declare function isPlatformOwnOrg(org: DeclaredOrg, platformApex: string | null): boolean;
|
|
120
129
|
/** Restated from `executor.ts`, the same rule: the id of an ai key minted but not delivered. */
|
|
121
130
|
export declare const AI_KEY_UNDELIVERED_SCOPE_KEY = "ai_key_undelivered";
|
|
122
131
|
/** Restated from `executor.ts`, the same rule: the id of a jobs project registered but not delivered. */
|
package/dist/plan/diff.js
CHANGED
|
@@ -734,7 +734,7 @@ function planOwners(org, recordedIds, authReady, authIssuer) {
|
|
|
734
734
|
* computed `auth.<domain>` against, so the platform's own org never plans
|
|
735
735
|
* its login host equal to the shared ZITADEL instance's own.
|
|
736
736
|
*/
|
|
737
|
-
function isPlatformOwnOrg(org, platformApex) {
|
|
737
|
+
export function isPlatformOwnOrg(org, platformApex) {
|
|
738
738
|
return Boolean(org.domain && platformApex && org.domain.toLowerCase() === platformApex.toLowerCase());
|
|
739
739
|
}
|
|
740
740
|
function platformAuthHostnameFrom(issuer) {
|
package/dist/plan/executor.d.ts
CHANGED
|
@@ -135,7 +135,6 @@ export interface CoolifyPort {
|
|
|
135
135
|
name: string;
|
|
136
136
|
postgres_db: string;
|
|
137
137
|
postgres_user: string;
|
|
138
|
-
postgres_initdb_args?: string;
|
|
139
138
|
}): Promise<{
|
|
140
139
|
uuid: string;
|
|
141
140
|
internal_db_url: string;
|
|
@@ -2220,6 +2219,14 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
|
|
|
2220
2219
|
* `system:kind:key` slot of its own for the same reason.
|
|
2221
2220
|
*/
|
|
2222
2221
|
private ensureCallsDatabase;
|
|
2222
|
+
/**
|
|
2223
|
+
* Sets `POSTGRES_INITDB_ARGS` on a Postgres database, once: the official
|
|
2224
|
+
* image's entrypoint passes it to `initdb`, which Coolify's own
|
|
2225
|
+
* `postgres_initdb_args` field never does ({@link CALLS_SYNAPSE_DB_INITDB_ARGS}).
|
|
2226
|
+
* `/databases/{uuid}/envs` is not a typed port method yet, so this goes
|
|
2227
|
+
* through `raw`. Nothing here is a secret.
|
|
2228
|
+
*/
|
|
2229
|
+
private ensureDatabaseInitdbArgs;
|
|
2223
2230
|
/**
|
|
2224
2231
|
* Reads back {@link CALLS_SECRET_ENV_KEYS} from the MAS app's own envs
|
|
2225
2232
|
* before minting fresh ones -- this stack's only memory across a re-run
|
package/dist/plan/executor.js
CHANGED
|
@@ -567,21 +567,17 @@ const CALLS_MAS_DB_SCOPE_KEY = 'coolify_calls_mas_database';
|
|
|
567
567
|
* Synapse refuses to start against a database whose `LC_COLLATE`/`LC_CTYPE`
|
|
568
568
|
* is not `C` (https://element-hq.github.io/synapse/latest/postgres.html);
|
|
569
569
|
* Coolify's official postgres image otherwise creates one with `en_US.utf8`.
|
|
570
|
-
* `
|
|
571
|
-
* `
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* such requirement, so this is passed for the Synapse database only. Not
|
|
582
|
-
* using Synapse's own `allow_unsafe_locale` escape hatch regardless of
|
|
583
|
-
* which fix lands: that trades a loud startup refusal for silent index
|
|
584
|
-
* corruption after a future glibc upgrade.
|
|
570
|
+
* Coolify's `postgres_initdb_args` field is stored and never handed to
|
|
571
|
+
* `initdb` (its Postgres start action builds the container's env from
|
|
572
|
+
* `postgres_user`/`postgres_password`/`postgres_db` and runtime envs only,
|
|
573
|
+
* https://github.com/coollabsio/coolify/blob/main/app/Actions/Database/StartPostgresql.php),
|
|
574
|
+
* so foundry no longer sends it (#494). The official `postgres` image's
|
|
575
|
+
* entrypoint does read the `POSTGRES_INITDB_ARGS` env: `ensureCallsDatabase`
|
|
576
|
+
* sets it on the Synapse database before that database first starts. It
|
|
577
|
+
* cannot change a database whose data directory `initdb` already made; MAS
|
|
578
|
+
* has no such requirement, so the env goes on the Synapse database only. Not
|
|
579
|
+
* using Synapse's own `allow_unsafe_locale` escape hatch: that trades a loud
|
|
580
|
+
* startup refusal for silent index corruption after a future glibc upgrade.
|
|
585
581
|
*/
|
|
586
582
|
const CALLS_SYNAPSE_DB_INITDB_ARGS = '--locale=C --encoding=UTF8';
|
|
587
583
|
/**
|
|
@@ -3298,13 +3294,23 @@ export class FoundryStepExecutor {
|
|
|
3298
3294
|
* database, whose {@link CALLS_SERVICE_DB_SCOPE_KEY} has no
|
|
3299
3295
|
* `system:kind:key` slot of its own for the same reason.
|
|
3300
3296
|
*/
|
|
3301
|
-
async ensureCallsDatabase(org, context, projectUuid, environment, name, postgresDb, scopeKey,
|
|
3297
|
+
async ensureCallsDatabase(org, context, projectUuid, environment, name, postgresDb, scopeKey, initdbArgs) {
|
|
3298
|
+
// A database found by id or name may be one a failed run created and never
|
|
3299
|
+
// started (its env call threw), so the env is checked on every path. It
|
|
3300
|
+
// only takes effect before the first start; the callers start it later
|
|
3301
|
+
// (`getDatabaseStarted`).
|
|
3302
3302
|
const existing = context.recordedIds()[scopeKey];
|
|
3303
|
-
if (existing)
|
|
3303
|
+
if (existing) {
|
|
3304
|
+
if (initdbArgs)
|
|
3305
|
+
await this.ensureDatabaseInitdbArgs(existing, initdbArgs);
|
|
3304
3306
|
return existing;
|
|
3307
|
+
}
|
|
3305
3308
|
const found = (await this.ports.coolify.listDatabases()).find((d) => d.name === name && d.environment_id === environment.id);
|
|
3306
|
-
if (found)
|
|
3309
|
+
if (found) {
|
|
3310
|
+
if (initdbArgs)
|
|
3311
|
+
await this.ensureDatabaseInitdbArgs(found.uuid, initdbArgs);
|
|
3307
3312
|
return found.uuid;
|
|
3313
|
+
}
|
|
3308
3314
|
const server = await this.resolveServer(org, context);
|
|
3309
3315
|
const created = await this.ports.coolify.createPostgresDatabase({
|
|
3310
3316
|
project_uuid: projectUuid,
|
|
@@ -3314,10 +3320,27 @@ export class FoundryStepExecutor {
|
|
|
3314
3320
|
name,
|
|
3315
3321
|
postgres_db: postgresDb,
|
|
3316
3322
|
postgres_user: postgresDb,
|
|
3317
|
-
...(postgresInitdbArgs ? { postgres_initdb_args: postgresInitdbArgs } : {}),
|
|
3318
3323
|
});
|
|
3324
|
+
if (initdbArgs)
|
|
3325
|
+
await this.ensureDatabaseInitdbArgs(created.uuid, initdbArgs);
|
|
3319
3326
|
return created.uuid;
|
|
3320
3327
|
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Sets `POSTGRES_INITDB_ARGS` on a Postgres database, once: the official
|
|
3330
|
+
* image's entrypoint passes it to `initdb`, which Coolify's own
|
|
3331
|
+
* `postgres_initdb_args` field never does ({@link CALLS_SYNAPSE_DB_INITDB_ARGS}).
|
|
3332
|
+
* `/databases/{uuid}/envs` is not a typed port method yet, so this goes
|
|
3333
|
+
* through `raw`. Nothing here is a secret.
|
|
3334
|
+
*/
|
|
3335
|
+
async ensureDatabaseInitdbArgs(dbId, args) {
|
|
3336
|
+
const envs = (await this.ports.coolify.raw('GET', `/databases/${dbId}/envs`));
|
|
3337
|
+
if (envs.some((e) => e.key === 'POSTGRES_INITDB_ARGS'))
|
|
3338
|
+
return;
|
|
3339
|
+
await this.ports.coolify.raw('POST', `/databases/${dbId}/envs`, {
|
|
3340
|
+
key: 'POSTGRES_INITDB_ARGS',
|
|
3341
|
+
value: args,
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3321
3344
|
/**
|
|
3322
3345
|
* Reads back {@link CALLS_SECRET_ENV_KEYS} from the MAS app's own envs
|
|
3323
3346
|
* before minting fresh ones -- this stack's only memory across a re-run
|
|
@@ -71,7 +71,9 @@ export interface HealthTarget {
|
|
|
71
71
|
* the broker whenever the org has them; then whichever of hosting or site
|
|
72
72
|
* the org runs, if either.
|
|
73
73
|
*/
|
|
74
|
-
export declare function healthTargets(org: DeclaredOrg
|
|
74
|
+
export declare function healthTargets(org: DeclaredOrg, options?: {
|
|
75
|
+
platform?: DeclaredPlatform | null;
|
|
76
|
+
}): HealthTarget[];
|
|
75
77
|
/**
|
|
76
78
|
* The seven platform services #541 names (`platform:auth`, `platform:archon`,
|
|
77
79
|
* `platform:mail`, `platform:ai`, `platform:files`, `platform:integrations`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appEntry } from '../org.js';
|
|
2
2
|
import { agoraHostnameFor } from './agora.js';
|
|
3
3
|
import { bouleHostnameFor } from './boule.js';
|
|
4
|
+
import { isPlatformOwnOrg } from './diff.js';
|
|
4
5
|
import { callsHostnameFor, cmsHostnameFor } from './executor.js';
|
|
5
6
|
import { isBehindGate } from './gates.js';
|
|
6
7
|
import { entryHostnames } from './hostnames.js';
|
|
@@ -12,8 +13,6 @@ const DEFAULT_API_HEALTH_PATH = '/api/health';
|
|
|
12
13
|
const STATIC_PAGE_PATH = '/';
|
|
13
14
|
/** zitadel-login's own readiness route (#541's "Real paths today"). `depth: 'shallow'`, not `'deep'`: login holds no database of its own (`login-host.ts`), whatever its name suggests. */
|
|
14
15
|
const LOGIN_READY_PATH = '/ready';
|
|
15
|
-
/** The broker answers on `platform.auth.hostname` under `AUTH_BROKER_PATH_PATTERN` (`platform-providers.ts`, `'^/api(/|$)'`), so its own health path carries that prefix. */
|
|
16
|
-
const BROKER_HEALTH_PATH = '/api/healthy';
|
|
17
16
|
/** `platform-providers.ts`'s own `waitForInstance` call target (`` `https://${hostname}/debug/healthz}` ``), also the one path `AUTH_ZITADEL_PATH_PATTERN` names by name. */
|
|
18
17
|
const ZITADEL_HEALTH_PATH = '/debug/healthz';
|
|
19
18
|
/**
|
|
@@ -26,7 +25,12 @@ const STALWART_READY_PATH = '/healthz/ready';
|
|
|
26
25
|
const CHAT_SERVICE_HEALTH_PATH = '/_chat/health';
|
|
27
26
|
const CALLS_HEALTH_PATH = '/healthz';
|
|
28
27
|
/** Boule, Agora and the company's own login host: every org gets these, with no flag to check (the "Why" this issue opens with). */
|
|
29
|
-
function alwaysOnTargets(org) {
|
|
28
|
+
function alwaysOnTargets(org, platform) {
|
|
29
|
+
// The platform's own org signs in on the shared instance host, not its own
|
|
30
|
+
// `loginHostnameFor` (#488; `login-host.ts`, `diff.ts`'s `planLoginHost`).
|
|
31
|
+
const loginHost = platform && isPlatformOwnOrg(org, platform.apex)
|
|
32
|
+
? platform.auth.hostname
|
|
33
|
+
: loginHostnameFor(org);
|
|
30
34
|
return [
|
|
31
35
|
{
|
|
32
36
|
service: 'boule',
|
|
@@ -49,7 +53,7 @@ function alwaysOnTargets(org) {
|
|
|
49
53
|
},
|
|
50
54
|
{
|
|
51
55
|
service: 'login',
|
|
52
|
-
url: `https://${
|
|
56
|
+
url: `https://${loginHost}${LOGIN_READY_PATH}`,
|
|
53
57
|
expect: okExpect(),
|
|
54
58
|
depth: 'shallow',
|
|
55
59
|
restartSafe: false,
|
|
@@ -224,8 +228,8 @@ function appTargets(org) {
|
|
|
224
228
|
* the broker whenever the org has them; then whichever of hosting or site
|
|
225
229
|
* the org runs, if either.
|
|
226
230
|
*/
|
|
227
|
-
export function healthTargets(org) {
|
|
228
|
-
const targets = alwaysOnTargets(org);
|
|
231
|
+
export function healthTargets(org, options = {}) {
|
|
232
|
+
const targets = alwaysOnTargets(org, options.platform ?? null);
|
|
229
233
|
const broker = brokerTarget(org);
|
|
230
234
|
if (broker)
|
|
231
235
|
targets.push(broker);
|
|
@@ -265,12 +269,18 @@ function authTargets(platform) {
|
|
|
265
269
|
behindAccess: false,
|
|
266
270
|
},
|
|
267
271
|
{
|
|
272
|
+
// The broker's only unauthenticated route is `/healthy` at its own root
|
|
273
|
+
// (auth's `broker/src/server.ts`), but the tunnel hands it only
|
|
274
|
+
// `AUTH_BROKER_PATH_PATTERN` ('^/api(/|$)') unchanged: `/api/healthy`
|
|
275
|
+
// reaches the broker and answers 401, and bare `/healthy` reaches
|
|
276
|
+
// zitadel-login instead, which would read as the broker being up.
|
|
268
277
|
service: 'auth-broker',
|
|
269
|
-
url:
|
|
278
|
+
url: null,
|
|
270
279
|
expect: okExpect(),
|
|
271
280
|
depth: 'shallow',
|
|
272
281
|
restartSafe: false,
|
|
273
282
|
behindAccess: false,
|
|
283
|
+
reason: "the broker's health route (/healthy) is not reachable from outside: the tunnel sends it only /api/*, which answers 401 without a session, and /healthy on this host is zitadel-login's",
|
|
274
284
|
},
|
|
275
285
|
];
|
|
276
286
|
}
|
|
@@ -302,11 +312,15 @@ export function platformHealthTargets(platform) {
|
|
|
302
312
|
...authTargets(platform),
|
|
303
313
|
{
|
|
304
314
|
service: 'archon',
|
|
315
|
+
// `provisionPlatformArchon` always puts a Cloudflare Access app in front
|
|
316
|
+
// of this hostname (it throws with no Access emails), so a prober with no
|
|
317
|
+
// service token reads a redirect: it proves Access and the tunnel are up,
|
|
318
|
+
// not that Archon answers.
|
|
305
319
|
url: `https://${platform.archon.hostname}${DEFAULT_API_HEALTH_PATH}`,
|
|
306
320
|
expect: okExpect(),
|
|
307
|
-
depth: '
|
|
321
|
+
depth: 'shallow',
|
|
308
322
|
restartSafe: false,
|
|
309
|
-
behindAccess:
|
|
323
|
+
behindAccess: true,
|
|
310
324
|
},
|
|
311
325
|
{
|
|
312
326
|
service: 'mail',
|