@wtfalch/foundry 0.60.0 → 0.61.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 +10 -5
- package/dist/index.js +5 -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/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/org.d.ts +163 -22
- package/dist/org.js +419 -47
- 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 +41 -1
- package/dist/plan/blue-green.js +80 -2
- 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/desktop-callback.js +12 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +373 -42
- package/dist/plan/edge-access.d.ts +6 -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 +191 -1
- package/dist/plan/executor.js +1505 -90
- 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/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +2 -2
- 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.js +12 -1
- 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-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 +7 -2
- package/dist/plan/post-logout-uris.js +7 -1
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- 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/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 +11 -0
- 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 +10 -6
- package/dist/store/postgres.js +12 -2
- package/dist/store/schema.d.ts +37 -1
- package/dist/store/schema.js +4 -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 +18 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { DraftStep } from './diff.js';
|
|
3
|
+
import type { StepExecutionContext } from './plan-apply.js';
|
|
4
|
+
import { type WorkerAppDatabaseDeps } from './worker-app-database.js';
|
|
5
|
+
/**
|
|
6
|
+
* F4 (#925): the Hyperdrive route to the company's own Postgres server (`<slug>-postgres`, the one the
|
|
7
|
+
* apps admin role lives on). Boule creates a database per app copy on that server (`app_<app>_<copy>`) and
|
|
8
|
+
* the edge it calls makes one Hyperdrive config per database. Hyperdrive reaches a database inside Coolify's
|
|
9
|
+
* Docker network only through the company's Cloudflare tunnel behind an Access service token, so this step
|
|
10
|
+
* makes the route once for the server, not once per database:
|
|
11
|
+
*
|
|
12
|
+
* Access app on `<8 hex of org id>-apps-db-hd.<zone>` (a service-token policy) -> tunnel rule
|
|
13
|
+
* `tcp://<server host>:5432` -> proxied CNAME to the tunnel,
|
|
14
|
+
*
|
|
15
|
+
* the same three parts `worker-app-database.ts` makes for a Worker app's own database (`ensureDatabaseRoute`
|
|
16
|
+
* is shared), under the scope-id family `bgdb.apps-db.*`, so org destroy and the existing teardown find them.
|
|
17
|
+
* The zone is the company's domain, or `oikos.host` for a company without one. The service token is minted
|
|
18
|
+
* once (platform-step rule 1) and held; its id and secret are given to Boule's env, and nowhere else.
|
|
19
|
+
*
|
|
20
|
+
* Boule's env gets: {@link APPS_DATABASE_HOST_ENV} (the tunnel hostname Hyperdrive connects to),
|
|
21
|
+
* {@link APPS_DATABASE_ACCESS_CLIENT_ID_ENV} and {@link APPS_DATABASE_ACCESS_CLIENT_SECRET_ENV} (the Access
|
|
22
|
+
* service token), {@link APPS_DATABASE_PORT_ENV}. The company's Postgres server speaks no TLS of its own, so
|
|
23
|
+
* there is no CA to give: the encrypted leg is Hyperdrive to Cloudflare's edge and the tunnel.
|
|
24
|
+
*/
|
|
25
|
+
export declare const APPS_DB_ENTRY_KEY = "apps-db";
|
|
26
|
+
export declare const APPS_DATABASE_HOST_ENV = "APPS_DATABASE_HOST";
|
|
27
|
+
export declare const APPS_DATABASE_PORT_ENV = "APPS_DATABASE_PORT";
|
|
28
|
+
export declare const APPS_DATABASE_ACCESS_CLIENT_ID_ENV = "APPS_DATABASE_ACCESS_CLIENT_ID";
|
|
29
|
+
export declare const APPS_DATABASE_ACCESS_CLIENT_SECRET_ENV = "APPS_DATABASE_ACCESS_CLIENT_SECRET";
|
|
30
|
+
export declare const APPS_DATABASE_ADMIN_URL_ENV = "APPS_DATABASE_ADMIN_URL";
|
|
31
|
+
export declare const APPS_DATABASE_ROUTE_ENVS: readonly ["APPS_DATABASE_HOST", "APPS_DATABASE_PORT", "APPS_DATABASE_ACCESS_CLIENT_ID", "APPS_DATABASE_ACCESS_CLIENT_SECRET"];
|
|
32
|
+
/** The tunnel hostname last written to Boule, recorded last so a failed run is repeated. */
|
|
33
|
+
export declare const APPS_DB_ROUTE_APPLIED_SCOPE_KEY = "apps_db_route_applied";
|
|
34
|
+
/** The tunnel hostname of the company's apps database route. */
|
|
35
|
+
export declare const appsDbHostname: (org: DeclaredOrg) => string;
|
|
36
|
+
export interface PlanAppsDbRouteOptions {
|
|
37
|
+
/** Access, tunnel, DNS and zone ports are available (edge). */
|
|
38
|
+
accessReady: boolean;
|
|
39
|
+
canHoldSecrets: boolean;
|
|
40
|
+
/** The tunnel is recorded or planned in this plan. */
|
|
41
|
+
tunnelReady: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pure. `apply` once the company's apps are on and everything the route needs is recorded or planned;
|
|
45
|
+
* `retire` when they went off and the route is still recorded. Anything missing is blocked, not planned
|
|
46
|
+
* (platform-step rule 2); a service token minted and never held is blocked and never minted again (rule 1).
|
|
47
|
+
*/
|
|
48
|
+
export declare function planAppsDbRoute(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], options: PlanAppsDbRouteOptions): {
|
|
49
|
+
steps: DraftStep[];
|
|
50
|
+
blocked: Array<{
|
|
51
|
+
description: string;
|
|
52
|
+
key: string;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
/** The Boule app and its env, as the executor wires them. */
|
|
56
|
+
export interface AppsDbRouteBoule {
|
|
57
|
+
upsertEnvs(rows: Array<{
|
|
58
|
+
key: string;
|
|
59
|
+
value: string;
|
|
60
|
+
literal: true;
|
|
61
|
+
}>): Promise<void>;
|
|
62
|
+
deleteEnvs(names: readonly string[]): Promise<void>;
|
|
63
|
+
restart(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
export interface AppsDbRouteDeps {
|
|
66
|
+
database: Pick<WorkerAppDatabaseDeps, 'tunnels' | 'access' | 'dns' | 'zones'>;
|
|
67
|
+
heldKeys: {
|
|
68
|
+
resolve(ref: string): Promise<string | null | undefined>;
|
|
69
|
+
put?(ref: string, value: string): Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
/** Registers a value for redaction before it is written anywhere. */
|
|
72
|
+
secret(value: string): void;
|
|
73
|
+
/** The Postgres server's host inside the Docker network (its internal URL's host). */
|
|
74
|
+
upstreamHost(serverId: string): Promise<string>;
|
|
75
|
+
boule: AppsDbRouteBoule;
|
|
76
|
+
/** Test hook: the timeout of a call, in ms. */
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
/** Makes the route true, then gives Boule its env and restarts it. The applied marker is recorded last. */
|
|
80
|
+
export declare function applyAppsDbRoute(deps: AppsDbRouteDeps, org: DeclaredOrg, context: StepExecutionContext): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Apps went off: Boule's env rows, then the DNS record, tunnel rule, Access app and service token (and the
|
|
83
|
+
* held token), then the applied marker. The databases and the role on the server are not touched. A run that
|
|
84
|
+
* fails halfway is repeated by the next apply (the ids are cleared last).
|
|
85
|
+
*/
|
|
86
|
+
export declare function retireAppsDbRoute(deps: AppsDbRouteDeps, org: DeclaredOrg, context: StepExecutionContext): Promise<void>;
|
|
87
|
+
/** The held secret ref of the route's Access token (for tests and the executor). */
|
|
88
|
+
export declare const appsDbAccessRef: (org: Pick<DeclaredOrg, "id">) => string;
|
|
89
|
+
/** True when the apps admin role is recorded: the route is worth making only with it (used by the planner's caller). */
|
|
90
|
+
export declare const appsAdminRecorded: (recorded: Readonly<Record<string, string>>) => boolean;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ORG_POSTGRES_SERVER_SCOPE_KEY, appsOn } from '../org.js';
|
|
2
|
+
import { withCallTimeout } from '../worker-app-upload.js';
|
|
3
|
+
import { ORG_APPS_ADMIN_SCOPE_KEY } from './org-apps-admin.js';
|
|
4
|
+
import { REMOTE_DATABASE_PORT, remoteDatabaseKeys } from './remote-database.js';
|
|
5
|
+
import { deleteWorkerAppDatabaseRoute, ensureDatabaseRoute, ensureServiceToken, hyperdriveHostname, } from './worker-app-database.js';
|
|
6
|
+
import { WORKER_APP_CALL_TIMEOUT_MS, workerAppApex, workerAppSecretRef } from './worker-app.js';
|
|
7
|
+
/**
|
|
8
|
+
* F4 (#925): the Hyperdrive route to the company's own Postgres server (`<slug>-postgres`, the one the
|
|
9
|
+
* apps admin role lives on). Boule creates a database per app copy on that server (`app_<app>_<copy>`) and
|
|
10
|
+
* the edge it calls makes one Hyperdrive config per database. Hyperdrive reaches a database inside Coolify's
|
|
11
|
+
* Docker network only through the company's Cloudflare tunnel behind an Access service token, so this step
|
|
12
|
+
* makes the route once for the server, not once per database:
|
|
13
|
+
*
|
|
14
|
+
* Access app on `<8 hex of org id>-apps-db-hd.<zone>` (a service-token policy) -> tunnel rule
|
|
15
|
+
* `tcp://<server host>:5432` -> proxied CNAME to the tunnel,
|
|
16
|
+
*
|
|
17
|
+
* the same three parts `worker-app-database.ts` makes for a Worker app's own database (`ensureDatabaseRoute`
|
|
18
|
+
* is shared), under the scope-id family `bgdb.apps-db.*`, so org destroy and the existing teardown find them.
|
|
19
|
+
* The zone is the company's domain, or `oikos.host` for a company without one. The service token is minted
|
|
20
|
+
* once (platform-step rule 1) and held; its id and secret are given to Boule's env, and nowhere else.
|
|
21
|
+
*
|
|
22
|
+
* Boule's env gets: {@link APPS_DATABASE_HOST_ENV} (the tunnel hostname Hyperdrive connects to),
|
|
23
|
+
* {@link APPS_DATABASE_ACCESS_CLIENT_ID_ENV} and {@link APPS_DATABASE_ACCESS_CLIENT_SECRET_ENV} (the Access
|
|
24
|
+
* service token), {@link APPS_DATABASE_PORT_ENV}. The company's Postgres server speaks no TLS of its own, so
|
|
25
|
+
* there is no CA to give: the encrypted leg is Hyperdrive to Cloudflare's edge and the tunnel.
|
|
26
|
+
*/
|
|
27
|
+
export const APPS_DB_ENTRY_KEY = 'apps-db';
|
|
28
|
+
export const APPS_DATABASE_HOST_ENV = 'APPS_DATABASE_HOST';
|
|
29
|
+
export const APPS_DATABASE_PORT_ENV = 'APPS_DATABASE_PORT';
|
|
30
|
+
export const APPS_DATABASE_ACCESS_CLIENT_ID_ENV = 'APPS_DATABASE_ACCESS_CLIENT_ID';
|
|
31
|
+
export const APPS_DATABASE_ACCESS_CLIENT_SECRET_ENV = 'APPS_DATABASE_ACCESS_CLIENT_SECRET';
|
|
32
|
+
export const APPS_DATABASE_ADMIN_URL_ENV = 'APPS_DATABASE_ADMIN_URL';
|
|
33
|
+
export const APPS_DATABASE_ROUTE_ENVS = [
|
|
34
|
+
APPS_DATABASE_HOST_ENV,
|
|
35
|
+
APPS_DATABASE_PORT_ENV,
|
|
36
|
+
APPS_DATABASE_ACCESS_CLIENT_ID_ENV,
|
|
37
|
+
APPS_DATABASE_ACCESS_CLIENT_SECRET_ENV,
|
|
38
|
+
];
|
|
39
|
+
/** The tunnel hostname last written to Boule, recorded last so a failed run is repeated. */
|
|
40
|
+
export const APPS_DB_ROUTE_APPLIED_SCOPE_KEY = 'apps_db_route_applied';
|
|
41
|
+
/** The tunnel hostname of the company's apps database route. */
|
|
42
|
+
export const appsDbHostname = (org) => hyperdriveHostname(org, APPS_DB_ENTRY_KEY, workerAppApex(org));
|
|
43
|
+
const routeKeys = remoteDatabaseKeys(APPS_DB_ENTRY_KEY);
|
|
44
|
+
/**
|
|
45
|
+
* Pure. `apply` once the company's apps are on and everything the route needs is recorded or planned;
|
|
46
|
+
* `retire` when they went off and the route is still recorded. Anything missing is blocked, not planned
|
|
47
|
+
* (platform-step rule 2); a service token minted and never held is blocked and never minted again (rule 1).
|
|
48
|
+
*/
|
|
49
|
+
export function planAppsDbRoute(org, recordedIds, plannedSteps, options) {
|
|
50
|
+
const key = `cloudflare:apps-db-route:${org.id}`;
|
|
51
|
+
const at = `${org.slug}'s apps database route`;
|
|
52
|
+
const blocked = [];
|
|
53
|
+
const block = (why) => {
|
|
54
|
+
blocked.push({ description: `${at} is blocked, not planned: ${why}`, key });
|
|
55
|
+
return { steps: [], blocked };
|
|
56
|
+
};
|
|
57
|
+
const draft = (desired, description) => ({
|
|
58
|
+
action: { system: 'cloudflare', kind: 'apps-db-route', key: org.id, desired },
|
|
59
|
+
step: { description, destroys: desired.mode === 'retire', migrates: false },
|
|
60
|
+
});
|
|
61
|
+
const recordedRoute = Boolean(recordedIds[routeKeys.route] ||
|
|
62
|
+
recordedIds[routeKeys.access] ||
|
|
63
|
+
recordedIds[routeKeys.token] ||
|
|
64
|
+
recordedIds[routeKeys.undelivered] ||
|
|
65
|
+
recordedIds[APPS_DB_ROUTE_APPLIED_SCOPE_KEY]);
|
|
66
|
+
if (!appsOn(org)) {
|
|
67
|
+
if (!recordedRoute)
|
|
68
|
+
return { steps: [], blocked };
|
|
69
|
+
if (!options.accessReady)
|
|
70
|
+
return block('the host supplies no Access, tunnel, DNS or zone port');
|
|
71
|
+
return {
|
|
72
|
+
steps: [
|
|
73
|
+
draft({ mode: 'retire' }, `remove ${org.slug}'s apps database route (the company's Boule runs no apps); the databases stay`),
|
|
74
|
+
],
|
|
75
|
+
blocked,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const planned = (system, kind) => plannedSteps.some((p) => p.action.system === system && p.action.kind === kind && p.action.key === org.id);
|
|
79
|
+
const hostname = appsDbHostname(org);
|
|
80
|
+
if (recordedIds[APPS_DB_ROUTE_APPLIED_SCOPE_KEY] === hostname)
|
|
81
|
+
return { steps: [], blocked };
|
|
82
|
+
if (!options.accessReady)
|
|
83
|
+
return block('the host supplies no Access, tunnel, DNS or zone port');
|
|
84
|
+
if (!options.canHoldSecrets)
|
|
85
|
+
return block('the host cannot hold secrets');
|
|
86
|
+
const lost = recordedIds[routeKeys.undelivered];
|
|
87
|
+
if (lost) {
|
|
88
|
+
return block(`service token ${lost} was created but its secret was never saved, so it cannot be used. To start over, turn apps off and apply (that removes the route and clears this note), delete the token ${lost} in Cloudflare Zero Trust if it is still listed, then turn apps on again`);
|
|
89
|
+
}
|
|
90
|
+
if (!recordedIds[ORG_POSTGRES_SERVER_SCOPE_KEY] && !planned('coolify', 'org-postgres')) {
|
|
91
|
+
return block('its Postgres server is not provisioned yet');
|
|
92
|
+
}
|
|
93
|
+
if (!options.tunnelReady)
|
|
94
|
+
return block("the company's Cloudflare tunnel is not applied yet");
|
|
95
|
+
if (!recordedIds[`coolify:boule:${org.id}`] && !planned('coolify', 'boule')) {
|
|
96
|
+
return block('Boule is not recorded yet');
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
steps: [
|
|
100
|
+
draft({ mode: 'apply', hostname }, `make ${org.slug}'s apps database route (${hostname}) and give Boule the host and its Access token`),
|
|
101
|
+
],
|
|
102
|
+
blocked,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Makes the route true, then gives Boule its env and restarts it. The applied marker is recorded last. */
|
|
106
|
+
export async function applyAppsDbRoute(deps, org, context) {
|
|
107
|
+
const at = `cloudflare:apps-db-route for ${org.slug}`;
|
|
108
|
+
const ms = deps.timeoutMs ?? WORKER_APP_CALL_TIMEOUT_MS;
|
|
109
|
+
const call = (label, task, over) => withCallTimeout(over ?? ms, `${at}: ${label}`, task);
|
|
110
|
+
if (!deps.heldKeys.put)
|
|
111
|
+
throw new Error(`${at}: needs a held-key store that can put`);
|
|
112
|
+
const put = deps.heldKeys.put;
|
|
113
|
+
const recorded = context.recordedIds();
|
|
114
|
+
if (!appsOn(org))
|
|
115
|
+
throw new Error(`${at}: the company's Boule runs no apps (org.boule.apps)`);
|
|
116
|
+
const serverId = recorded[ORG_POSTGRES_SERVER_SCOPE_KEY];
|
|
117
|
+
if (!serverId)
|
|
118
|
+
throw new Error(`${at}: ${org.slug}'s Postgres server is not recorded yet`);
|
|
119
|
+
const tunnelId = recorded[`cloudflare:tunnel:${org.id}`];
|
|
120
|
+
if (!tunnelId)
|
|
121
|
+
throw new Error(`${at}: ${org.slug}'s Cloudflare tunnel is not applied yet`);
|
|
122
|
+
if (!recorded[`coolify:boule:${org.id}`])
|
|
123
|
+
throw new Error(`${at}: Boule is not recorded yet`);
|
|
124
|
+
const upstreamHost = await call('read the database host', () => deps.upstreamHost(serverId));
|
|
125
|
+
if (!upstreamHost)
|
|
126
|
+
throw new Error(`${at}: the Postgres server reports no host`);
|
|
127
|
+
const hostname = appsDbHostname(org);
|
|
128
|
+
const access = await ensureServiceToken({ access: deps.database.access, heldKeys: deps.heldKeys, secret: deps.secret }, org, { key: APPS_DB_ENTRY_KEY }, `${org.slug}-apps`, at, call, context, put);
|
|
129
|
+
await ensureDatabaseRoute(deps.database, call, context, {
|
|
130
|
+
entryKey: APPS_DB_ENTRY_KEY,
|
|
131
|
+
accessName: `${org.slug}-apps-db-access`,
|
|
132
|
+
hostname,
|
|
133
|
+
upstreamHost,
|
|
134
|
+
markerDatabase: serverId,
|
|
135
|
+
tunnelId,
|
|
136
|
+
zoneApex: workerAppApex(org),
|
|
137
|
+
tokenId: access.id,
|
|
138
|
+
at,
|
|
139
|
+
});
|
|
140
|
+
deps.secret(access.clientSecret);
|
|
141
|
+
await call('write Boule env', () => deps.boule.upsertEnvs([
|
|
142
|
+
{ key: APPS_DATABASE_HOST_ENV, value: hostname, literal: true },
|
|
143
|
+
{ key: APPS_DATABASE_PORT_ENV, value: String(REMOTE_DATABASE_PORT), literal: true },
|
|
144
|
+
{ key: APPS_DATABASE_ACCESS_CLIENT_ID_ENV, value: access.clientId, literal: true },
|
|
145
|
+
{ key: APPS_DATABASE_ACCESS_CLIENT_SECRET_ENV, value: access.clientSecret, literal: true },
|
|
146
|
+
]));
|
|
147
|
+
await call('restart Boule', () => deps.boule.restart());
|
|
148
|
+
context.recordScopeId(APPS_DB_ROUTE_APPLIED_SCOPE_KEY, hostname);
|
|
149
|
+
await context.persistScopeIds();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Apps went off: Boule's env rows, then the DNS record, tunnel rule, Access app and service token (and the
|
|
153
|
+
* held token), then the applied marker. The databases and the role on the server are not touched. A run that
|
|
154
|
+
* fails halfway is repeated by the next apply (the ids are cleared last).
|
|
155
|
+
*/
|
|
156
|
+
export async function retireAppsDbRoute(deps, org, context) {
|
|
157
|
+
const at = `cloudflare:apps-db-route for ${org.slug}`;
|
|
158
|
+
const ms = deps.timeoutMs ?? WORKER_APP_CALL_TIMEOUT_MS;
|
|
159
|
+
const call = (label, task, over) => withCallTimeout(over ?? ms, `${at}: ${label}`, task);
|
|
160
|
+
await call('take the env rows out of Boule', () => deps.boule.deleteEnvs([...APPS_DATABASE_ROUTE_ENVS]));
|
|
161
|
+
await deleteWorkerAppDatabaseRoute(deps.database, deps.heldKeys.put ? { put: deps.heldKeys.put } : undefined, org.id, APPS_DB_ENTRY_KEY, context.recordedIds(), context, call);
|
|
162
|
+
context.recordScopeId(APPS_DB_ROUTE_APPLIED_SCOPE_KEY, '');
|
|
163
|
+
await context.persistScopeIds();
|
|
164
|
+
}
|
|
165
|
+
/** The held secret ref of the route's Access token (for tests and the executor). */
|
|
166
|
+
export const appsDbAccessRef = (org) => workerAppSecretRef(org.id, APPS_DB_ENTRY_KEY, 'HYPERDRIVE_ACCESS');
|
|
167
|
+
/** True when the apps admin role is recorded: the route is worth making only with it (used by the planner's caller). */
|
|
168
|
+
export const appsAdminRecorded = (recorded) => Boolean(recorded[ORG_APPS_ADMIN_SCOPE_KEY]);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { AppsDbRouteBoule } from './apps-db-route.js';
|
|
3
|
+
import type { DraftStep } from './diff.js';
|
|
4
|
+
import type { ZonePort } from './edge-zones.js';
|
|
5
|
+
import type { StepExecutionContext } from './plan-apply.js';
|
|
6
|
+
/**
|
|
7
|
+
* FZ: the address facts Boule's managed backend needs next to F3/F4's keys (`EDGE_URL`,
|
|
8
|
+
* `APPS_DATABASE_*`). With the company's Apps switch on, its Boule env gets:
|
|
9
|
+
*
|
|
10
|
+
* APPS_COMPANY_SLUG `org.slug`
|
|
11
|
+
* APPS_PLATFORM_DOMAIN the zone the live host `<slug>-<app>.<domain>` sits in: the company's own
|
|
12
|
+
* domain when the company hosts on it, `oikos.host` otherwise
|
|
13
|
+
* APPS_ZONE_ID the Cloudflare zone id of that zone (looked up through the zone port)
|
|
14
|
+
* APPS_WORKSPACE_DOMAIN the workspace domain, a platform constant (`APPS_DOMAIN`)
|
|
15
|
+
* APPS_WORKSPACE_ZONE_ID the zone id of the workspace domain (looked up the same way)
|
|
16
|
+
*
|
|
17
|
+
* Boule is restarted after the write. The marker below holds `<slug>|<live domain>` and is recorded last,
|
|
18
|
+
* so a changed slug or domain is delivered again and a failed run is repeated. Apps off: the five rows
|
|
19
|
+
* come out and the marker is cleared.
|
|
20
|
+
*/
|
|
21
|
+
export declare const APPS_COMPANY_SLUG_ENV = "APPS_COMPANY_SLUG";
|
|
22
|
+
export declare const APPS_PLATFORM_DOMAIN_ENV = "APPS_PLATFORM_DOMAIN";
|
|
23
|
+
export declare const APPS_ZONE_ID_ENV = "APPS_ZONE_ID";
|
|
24
|
+
export declare const APPS_WORKSPACE_DOMAIN_ENV = "APPS_WORKSPACE_DOMAIN";
|
|
25
|
+
export declare const APPS_WORKSPACE_ZONE_ID_ENV = "APPS_WORKSPACE_ZONE_ID";
|
|
26
|
+
export declare const APPS_ZONES_ENVS: readonly ["APPS_COMPANY_SLUG", "APPS_PLATFORM_DOMAIN", "APPS_ZONE_ID", "APPS_WORKSPACE_DOMAIN", "APPS_WORKSPACE_ZONE_ID"];
|
|
27
|
+
/** What was last written to Boule, recorded last. */
|
|
28
|
+
export declare const APPS_ZONES_APPLIED_SCOPE_KEY = "apps_zones_applied";
|
|
29
|
+
/** The value the marker holds for an org as declared now. */
|
|
30
|
+
export declare const appsZonesMarker: (org: DeclaredOrg) => string;
|
|
31
|
+
export interface PlanAppsZonesOptions {
|
|
32
|
+
/** The zone port is available (edge). */
|
|
33
|
+
zonesReady: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pure. `apply` with Apps on once the zone port exists and Boule is recorded or planned, whenever what
|
|
37
|
+
* Boule holds is not the declared slug and domains; `retire` when Apps went off and a marker is recorded.
|
|
38
|
+
* Anything missing is blocked in plain words, not planned.
|
|
39
|
+
*/
|
|
40
|
+
export declare function planAppsZones(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], options: PlanAppsZonesOptions): {
|
|
41
|
+
steps: DraftStep[];
|
|
42
|
+
blocked: Array<{
|
|
43
|
+
description: string;
|
|
44
|
+
key: string;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
export interface AppsZonesDeps {
|
|
48
|
+
zones: Pick<ZonePort, 'findZoneByApex'>;
|
|
49
|
+
boule: AppsDbRouteBoule;
|
|
50
|
+
/** Test hook: the timeout of a call, in ms. */
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Looks both zones up, writes the five rows, restarts Boule, then records the marker. */
|
|
54
|
+
export declare function applyAppsZones(deps: AppsZonesDeps, org: DeclaredOrg, context: StepExecutionContext): Promise<void>;
|
|
55
|
+
/** Apps went off: the rows out of Boule, Boule restarted, then the marker cleared. */
|
|
56
|
+
export declare function retireAppsZones(deps: Pick<AppsZonesDeps, 'boule' | 'timeoutMs'>, org: DeclaredOrg, context: StepExecutionContext): Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { APPS_DOMAIN, appsOn } from '../org.js';
|
|
2
|
+
import { withCallTimeout } from '../worker-app-upload.js';
|
|
3
|
+
import { WORKER_APP_CALL_TIMEOUT_MS, workerAppApex } from './worker-app.js';
|
|
4
|
+
/**
|
|
5
|
+
* FZ: the address facts Boule's managed backend needs next to F3/F4's keys (`EDGE_URL`,
|
|
6
|
+
* `APPS_DATABASE_*`). With the company's Apps switch on, its Boule env gets:
|
|
7
|
+
*
|
|
8
|
+
* APPS_COMPANY_SLUG `org.slug`
|
|
9
|
+
* APPS_PLATFORM_DOMAIN the zone the live host `<slug>-<app>.<domain>` sits in: the company's own
|
|
10
|
+
* domain when the company hosts on it, `oikos.host` otherwise
|
|
11
|
+
* APPS_ZONE_ID the Cloudflare zone id of that zone (looked up through the zone port)
|
|
12
|
+
* APPS_WORKSPACE_DOMAIN the workspace domain, a platform constant (`APPS_DOMAIN`)
|
|
13
|
+
* APPS_WORKSPACE_ZONE_ID the zone id of the workspace domain (looked up the same way)
|
|
14
|
+
*
|
|
15
|
+
* Boule is restarted after the write. The marker below holds `<slug>|<live domain>` and is recorded last,
|
|
16
|
+
* so a changed slug or domain is delivered again and a failed run is repeated. Apps off: the five rows
|
|
17
|
+
* come out and the marker is cleared.
|
|
18
|
+
*/
|
|
19
|
+
export const APPS_COMPANY_SLUG_ENV = 'APPS_COMPANY_SLUG';
|
|
20
|
+
export const APPS_PLATFORM_DOMAIN_ENV = 'APPS_PLATFORM_DOMAIN';
|
|
21
|
+
export const APPS_ZONE_ID_ENV = 'APPS_ZONE_ID';
|
|
22
|
+
export const APPS_WORKSPACE_DOMAIN_ENV = 'APPS_WORKSPACE_DOMAIN';
|
|
23
|
+
export const APPS_WORKSPACE_ZONE_ID_ENV = 'APPS_WORKSPACE_ZONE_ID';
|
|
24
|
+
export const APPS_ZONES_ENVS = [
|
|
25
|
+
APPS_COMPANY_SLUG_ENV,
|
|
26
|
+
APPS_PLATFORM_DOMAIN_ENV,
|
|
27
|
+
APPS_ZONE_ID_ENV,
|
|
28
|
+
APPS_WORKSPACE_DOMAIN_ENV,
|
|
29
|
+
APPS_WORKSPACE_ZONE_ID_ENV,
|
|
30
|
+
];
|
|
31
|
+
/** What was last written to Boule, recorded last. */
|
|
32
|
+
export const APPS_ZONES_APPLIED_SCOPE_KEY = 'apps_zones_applied';
|
|
33
|
+
/** The value the marker holds for an org as declared now. */
|
|
34
|
+
export const appsZonesMarker = (org) => `${org.slug}|${workerAppApex(org)}|${APPS_DOMAIN}`;
|
|
35
|
+
/**
|
|
36
|
+
* Pure. `apply` with Apps on once the zone port exists and Boule is recorded or planned, whenever what
|
|
37
|
+
* Boule holds is not the declared slug and domains; `retire` when Apps went off and a marker is recorded.
|
|
38
|
+
* Anything missing is blocked in plain words, not planned.
|
|
39
|
+
*/
|
|
40
|
+
export function planAppsZones(org, recordedIds, plannedSteps, options) {
|
|
41
|
+
const key = `cloudflare:apps-zones:${org.id}`;
|
|
42
|
+
const blocked = [];
|
|
43
|
+
const draft = (desired, description) => ({
|
|
44
|
+
action: { system: 'cloudflare', kind: 'apps-zones', key: org.id, desired },
|
|
45
|
+
step: { description, destroys: desired.mode === 'retire', migrates: false },
|
|
46
|
+
});
|
|
47
|
+
const applied = recordedIds[APPS_ZONES_APPLIED_SCOPE_KEY];
|
|
48
|
+
if (!appsOn(org)) {
|
|
49
|
+
if (!applied)
|
|
50
|
+
return { steps: [], blocked };
|
|
51
|
+
return {
|
|
52
|
+
steps: [
|
|
53
|
+
draft({ mode: 'retire' }, `take ${org.slug}'s slug and zone ids out of Boule's env (the company's Boule runs no apps)`),
|
|
54
|
+
],
|
|
55
|
+
blocked,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (applied === appsZonesMarker(org))
|
|
59
|
+
return { steps: [], blocked };
|
|
60
|
+
const block = (why) => {
|
|
61
|
+
blocked.push({
|
|
62
|
+
description: `${org.slug}'s slug and zone ids for Boule are blocked, not planned: ${why}`,
|
|
63
|
+
key,
|
|
64
|
+
});
|
|
65
|
+
return { steps: [], blocked };
|
|
66
|
+
};
|
|
67
|
+
if (!options.zonesReady)
|
|
68
|
+
return block('the host supplies no zone port');
|
|
69
|
+
const boulePlanned = plannedSteps.some((p) => p.action.system === 'coolify' && p.action.kind === 'boule' && p.action.key === org.id);
|
|
70
|
+
if (!recordedIds[`coolify:boule:${org.id}`] && !boulePlanned) {
|
|
71
|
+
return block('Boule is not recorded yet');
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
steps: [
|
|
75
|
+
draft({ mode: 'apply', slug: org.slug, domain: workerAppApex(org), workspaceDomain: APPS_DOMAIN }, `give ${org.slug}'s Boule its slug and the zone ids of ${workerAppApex(org)} and ${APPS_DOMAIN}`),
|
|
76
|
+
],
|
|
77
|
+
blocked,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Looks both zones up, writes the five rows, restarts Boule, then records the marker. */
|
|
81
|
+
export async function applyAppsZones(deps, org, context) {
|
|
82
|
+
const at = `cloudflare:apps-zones for ${org.slug}`;
|
|
83
|
+
const ms = deps.timeoutMs ?? WORKER_APP_CALL_TIMEOUT_MS;
|
|
84
|
+
const call = (label, task) => withCallTimeout(ms, `${at}: ${label}`, task);
|
|
85
|
+
if (!appsOn(org))
|
|
86
|
+
throw new Error(`${at}: the company's Boule runs no apps (org.boule.apps)`);
|
|
87
|
+
if (!context.recordedIds()[`coolify:boule:${org.id}`]) {
|
|
88
|
+
throw new Error(`${at}: Boule is not recorded yet`);
|
|
89
|
+
}
|
|
90
|
+
const domain = workerAppApex(org);
|
|
91
|
+
const live = await call(`find the zone of ${domain}`, () => deps.zones.findZoneByApex(domain));
|
|
92
|
+
if (!live?.id) {
|
|
93
|
+
throw new Error(`${at} is blocked: no Cloudflare zone for ${domain} was found, so Boule cannot be told its zone id; make the zone, then apply again`);
|
|
94
|
+
}
|
|
95
|
+
const workspace = await call(`find the zone of ${APPS_DOMAIN}`, () => deps.zones.findZoneByApex(APPS_DOMAIN));
|
|
96
|
+
if (!workspace?.id) {
|
|
97
|
+
throw new Error(`${at} is blocked: no Cloudflare zone for the workspace domain ${APPS_DOMAIN} was found, so Boule cannot be told its zone id; the platform needs that zone before workspace copies can be served`);
|
|
98
|
+
}
|
|
99
|
+
await call('write Boule env', () => deps.boule.upsertEnvs([
|
|
100
|
+
{ key: APPS_COMPANY_SLUG_ENV, value: org.slug, literal: true },
|
|
101
|
+
{ key: APPS_PLATFORM_DOMAIN_ENV, value: domain, literal: true },
|
|
102
|
+
{ key: APPS_ZONE_ID_ENV, value: live.id, literal: true },
|
|
103
|
+
{ key: APPS_WORKSPACE_DOMAIN_ENV, value: APPS_DOMAIN, literal: true },
|
|
104
|
+
{ key: APPS_WORKSPACE_ZONE_ID_ENV, value: workspace.id, literal: true },
|
|
105
|
+
]));
|
|
106
|
+
await call('restart Boule', () => deps.boule.restart());
|
|
107
|
+
context.recordScopeId(APPS_ZONES_APPLIED_SCOPE_KEY, appsZonesMarker(org));
|
|
108
|
+
await context.persistScopeIds();
|
|
109
|
+
}
|
|
110
|
+
/** Apps went off: the rows out of Boule, Boule restarted, then the marker cleared. */
|
|
111
|
+
export async function retireAppsZones(deps, org, context) {
|
|
112
|
+
const at = `cloudflare:apps-zones for ${org.slug}`;
|
|
113
|
+
const ms = deps.timeoutMs ?? WORKER_APP_CALL_TIMEOUT_MS;
|
|
114
|
+
await withCallTimeout(ms, `${at}: take the env rows out of Boule`, () => deps.boule.deleteEnvs([...APPS_ZONES_ENVS]));
|
|
115
|
+
await withCallTimeout(ms, `${at}: restart Boule`, () => deps.boule.restart());
|
|
116
|
+
context.recordScopeId(APPS_ZONES_APPLIED_SCOPE_KEY, '');
|
|
117
|
+
await context.persistScopeIds();
|
|
118
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { sharedSecretRef } from '../org.js';
|
|
3
3
|
import { applyBlueGreenServer } from './blue-green-apply.js';
|
|
4
|
-
import { ARCHON_BG_ORIGIN_SCOPE_KEY, DEFAULT_WEIGHTS, PRIMARY_SERVER_SCOPE_KEY, actionsKeyName, bgKey, healthTokenName, lbOrigin, mintActionsKey, splitServers, } from './blue-green.js';
|
|
4
|
+
import { ARCHON_BG_ORIGIN_SCOPE_KEY, DEFAULT_WEIGHTS, PRIMARY_SERVER_SCOPE_KEY, actionsKeyName, bgKey, healthTokenName, lbOrigin, mintActionsKey, mintPinKey, pinKeyName, splitServers, } from './blue-green.js';
|
|
5
5
|
/**
|
|
6
6
|
* platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
|
|
7
7
|
* blue; green, the balancer and stage are new apps beside it on the primary server. The tunnel moves to the
|
|
@@ -60,6 +60,16 @@ export async function applyArchonBlueGreen(run) {
|
|
|
60
60
|
isRunning: run.isRunning,
|
|
61
61
|
adoptRunningBlue: true,
|
|
62
62
|
stageSink: { slug: 'platform' },
|
|
63
|
+
pinKey: () => mintPinKey({
|
|
64
|
+
id,
|
|
65
|
+
ref: sharedSecretRef(run.ownerOrgId, pinKeyName(id)),
|
|
66
|
+
recorded: run.recorded,
|
|
67
|
+
record: run.record,
|
|
68
|
+
persist: run.persist,
|
|
69
|
+
heldKeys: run.heldKeys,
|
|
70
|
+
generate: () => randomBytes(32).toString('hex'),
|
|
71
|
+
secret: run.secret,
|
|
72
|
+
}),
|
|
63
73
|
actionsKey: async () => {
|
|
64
74
|
const key = await mintActionsKey({
|
|
65
75
|
id,
|
|
@@ -3,9 +3,10 @@ import { ensureConnectorsOnServers } from './cloudflared-connector.js';
|
|
|
3
3
|
import { type DeployHealthRuntime } from './deploy-health.js';
|
|
4
4
|
import type { TunnelPort } from './edge-tunnels.js';
|
|
5
5
|
import type { CoolifyPort } from './executor.js';
|
|
6
|
+
import { type StageSignIn } from './stage-env.js';
|
|
6
7
|
import { type StageGuardDeps } from './stage-guards.js';
|
|
7
8
|
/** The Coolify calls this module makes. */
|
|
8
|
-
type Coolify = Pick<CoolifyPort, 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'deployAndWait' | 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'updateFileStorage' | 'requireGithubApp' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw'> & {
|
|
9
|
+
type Coolify = Pick<CoolifyPort, 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'listEnvs' | 'deleteEnv' | 'deploy' | 'deployAndWait' | 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'updateFileStorage' | 'requireGithubApp' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw'> & {
|
|
9
10
|
listApplicationDeployments?: CoolifyPort['listApplicationDeployments'];
|
|
10
11
|
/** Needed by the stage mail sink; a host without it gets no sink, so no stage mail env. */
|
|
11
12
|
createDockerImageApplication?: CoolifyPort['createDockerImageApplication'];
|
|
@@ -24,8 +25,12 @@ export interface BlueGreenApplyDeps {
|
|
|
24
25
|
};
|
|
25
26
|
/** The entry's one health token: held once, handed to the env writes below and nowhere else. */
|
|
26
27
|
healthToken: () => Promise<string>;
|
|
28
|
+
/** The balancer's pin key: held, minted once (`mintPinKey`). It goes into the balancer's config file and nowhere else. */
|
|
29
|
+
pinKey: () => Promise<string>;
|
|
27
30
|
/** Registers a value for redaction before it is written anywhere. */
|
|
28
31
|
secret: (value: string) => void;
|
|
32
|
+
/** The Coolify GitHub source uuid a new app of `repo` is created through. Absent: the platform's (`requireGithubApp`). */
|
|
33
|
+
githubAppUuid?: (repo: string) => Promise<string>;
|
|
29
34
|
runtime: DeployHealthRuntime;
|
|
30
35
|
sleep: (ms: number) => Promise<void>;
|
|
31
36
|
/** The step's name, for errors. */
|
|
@@ -57,8 +62,22 @@ export interface BlueGreenApplyDeps {
|
|
|
57
62
|
slug: string;
|
|
58
63
|
};
|
|
59
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* What the stage code (the git app, the env, the deploy, the sink, `ensureStage`) needs. A blue/green apply and a
|
|
67
|
+
* workspace apply both have it. `primaryBlueAppId` is the live app whose env stage's names are checked against;
|
|
68
|
+
* `null` for a workspace with no live app yet, which has no production to be isolated from.
|
|
69
|
+
*/
|
|
70
|
+
export type StageDeps = Pick<BlueGreenApplyDeps, 'coolify' | 'recorded' | 'record' | 'persist' | 'projectUuid' | 'environment' | 'secret' | 'githubAppUuid' | 'runtime' | 'sleep' | 'label' | 'logDir' | 'isRunning' | 'stageGuards' | 'stageSink' | 'actionsKey'> & {
|
|
71
|
+
primaryBlueAppId: string | null;
|
|
72
|
+
/** The stage's own sign-in (a workspace of an entry with `uses.auth`). Absent: stage holds no sign-in env of its own. */
|
|
73
|
+
signIn?: StageSignIn;
|
|
74
|
+
/** Where the stage app says it is served (`APP_URL`). Absent: its address on the company network. */
|
|
75
|
+
stageAppUrl?: string;
|
|
76
|
+
};
|
|
60
77
|
/** What writing a balancer's config needs; {@link BlueGreenApplyDeps} and the release runner both have it. */
|
|
61
78
|
export interface LbConfigDeps {
|
|
79
|
+
/** The balancer's pin key: held, minted once, never recorded as a value (`mintPinKey`). */
|
|
80
|
+
pinKey: () => Promise<string>;
|
|
62
81
|
coolify: Pick<Coolify, 'listStorages' | 'createFileStorage' | 'updateFileStorage'>;
|
|
63
82
|
recorded: () => Readonly<Record<string, string>>;
|
|
64
83
|
record: (key: string, value: string) => void;
|
|
@@ -80,6 +99,14 @@ export declare function applyBlueGreenServer(deps: BlueGreenApplyDeps, subject:
|
|
|
80
99
|
uuid: string;
|
|
81
100
|
primaryUuid: string;
|
|
82
101
|
}): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Applies a server workspace on the primary: the stage mail sink, then the stage app with its own empty
|
|
104
|
+
* database, the stage guards and the gate, exactly as a blue/green stage, and no colors. The commit the stage
|
|
105
|
+
* deployed is recorded as `ws.<id>.commit` for the live app to pin to. A failure halfway leaves the sink, app
|
|
106
|
+
* and database already made, recorded by id; the next run finds them and does the rest. Stage fails closed for
|
|
107
|
+
* itself: a refused guard records `bg.<id>.stagestate` and writes nothing to the stage app.
|
|
108
|
+
*/
|
|
109
|
+
export declare function applyWorkspaceServer(deps: StageDeps, subject: BlueGreenSubject, serverUuid: string): Promise<void>;
|
|
83
110
|
/** Writes the balancer's config with the recorded weights and records them for this server. */
|
|
84
111
|
export declare function applyBlueGreenWeights(deps: LbConfigDeps, subject: BlueGreenSubject, server: {
|
|
85
112
|
uuid: string;
|
|
@@ -104,6 +131,15 @@ export declare function copiedEnv(deps: {
|
|
|
104
131
|
key: string;
|
|
105
132
|
value: string;
|
|
106
133
|
}>;
|
|
134
|
+
/**
|
|
135
|
+
* One Mailpit app per company beside stage, shared by every stage of the company. It is checked against the live
|
|
136
|
+
* app on every run: one that is stopped or gone is deployed again. `stage_sink_applied` is recorded only after
|
|
137
|
+
* the sink deploys; a failure clears it, logs fixed text and the error name, and returns false. Production's
|
|
138
|
+
* structure goes on (stage then holds no mail env and the copy stays refused).
|
|
139
|
+
*/
|
|
140
|
+
export declare function ensureStageSink(deps: StageDeps, subject: BlueGreenSubject, serverUuid: string): Promise<boolean>;
|
|
141
|
+
/** The stage app and its own empty database, on the primary. Its env comes from the allowlist, never from a copy of the live app's. */
|
|
142
|
+
export declare function ensureStage(deps: StageDeps, subject: BlueGreenSubject, serverUuid: string): Promise<void>;
|
|
107
143
|
/** A connector for the org's tunnel on each given server; the primary's is the existing one. */
|
|
108
144
|
export declare function applyBlueGreenConnectors(input: {
|
|
109
145
|
coolify: Parameters<typeof ensureConnectorsOnServers>[0]['coolify'];
|