@wtfalch/foundry 0.60.0 → 0.62.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/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +7 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +60 -8
- package/dist/plan/blue-green.js +115 -10
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +231 -19
- package/dist/plan/executor.js +1648 -129
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +19 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { DraftStep } from './diff.js';
|
|
3
|
+
import type { CoolifyPort } from './executor.js';
|
|
4
|
+
import { type JobRuntime } from './job-app.js';
|
|
5
|
+
import type { StepScope } from './own-services.js';
|
|
6
|
+
/** The role's name on the server: the org's slug plus `_apps_admin`. */
|
|
7
|
+
export declare const appsAdminRoleName: (org: Pick<DeclaredOrg, "slug">) => string;
|
|
8
|
+
/** The job app's name in Coolify. */
|
|
9
|
+
export declare const appsAdminJobName: (org: Pick<DeclaredOrg, "slug">) => string;
|
|
10
|
+
/** Recorded once the role is made, its password set, and the URL delivered. */
|
|
11
|
+
export declare const ORG_APPS_ADMIN_SCOPE_KEY = "org_apps_admin";
|
|
12
|
+
/**
|
|
13
|
+
* An opaque id, set once the password is minted and the job that sets it is about to run, until the URL is
|
|
14
|
+
* delivered. Never the password. A failure before that point leaves no id, so the next apply tries again.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY = "org_apps_admin_undelivered";
|
|
17
|
+
/** The job app's id while it exists. */
|
|
18
|
+
export declare const ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY = "org_apps_admin_job_app";
|
|
19
|
+
/** Where the host holds the role's connection URL, once delivered -- `deliverSecret`'s own naming convention. */
|
|
20
|
+
export declare const appsAdminUrlRef: (org: Pick<DeclaredOrg, "slug">) => string;
|
|
21
|
+
/** How many times the URL is handed to `deliverSecret`. */
|
|
22
|
+
export declare const ORG_APPS_ADMIN_DELIVERY_ATTEMPTS = 3;
|
|
23
|
+
export interface PlanOrgAppsAdminOptions {
|
|
24
|
+
/** The host can store a secret under a ref (`deliverSecret`). Default true. */
|
|
25
|
+
canHoldSecrets?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pure. Plans `coolify:org-apps-admin` once, after the company's Postgres server is recorded
|
|
29
|
+
* (platform-step rule 2: blocked, not failing, while it is not). A lost mint -- an undelivered id
|
|
30
|
+
* with no done guard -- is reported blocked and never minted again (rule 1); the operator clears the
|
|
31
|
+
* recorded id once the role is confirmed gone from the server.
|
|
32
|
+
*/
|
|
33
|
+
export declare function planOrgAppsAdminStep(org: Pick<DeclaredOrg, 'id' | 'slug'>, recordedIds: Readonly<Record<string, string>>, options?: PlanOrgAppsAdminOptions): {
|
|
34
|
+
step: DraftStep | null;
|
|
35
|
+
blocked: string | null;
|
|
36
|
+
};
|
|
37
|
+
export interface PlanAppsAdminOptions {
|
|
38
|
+
canHoldSecrets: boolean;
|
|
39
|
+
/** `coolify:org-postgres` is planned in this same plan, ahead of this step. */
|
|
40
|
+
postgresPlanned: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pure. F4 (#925): the wiring of {@link planOrgAppsAdminStep} to `org.boule.apps`. Apps on: the mint
|
|
44
|
+
* step, once Boule (which receives the URL) and the Postgres server are recorded or planned. Apps off with
|
|
45
|
+
* the role recorded (or a mint never delivered): the retire step, which needs no secret and drops nothing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function planAppsAdmin(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], options: PlanAppsAdminOptions): {
|
|
48
|
+
steps: DraftStep[];
|
|
49
|
+
blocked: Array<{
|
|
50
|
+
description: string;
|
|
51
|
+
key: string;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The job's script: makes the role (idempotent, kept if it already exists), sets its password and grants
|
|
56
|
+
* it CONNECT on `postgres`. It revokes nothing from PUBLIC (see the file comment). Roles are made by
|
|
57
|
+
* `format('%I')` and the password by `format('%L')`, so no value is spliced into SQL by the shell. Setting
|
|
58
|
+
* the password again is harmless, so a mint that is repeated simply replaces it. It never prints a URL or a password: psql's output is discarded and the one
|
|
59
|
+
* error line names neither.
|
|
60
|
+
*/
|
|
61
|
+
export declare function orgAppsAdminScript(role: string, mode?: 'mint' | 'retire'): string[];
|
|
62
|
+
/** The job: the same pinned image as the other Postgres jobs, so `psql` matches the server's major version. */
|
|
63
|
+
export declare function orgAppsAdminDockerfile(role: string, mode?: 'mint' | 'retire'): string;
|
|
64
|
+
type AppsAdminCoolify = Pick<CoolifyPort, 'findOrCreateEnvironment' | 'findApplicationByName' | 'getApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'deploy' | 'raw'>;
|
|
65
|
+
/** Everything the step takes from the executor. New ones go here, never as positional arguments. */
|
|
66
|
+
export interface OrgAppsAdminDeps {
|
|
67
|
+
coolify: AppsAdminCoolify;
|
|
68
|
+
ensureProject(): Promise<string>;
|
|
69
|
+
serverUuid(): Promise<string>;
|
|
70
|
+
/** Starts the company's Postgres server when it is not running and returns its live record. */
|
|
71
|
+
startDatabase(uuid: string): Promise<{
|
|
72
|
+
internal_db_url?: string;
|
|
73
|
+
status?: string;
|
|
74
|
+
}>;
|
|
75
|
+
deliverSecret(ref: string, secret: string): Promise<void>;
|
|
76
|
+
/** Registers a value for redaction. */
|
|
77
|
+
onSecret?(value: string): void;
|
|
78
|
+
/** Test seam: a password. Default: 24 random bytes, base64url. */
|
|
79
|
+
mintPassword?(): string;
|
|
80
|
+
/** Test seam: an opaque id. */
|
|
81
|
+
mintId?(): string;
|
|
82
|
+
runtime?: JobRuntime;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Makes the role true and delivers its connection URL. The password is minted in memory. The opaque id
|
|
86
|
+
* {@link ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY} is recorded and persisted only once the job that sets the
|
|
87
|
+
* password is about to be deployed ("password minted and role touched"); a failure before that leaves no
|
|
88
|
+
* id and the next apply simply retries, since setting a password again is harmless. The done guard is
|
|
89
|
+
* recorded only once the URL is delivered. A run that dies between the two finds the id and stops (the
|
|
90
|
+
* planner reports it blocked); retiring the role (apps off) clears it.
|
|
91
|
+
*/
|
|
92
|
+
export declare function provisionOrgAppsAdmin(scope: StepScope, deps: OrgAppsAdminDeps, org: DeclaredOrg): Promise<void>;
|
|
93
|
+
/** What the retire form takes from the executor: the mint form's deps, and the held-key blanking. */
|
|
94
|
+
export interface RetireOrgAppsAdminDeps extends Omit<OrgAppsAdminDeps, 'deliverSecret'> {
|
|
95
|
+
/** Blanks the held URL (the store has no delete: an empty value reads as not held). */
|
|
96
|
+
clearHeldUrl(ref: string): Promise<void>;
|
|
97
|
+
/** Takes `APPS_DATABASE_ADMIN_URL` out of Boule's env. A row already gone is not an error. */
|
|
98
|
+
removeBouleEnv(): Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Apps went off (F4, #925): the role stops logging in, the URL leaves Boule's env and the held store,
|
|
102
|
+
* and the recorded ids are cleared last so a failed run repeats. Never drops the role, a database or
|
|
103
|
+
* a role an app made (CLAUDE.md: never drop data); turning apps on again mints a new password for the
|
|
104
|
+
* same role. A recorded mint that was never delivered is retired the same way, which is also how an
|
|
105
|
+
* operator unblocks a lost mint. The job needs the server's admin URL and nothing else secret.
|
|
106
|
+
*/
|
|
107
|
+
export declare function retireOrgAppsAdmin(scope: StepScope, deps: RetireOrgAppsAdminDeps, org: DeclaredOrg): Promise<void>;
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { pinnedRef } from '../bootstrap/versions.js';
|
|
3
|
+
import { ORG_POSTGRES_SERVER_SCOPE_KEY, appsOn } from '../org.js';
|
|
4
|
+
import { JobFailure, waitForJob } from './job-app.js';
|
|
5
|
+
import { orgPostgresServerName } from './org-postgres.js';
|
|
6
|
+
import { boundedCall, withCallTimeout } from './signin-db.js';
|
|
7
|
+
/**
|
|
8
|
+
* estate#210: a narrow role on the company's one Postgres server (`org-postgres.ts`) that Boule uses
|
|
9
|
+
* to create a database per app copy itself -- `LOGIN CREATEDB CREATEROLE` only, never superuser,
|
|
10
|
+
* never able to touch another role's data or database (estate#215, tested on Postgres 16.15). Its
|
|
11
|
+
* connection URL is the one credential Boule is handed; it never sees the server's own admin URL.
|
|
12
|
+
*
|
|
13
|
+
* This step does NOT touch CONNECT on any database. `org-postgres-job.ts` deliberately leaves CONNECT open
|
|
14
|
+
* on PUBLIC (a template app creates its own runtime role on first boot and relies on it), so a server-wide
|
|
15
|
+
* `REVOKE CONNECT ... FROM PUBLIC` here would lock out every existing home. Isolation between app databases
|
|
16
|
+
* is done by Boule instead (contract with the Boule builder): on each `app_*` database it creates, Boule
|
|
17
|
+
* revokes CONNECT from PUBLIC and grants it to that database's own role only. The admin role holds CONNECT
|
|
18
|
+
* on `postgres` alone.
|
|
19
|
+
*
|
|
20
|
+
* Wired by F4 (#925): the step is planned only for a company whose Boule runs apps (`appsOn`,
|
|
21
|
+
* `org.boule.apps`), after the company's Postgres server and Boule are recorded or planned. The URL is
|
|
22
|
+
* held at {@link appsAdminUrlRef} and written to Boule's env as `APPS_DATABASE_ADMIN_URL`. Turning apps
|
|
23
|
+
* off plans the retire form ({@link retireOrgAppsAdmin}): the env row goes, the held URL is blanked, and
|
|
24
|
+
* the role is set NOLOGIN and its sessions ended. Nothing is dropped: the role, the `app_*` databases
|
|
25
|
+
* and their roles stay, so turning apps on again mints a new password for the same role.
|
|
26
|
+
*/
|
|
27
|
+
const NAME = /^[a-z][a-z0-9_]{0,62}$/;
|
|
28
|
+
function checked(name) {
|
|
29
|
+
if (!NAME.test(name))
|
|
30
|
+
throw new Error(`Postgres name is not valid: ${name}`);
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
/** The role's name on the server: the org's slug plus `_apps_admin`. */
|
|
34
|
+
export const appsAdminRoleName = (org) => `${org.slug}_apps_admin`;
|
|
35
|
+
/** The job app's name in Coolify. */
|
|
36
|
+
export const appsAdminJobName = (org) => `${org.slug}-apps-admin-job`;
|
|
37
|
+
/** Recorded once the role is made, its password set, and the URL delivered. */
|
|
38
|
+
export const ORG_APPS_ADMIN_SCOPE_KEY = 'org_apps_admin';
|
|
39
|
+
/**
|
|
40
|
+
* An opaque id, set once the password is minted and the job that sets it is about to run, until the URL is
|
|
41
|
+
* delivered. Never the password. A failure before that point leaves no id, so the next apply tries again.
|
|
42
|
+
*/
|
|
43
|
+
export const ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY = 'org_apps_admin_undelivered';
|
|
44
|
+
/** The job app's id while it exists. */
|
|
45
|
+
export const ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY = 'org_apps_admin_job_app';
|
|
46
|
+
/** Where the host holds the role's connection URL, once delivered -- `deliverSecret`'s own naming convention. */
|
|
47
|
+
export const appsAdminUrlRef = (org) => `org-apps-admin:${org.slug}:adminUrl`;
|
|
48
|
+
/** How many times the URL is handed to `deliverSecret`. */
|
|
49
|
+
export const ORG_APPS_ADMIN_DELIVERY_ATTEMPTS = 3;
|
|
50
|
+
/**
|
|
51
|
+
* Pure. Plans `coolify:org-apps-admin` once, after the company's Postgres server is recorded
|
|
52
|
+
* (platform-step rule 2: blocked, not failing, while it is not). A lost mint -- an undelivered id
|
|
53
|
+
* with no done guard -- is reported blocked and never minted again (rule 1); the operator clears the
|
|
54
|
+
* recorded id once the role is confirmed gone from the server.
|
|
55
|
+
*/
|
|
56
|
+
export function planOrgAppsAdminStep(org, recordedIds, options = {}) {
|
|
57
|
+
if (recordedIds[ORG_APPS_ADMIN_SCOPE_KEY])
|
|
58
|
+
return { step: null, blocked: null };
|
|
59
|
+
if (!recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY]) {
|
|
60
|
+
return {
|
|
61
|
+
step: null,
|
|
62
|
+
blocked: `${org.slug}'s apps admin role is blocked, not planned: its Postgres server is not provisioned yet`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const lost = recordedIds[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY];
|
|
66
|
+
if (lost) {
|
|
67
|
+
return {
|
|
68
|
+
step: null,
|
|
69
|
+
blocked: `${org.slug}'s apps admin role (note ${lost}): a run set a new password on the role but its connection URL was never saved. Nothing is lost. To start over, turn apps off and apply (that retires the role and clears this note), then turn apps on again`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (options.canHoldSecrets === false) {
|
|
73
|
+
return {
|
|
74
|
+
step: null,
|
|
75
|
+
blocked: `${org.slug}'s apps admin role is blocked, not planned: the host cannot hold secrets`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
step: {
|
|
80
|
+
action: { system: 'coolify', kind: 'org-apps-admin', key: org.id, desired: {} },
|
|
81
|
+
step: {
|
|
82
|
+
description: `mint ${org.slug}'s database admin role (${appsAdminRoleName(org)}); it can create databases and roles but touches no existing database`,
|
|
83
|
+
destroys: false,
|
|
84
|
+
migrates: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
blocked: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Pure. F4 (#925): the wiring of {@link planOrgAppsAdminStep} to `org.boule.apps`. Apps on: the mint
|
|
92
|
+
* step, once Boule (which receives the URL) and the Postgres server are recorded or planned. Apps off with
|
|
93
|
+
* the role recorded (or a mint never delivered): the retire step, which needs no secret and drops nothing.
|
|
94
|
+
*/
|
|
95
|
+
export function planAppsAdmin(org, recordedIds, plannedSteps, options) {
|
|
96
|
+
const key = `coolify:org-apps-admin:${org.id}`;
|
|
97
|
+
const blocked = [];
|
|
98
|
+
if (!appsOn(org)) {
|
|
99
|
+
if (!recordedIds[ORG_APPS_ADMIN_SCOPE_KEY] &&
|
|
100
|
+
!recordedIds[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY]) {
|
|
101
|
+
return { steps: [], blocked };
|
|
102
|
+
}
|
|
103
|
+
if (!recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY]) {
|
|
104
|
+
blocked.push({
|
|
105
|
+
description: `${org.slug}'s apps admin role is not retired, not planned: its Postgres server is not recorded`,
|
|
106
|
+
key,
|
|
107
|
+
});
|
|
108
|
+
return { steps: [], blocked };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
steps: [
|
|
112
|
+
{
|
|
113
|
+
action: {
|
|
114
|
+
system: 'coolify',
|
|
115
|
+
kind: 'org-apps-admin',
|
|
116
|
+
key: org.id,
|
|
117
|
+
desired: { mode: 'retire' },
|
|
118
|
+
},
|
|
119
|
+
step: {
|
|
120
|
+
description: `retire ${org.slug}'s database admin role (${appsAdminRoleName(org)}): no login, out of Boule's env; no database is dropped`,
|
|
121
|
+
destroys: true,
|
|
122
|
+
migrates: false,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
blocked,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const bouleReady = Boolean(recordedIds[`coolify:boule:${org.id}`]) ||
|
|
130
|
+
plannedSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'boule' && p.action.key === org.id);
|
|
131
|
+
if (!bouleReady && !recordedIds[ORG_APPS_ADMIN_SCOPE_KEY]) {
|
|
132
|
+
blocked.push({
|
|
133
|
+
description: `${org.slug}'s apps admin role is blocked, not planned: Boule is not recorded yet`,
|
|
134
|
+
key,
|
|
135
|
+
});
|
|
136
|
+
return { steps: [], blocked };
|
|
137
|
+
}
|
|
138
|
+
const seen = options.postgresPlanned && !recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY]
|
|
139
|
+
? { ...recordedIds, [ORG_POSTGRES_SERVER_SCOPE_KEY]: 'planned' }
|
|
140
|
+
: recordedIds;
|
|
141
|
+
const planned = planOrgAppsAdminStep(org, seen, { canHoldSecrets: options.canHoldSecrets });
|
|
142
|
+
if (planned.blocked)
|
|
143
|
+
blocked.push({ description: planned.blocked, key });
|
|
144
|
+
return { steps: planned.step ? [planned.step] : [], blocked };
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The job's script: makes the role (idempotent, kept if it already exists), sets its password and grants
|
|
148
|
+
* it CONNECT on `postgres`. It revokes nothing from PUBLIC (see the file comment). Roles are made by
|
|
149
|
+
* `format('%I')` and the password by `format('%L')`, so no value is spliced into SQL by the shell. Setting
|
|
150
|
+
* the password again is harmless, so a mint that is repeated simply replaces it. It never prints a URL or a password: psql's output is discarded and the one
|
|
151
|
+
* error line names neither.
|
|
152
|
+
*/
|
|
153
|
+
export function orgAppsAdminScript(role, mode = 'mint') {
|
|
154
|
+
const owner = checked(role);
|
|
155
|
+
if (mode === 'retire')
|
|
156
|
+
return orgAppsAdminRetireScript(owner);
|
|
157
|
+
const JOB_DONE_FILE = '/tmp/done';
|
|
158
|
+
const JOB_FAILED_FILE = '/tmp/failed';
|
|
159
|
+
return [
|
|
160
|
+
'#!/bin/sh',
|
|
161
|
+
'set -eu',
|
|
162
|
+
`trap '[ -f ${JOB_DONE_FILE} ] || touch ${JOB_FAILED_FILE}' EXIT`,
|
|
163
|
+
"trap 'exit 1' HUP INT TERM",
|
|
164
|
+
`[ ! -f ${JOB_FAILED_FILE} ] || exit 1`,
|
|
165
|
+
`rm -f ${JOB_DONE_FILE}`,
|
|
166
|
+
'fail() { echo "org-apps-admin: $1 failed"; exit 1; }',
|
|
167
|
+
'[ -n "${PW:-}" ] || fail "role"',
|
|
168
|
+
'psql "$ADMIN_DATABASE_URL" -X -q -v ON_ERROR_STOP=1 > /dev/null 2>&1 <<\'SQL\' || fail "role"',
|
|
169
|
+
`\\set role '${owner}'`,
|
|
170
|
+
'\\getenv pw PW',
|
|
171
|
+
"SELECT format('CREATE ROLE %I LOGIN CREATEDB CREATEROLE', :'role') WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'role') \\gexec",
|
|
172
|
+
"SELECT format('ALTER ROLE %I LOGIN CREATEDB CREATEROLE PASSWORD %L', :'role', :'pw') \\gexec",
|
|
173
|
+
"SELECT format('GRANT CONNECT ON DATABASE postgres TO %I', :'role') \\gexec",
|
|
174
|
+
'SQL',
|
|
175
|
+
'echo "org-apps-admin: role ready"',
|
|
176
|
+
`touch ${JOB_DONE_FILE}`,
|
|
177
|
+
'exec sleep infinity',
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The retire script: the role stops being able to log in and its open sessions end. It drops nothing and
|
|
182
|
+
* grants nothing; CONNECT stays closed to PUBLIC. A role that is not there is left alone.
|
|
183
|
+
*/
|
|
184
|
+
function orgAppsAdminRetireScript(owner) {
|
|
185
|
+
const JOB_DONE_FILE = '/tmp/done';
|
|
186
|
+
const JOB_FAILED_FILE = '/tmp/failed';
|
|
187
|
+
return [
|
|
188
|
+
'#!/bin/sh',
|
|
189
|
+
'set -eu',
|
|
190
|
+
`trap '[ -f ${JOB_DONE_FILE} ] || touch ${JOB_FAILED_FILE}' EXIT`,
|
|
191
|
+
"trap 'exit 1' HUP INT TERM",
|
|
192
|
+
`[ ! -f ${JOB_FAILED_FILE} ] || exit 1`,
|
|
193
|
+
`rm -f ${JOB_DONE_FILE}`,
|
|
194
|
+
'fail() { echo "org-apps-admin: $1 failed"; exit 1; }',
|
|
195
|
+
'psql "$ADMIN_DATABASE_URL" -X -q -v ON_ERROR_STOP=1 > /dev/null 2>&1 <<\'SQL\' || fail "retire"',
|
|
196
|
+
`\\set role '${owner}'`,
|
|
197
|
+
"SELECT format('ALTER ROLE %I NOLOGIN', :'role') WHERE EXISTS (SELECT FROM pg_roles WHERE rolname = :'role') \\gexec",
|
|
198
|
+
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = :'role'",
|
|
199
|
+
'SQL',
|
|
200
|
+
'echo "org-apps-admin: role retired"',
|
|
201
|
+
`touch ${JOB_DONE_FILE}`,
|
|
202
|
+
'exec sleep infinity',
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
/** The job: the same pinned image as the other Postgres jobs, so `psql` matches the server's major version. */
|
|
206
|
+
export function orgAppsAdminDockerfile(role, mode = 'mint') {
|
|
207
|
+
const encoded = Buffer.from(`${orgAppsAdminScript(role, mode).join('\n')}\n`, 'utf8').toString('base64');
|
|
208
|
+
return [
|
|
209
|
+
`FROM postgres:${pinnedRef('postgres')}`,
|
|
210
|
+
`RUN echo '${encoded}' | base64 -d > /usr/local/bin/org-apps-admin && chmod 0755 /usr/local/bin/org-apps-admin`,
|
|
211
|
+
'ENTRYPOINT ["/usr/local/bin/org-apps-admin"]',
|
|
212
|
+
`HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 CMD ["test", "-f", "/tmp/done"]`,
|
|
213
|
+
].join('\n');
|
|
214
|
+
}
|
|
215
|
+
const mint = () => randomBytes(24).toString('base64url');
|
|
216
|
+
const mintOpaque = () => randomBytes(12).toString('hex');
|
|
217
|
+
/**
|
|
218
|
+
* Makes the role true and delivers its connection URL. The password is minted in memory. The opaque id
|
|
219
|
+
* {@link ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY} is recorded and persisted only once the job that sets the
|
|
220
|
+
* password is about to be deployed ("password minted and role touched"); a failure before that leaves no
|
|
221
|
+
* id and the next apply simply retries, since setting a password again is harmless. The done guard is
|
|
222
|
+
* recorded only once the URL is delivered. A run that dies between the two finds the id and stops (the
|
|
223
|
+
* planner reports it blocked); retiring the role (apps off) clears it.
|
|
224
|
+
*/
|
|
225
|
+
export async function provisionOrgAppsAdmin(scope, deps, org) {
|
|
226
|
+
const label = `${org.slug}'s apps admin role`;
|
|
227
|
+
const coolify = withCallTimeout(deps.coolify);
|
|
228
|
+
const recorded = scope.recordedIds();
|
|
229
|
+
if (recorded[ORG_APPS_ADMIN_SCOPE_KEY])
|
|
230
|
+
return;
|
|
231
|
+
const serverId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
|
|
232
|
+
if (!serverId)
|
|
233
|
+
throw new Error(`${label}: the Postgres server is not recorded yet`);
|
|
234
|
+
if (recorded[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY]) {
|
|
235
|
+
throw new Error(`${label}: an earlier run set a new password on the role but never saved its connection URL. Nothing is lost. To start over, turn apps off and apply (that retires the role and clears this note), then turn apps on again`);
|
|
236
|
+
}
|
|
237
|
+
const onSecret = (value) => deps.onSecret?.(value);
|
|
238
|
+
const server = await boundedCall(deps.startDatabase(serverId), 'Coolify startDatabase');
|
|
239
|
+
if (!server.internal_db_url || !URL.canParse(server.internal_db_url)) {
|
|
240
|
+
throw new Error(`${label}: the Postgres server has no usable internal URL`);
|
|
241
|
+
}
|
|
242
|
+
const adminUrl = server.internal_db_url;
|
|
243
|
+
onSecret(adminUrl);
|
|
244
|
+
const adminParsed = new URL(adminUrl);
|
|
245
|
+
const host = adminParsed.hostname;
|
|
246
|
+
const port = adminParsed.port || '5432';
|
|
247
|
+
if (!host)
|
|
248
|
+
throw new Error(`${label}: the Postgres server's internal URL has no host`);
|
|
249
|
+
const adminPassword = decodeURIComponent(adminParsed.password);
|
|
250
|
+
if (adminPassword)
|
|
251
|
+
onSecret(adminPassword);
|
|
252
|
+
const role = appsAdminRoleName(org);
|
|
253
|
+
const password = (deps.mintPassword ?? mint)();
|
|
254
|
+
onSecret(password);
|
|
255
|
+
const jobName = appsAdminJobName(org);
|
|
256
|
+
let jobAppId;
|
|
257
|
+
try {
|
|
258
|
+
const projectUuid = await deps.ensureProject();
|
|
259
|
+
const { environment } = await coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
260
|
+
const leftover = await coolify.findApplicationByName(jobName, {
|
|
261
|
+
environmentIds: [environment.id],
|
|
262
|
+
});
|
|
263
|
+
if (leftover)
|
|
264
|
+
await coolify.raw('DELETE', `/applications/${leftover.uuid}`);
|
|
265
|
+
const created = await coolify.createDockerfileApplication({
|
|
266
|
+
project_uuid: projectUuid,
|
|
267
|
+
server_uuid: await deps.serverUuid(),
|
|
268
|
+
environment_name: environment.name,
|
|
269
|
+
environment_uuid: environment.uuid,
|
|
270
|
+
dockerfile: orgAppsAdminDockerfile(role),
|
|
271
|
+
name: jobName,
|
|
272
|
+
description: `${org.slug}'s apps admin role job`,
|
|
273
|
+
instant_deploy: false,
|
|
274
|
+
autogenerate_domain: false,
|
|
275
|
+
});
|
|
276
|
+
jobAppId = created.uuid;
|
|
277
|
+
scope.recordScopeId(ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY, jobAppId);
|
|
278
|
+
await scope.persist();
|
|
279
|
+
await coolify.updateApplication(jobAppId, { health_check_enabled: false });
|
|
280
|
+
await coolify.bulkUpsertEnvs(jobAppId, [
|
|
281
|
+
{ key: 'ADMIN_DATABASE_URL', value: adminUrl },
|
|
282
|
+
{ key: 'PW', value: password },
|
|
283
|
+
]);
|
|
284
|
+
// From here the job can set the password: the marker is recorded now, not earlier.
|
|
285
|
+
scope.recordScopeId(ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY, (deps.mintId ?? mintOpaque)());
|
|
286
|
+
await scope.persist();
|
|
287
|
+
await coolify.deploy(jobAppId, true);
|
|
288
|
+
await waitForJob(coolify, jobAppId, deps.runtime ?? {}, {
|
|
289
|
+
job: 'the apps admin job',
|
|
290
|
+
stopped: 'the apps admin job stopped before it finished',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
const why = error instanceof JobFailure ? error.message : 'a Coolify call failed or timed out';
|
|
295
|
+
let cleaned = '';
|
|
296
|
+
if (jobAppId) {
|
|
297
|
+
const id = jobAppId;
|
|
298
|
+
try {
|
|
299
|
+
await boundedCall(coolify.raw('POST', `/applications/${id}/stop`), 'stop the job');
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
cleaned += '; the job app could not be stopped, stop it in Coolify';
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await boundedCall(coolify.bulkUpsertEnvs(id, [
|
|
306
|
+
{ key: 'ADMIN_DATABASE_URL', value: '' },
|
|
307
|
+
{ key: 'PW', value: '' },
|
|
308
|
+
]), 'clear the job env');
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
cleaned +=
|
|
312
|
+
'; its env could not be cleared, delete the app in Coolify (it holds the admin URL)';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`${label}: ${why}${jobAppId ? `; the job app ${jobAppId} is left stopped without secrets for inspection and the next apply replaces it${cleaned}` : ''}; apply again`);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await coolify.raw('DELETE', `/applications/${jobAppId}`);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
throw new Error(`${label}: the role was made, but its job app ${jobAppId} could not be deleted; delete it in Coolify (it holds the admin URL), then apply again`);
|
|
322
|
+
}
|
|
323
|
+
const url = `postgres://${encodeURIComponent(role)}:${encodeURIComponent(password)}@${host}:${port}/postgres`;
|
|
324
|
+
onSecret(url);
|
|
325
|
+
let delivered = false;
|
|
326
|
+
for (let attempt = 1; attempt <= ORG_APPS_ADMIN_DELIVERY_ATTEMPTS && !delivered; attempt++) {
|
|
327
|
+
try {
|
|
328
|
+
await boundedCall(deps.deliverSecret(appsAdminUrlRef(org), url), 'deliverSecret');
|
|
329
|
+
delivered = true;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Retried; the cause may echo the secret and is dropped.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!delivered) {
|
|
336
|
+
throw new Error(`${label}: the role was made, but its connection URL could not be saved after ${ORG_APPS_ADMIN_DELIVERY_ATTEMPTS} attempts. Nothing is lost. Turn apps off and apply (that retires the role and clears the note), then turn apps on again`);
|
|
337
|
+
}
|
|
338
|
+
scope.recordScopeId(ORG_APPS_ADMIN_SCOPE_KEY, orgPostgresServerName(org));
|
|
339
|
+
scope.recordScopeId(ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY, '');
|
|
340
|
+
await scope.persist();
|
|
341
|
+
scope.recordScopeId(ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY, '');
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Apps went off (F4, #925): the role stops logging in, the URL leaves Boule's env and the held store,
|
|
345
|
+
* and the recorded ids are cleared last so a failed run repeats. Never drops the role, a database or
|
|
346
|
+
* a role an app made (CLAUDE.md: never drop data); turning apps on again mints a new password for the
|
|
347
|
+
* same role. A recorded mint that was never delivered is retired the same way, which is also how an
|
|
348
|
+
* operator unblocks a lost mint. The job needs the server's admin URL and nothing else secret.
|
|
349
|
+
*/
|
|
350
|
+
export async function retireOrgAppsAdmin(scope, deps, org) {
|
|
351
|
+
const label = `${org.slug}'s apps admin role`;
|
|
352
|
+
const coolify = withCallTimeout(deps.coolify);
|
|
353
|
+
const recorded = scope.recordedIds();
|
|
354
|
+
const serverId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
|
|
355
|
+
const hadRole = Boolean(recorded[ORG_APPS_ADMIN_SCOPE_KEY] || recorded[ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY]);
|
|
356
|
+
if (hadRole) {
|
|
357
|
+
if (!serverId)
|
|
358
|
+
throw new Error(`${label}: the Postgres server is not recorded; cannot retire it`);
|
|
359
|
+
const onSecret = (value) => deps.onSecret?.(value);
|
|
360
|
+
const server = await boundedCall(deps.startDatabase(serverId), 'Coolify startDatabase');
|
|
361
|
+
if (!server.internal_db_url || !URL.canParse(server.internal_db_url)) {
|
|
362
|
+
throw new Error(`${label}: the Postgres server has no usable internal URL`);
|
|
363
|
+
}
|
|
364
|
+
onSecret(server.internal_db_url);
|
|
365
|
+
const password = decodeURIComponent(new URL(server.internal_db_url).password);
|
|
366
|
+
if (password)
|
|
367
|
+
onSecret(password);
|
|
368
|
+
const jobName = appsAdminJobName(org);
|
|
369
|
+
let jobAppId;
|
|
370
|
+
try {
|
|
371
|
+
const projectUuid = await deps.ensureProject();
|
|
372
|
+
const { environment } = await coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
373
|
+
const leftover = await coolify.findApplicationByName(jobName, {
|
|
374
|
+
environmentIds: [environment.id],
|
|
375
|
+
});
|
|
376
|
+
if (leftover)
|
|
377
|
+
await coolify.raw('DELETE', `/applications/${leftover.uuid}`);
|
|
378
|
+
const created = await coolify.createDockerfileApplication({
|
|
379
|
+
project_uuid: projectUuid,
|
|
380
|
+
server_uuid: await deps.serverUuid(),
|
|
381
|
+
environment_name: environment.name,
|
|
382
|
+
environment_uuid: environment.uuid,
|
|
383
|
+
dockerfile: orgAppsAdminDockerfile(appsAdminRoleName(org), 'retire'),
|
|
384
|
+
name: jobName,
|
|
385
|
+
description: `${org.slug}'s apps admin role retire job`,
|
|
386
|
+
instant_deploy: false,
|
|
387
|
+
autogenerate_domain: false,
|
|
388
|
+
});
|
|
389
|
+
jobAppId = created.uuid;
|
|
390
|
+
scope.recordScopeId(ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY, jobAppId);
|
|
391
|
+
await scope.persist();
|
|
392
|
+
await coolify.updateApplication(jobAppId, { health_check_enabled: false });
|
|
393
|
+
await coolify.bulkUpsertEnvs(jobAppId, [
|
|
394
|
+
{ key: 'ADMIN_DATABASE_URL', value: server.internal_db_url },
|
|
395
|
+
]);
|
|
396
|
+
await coolify.deploy(jobAppId, true);
|
|
397
|
+
await waitForJob(coolify, jobAppId, deps.runtime ?? {}, {
|
|
398
|
+
job: 'the apps admin retire job',
|
|
399
|
+
stopped: 'the apps admin retire job stopped before it finished',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
const why = error instanceof JobFailure ? error.message : 'a Coolify call failed or timed out';
|
|
404
|
+
let cleaned = '';
|
|
405
|
+
if (jobAppId) {
|
|
406
|
+
const id = jobAppId;
|
|
407
|
+
try {
|
|
408
|
+
await boundedCall(coolify.raw('POST', `/applications/${id}/stop`), 'stop the job');
|
|
409
|
+
await boundedCall(coolify.bulkUpsertEnvs(id, [{ key: 'ADMIN_DATABASE_URL', value: '' }]), 'clear the job env');
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
cleaned = '; delete the job app in Coolify (it holds the admin URL)';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
throw new Error(`${label}: ${why}${cleaned}; apply again`);
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
await coolify.raw('DELETE', `/applications/${jobAppId}`);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
throw new Error(`${label}: the role was retired, but its job app ${jobAppId} could not be deleted; delete it in Coolify (it holds the admin URL), then apply again`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
await deps.removeBouleEnv();
|
|
425
|
+
await deps.clearHeldUrl(appsAdminUrlRef(org));
|
|
426
|
+
for (const key of [
|
|
427
|
+
ORG_APPS_ADMIN_SCOPE_KEY,
|
|
428
|
+
ORG_APPS_ADMIN_UNDELIVERED_SCOPE_KEY,
|
|
429
|
+
ORG_APPS_ADMIN_JOB_APP_SCOPE_KEY,
|
|
430
|
+
]) {
|
|
431
|
+
scope.recordScopeId(key, '');
|
|
432
|
+
}
|
|
433
|
+
await scope.persist();
|
|
434
|
+
}
|
package/dist/plan/org-names.d.ts
CHANGED
|
@@ -6,13 +6,13 @@ export type OrgNameSource = 'own' | 'hosting';
|
|
|
6
6
|
* hosting entries: Agora, Boule, the tunnel and its connector, and chat,
|
|
7
7
|
* calls and the cms site while they are on.
|
|
8
8
|
*/
|
|
9
|
-
export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site'>): string[];
|
|
9
|
+
export declare function companyServiceNames(org: Pick<DeclaredOrg, 'slug' | 'chat' | 'calls' | 'site' | 'kosmos'>): string[];
|
|
10
10
|
/**
|
|
11
11
|
* Every container name, network alias, database, tunnel and bucket name that
|
|
12
12
|
* foundry derives for an org: its own-service names when it owns services,
|
|
13
13
|
* and its hosting-entry and database names.
|
|
14
14
|
*/
|
|
15
|
-
export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>): {
|
|
15
|
+
export declare function orgDerivedNames(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>): {
|
|
16
16
|
name: string;
|
|
17
17
|
source: OrgNameSource;
|
|
18
18
|
}[];
|
|
@@ -26,5 +26,5 @@ export interface NameClash {
|
|
|
26
26
|
source: OrgNameSource;
|
|
27
27
|
}
|
|
28
28
|
/** Every name `org` derives that another org or the platform also derives. */
|
|
29
|
-
export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site'>[]): NameClash[];
|
|
29
|
+
export declare function findNameClashes(org: Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>, others: readonly Pick<DeclaredOrg, 'slug' | 'domain' | 'services' | 'hosting' | 'chat' | 'calls' | 'site' | 'kosmos'>[]): NameClash[];
|
|
30
30
|
export declare function nameClashMessage(slug: string, clash: NameClash): string;
|
package/dist/plan/org-names.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ownsServices } from '../org.js';
|
|
1
|
+
import { kosmosOn, ownsServices } from '../org.js';
|
|
2
2
|
import { entryHasDatabase } from '../worker-app-config.js';
|
|
3
3
|
import { chatInternalNames, chatRekeyedInternalNames } from './chat-matrix.js';
|
|
4
|
-
import { workerAppHostname } from './hostnames.js';
|
|
4
|
+
import { workerAppHostname, workspaceHost, workspaceInboxHost } from './hostnames.js';
|
|
5
5
|
import { orgTarget, platformTarget } from './own-services.js';
|
|
6
6
|
import { appNameFor } from './platform-diff.js';
|
|
7
7
|
/**
|
|
@@ -20,6 +20,8 @@ export function companyServiceNames(org) {
|
|
|
20
20
|
`${s}-tunnel`,
|
|
21
21
|
`${s}-cloudflared`,
|
|
22
22
|
];
|
|
23
|
+
if (kosmosOn(org))
|
|
24
|
+
names.push(`${s}-kosmos`, `${s}-kosmos-db`);
|
|
23
25
|
if (org.chat?.enabled) {
|
|
24
26
|
names.push(...Object.values(chatInternalNames(s)), ...Object.values(chatRekeyedInternalNames(s)));
|
|
25
27
|
}
|
|
@@ -61,6 +63,17 @@ export function orgDerivedNames(org) {
|
|
|
61
63
|
if (org.hosting.entries.some(entryHasDatabase)) {
|
|
62
64
|
names.push({ name: `${org.slug}-db`, source: 'hosting' });
|
|
63
65
|
}
|
|
66
|
+
// A workspace's host and the test inbox's: one label under one zone, so it is compared across orgs like the names above.
|
|
67
|
+
if (org.hosting.enabled) {
|
|
68
|
+
for (const entry of org.hosting.entries) {
|
|
69
|
+
if (entry.workspace) {
|
|
70
|
+
names.push({ name: workspaceHost(org.slug, entry.key), source: 'hosting' });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (org.hosting.entries.some((e) => e.workspace && !e.workerApp)) {
|
|
74
|
+
names.push({ name: workspaceInboxHost(org.slug), source: 'hosting' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
64
77
|
return names;
|
|
65
78
|
}
|
|
66
79
|
/** Names the platform's own steps give apps, tunnels, connectors, databases and projects outside `platformTarget()`. */
|
package/dist/plan/own-diff.d.ts
CHANGED
|
@@ -24,6 +24,29 @@ export declare function planOwnJobs(options: PlanOwnJobsOptions): {
|
|
|
24
24
|
step: DraftStep | null;
|
|
25
25
|
blocked: string | null;
|
|
26
26
|
};
|
|
27
|
+
/** `unmanaged` key of the one line `planOwnJobsRoute` reports. */
|
|
28
|
+
export declare const OWN_JOBS_ROUTE_BLOCKED_KEY = "own-jobs-route";
|
|
29
|
+
export interface PlanOwnJobsRouteOptions {
|
|
30
|
+
org: DeclaredOrg;
|
|
31
|
+
recordedIds: Readonly<Record<string, string>>;
|
|
32
|
+
readiness: OrgPlatformReadiness;
|
|
33
|
+
/** The executor was built with an `ownServices` port. */
|
|
34
|
+
ownServices: boolean;
|
|
35
|
+
/** The org's zone is live and active: none is planned and none is waiting. */
|
|
36
|
+
zoneActive: boolean;
|
|
37
|
+
/** The org's own scheduler step is planned in this same plan, so its address is recorded by the time the route runs. */
|
|
38
|
+
schedulerPlanned: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The public route of an own org's jobs scheduler: `jobs.<domain>`, serving only what an app calls. The
|
|
42
|
+
* owner company always publishes its scheduler, since every company on shared services registers there; any
|
|
43
|
+
* other own org publishes its own only while an entry declares `uses.jobs`. Planned again when the hostname
|
|
44
|
+
* moved. Every blocked line is fixed text: it is part of the stale-plan comparison.
|
|
45
|
+
*/
|
|
46
|
+
export declare function planOwnJobsRoute(options: PlanOwnJobsRouteOptions): {
|
|
47
|
+
step: DraftStep | null;
|
|
48
|
+
blocked: string | null;
|
|
49
|
+
};
|
|
27
50
|
/** `unmanaged` key of the one line `planOwnAuth` reports. */
|
|
28
51
|
export declare const OWN_AUTH_BLOCKED_KEY = "own-auth";
|
|
29
52
|
export { OWN_AUTH_APPLIED_HOSTNAME_SCOPE_KEY };
|