@wtfalch/foundry 0.34.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/bootstrap.d.ts +1 -1
- package/dist/bootstrap/bootstrap.js +49 -11
- package/dist/bootstrap/declare.d.ts +12 -5
- package/dist/bootstrap/declare.js +33 -18
- package/dist/bootstrap/versions.d.ts +20 -0
- package/dist/bootstrap/versions.js +46 -0
- package/dist/calls-sfu.js +3 -3
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/migrations/0053_heartbeats_provider.sql +9 -0
- package/dist/migrations/0053_org_version_pin.sql +6 -0
- package/dist/org.d.ts +60 -14
- package/dist/org.js +118 -28
- package/dist/plan/backup-gate.d.ts +93 -0
- package/dist/plan/backup-gate.js +261 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +20 -3
- package/dist/plan/build-record.d.ts +56 -0
- package/dist/plan/build-record.js +109 -0
- package/dist/plan/calls-matrix.js +3 -3
- package/dist/plan/cloudflared-connector.js +2 -2
- package/dist/plan/company-backups.d.ts +31 -0
- package/dist/plan/company-backups.js +101 -0
- package/dist/plan/deploy-health.d.ts +12 -2
- package/dist/plan/deploy-health.js +3 -1
- package/dist/plan/destinations.d.ts +73 -0
- package/dist/plan/destinations.js +217 -0
- package/dist/plan/diff.d.ts +17 -8
- package/dist/plan/diff.js +131 -131
- package/dist/plan/edge-access.d.ts +15 -0
- package/dist/plan/edge-access.js +21 -2
- package/dist/plan/executor.d.ts +166 -7
- package/dist/plan/executor.js +547 -106
- package/dist/plan/health-targets.d.ts +1 -3
- package/dist/plan/health-targets.js +36 -36
- package/dist/plan/held-key-refs.d.ts +5 -0
- package/dist/plan/held-key-refs.js +5 -0
- package/dist/plan/hostnames.d.ts +10 -0
- package/dist/plan/hostnames.js +14 -0
- package/dist/plan/index.d.ts +5 -0
- package/dist/plan/index.js +3 -0
- package/dist/plan/live.d.ts +6 -0
- package/dist/plan/live.js +3 -2
- package/dist/plan/locked-hostnames.d.ts +2 -1
- package/dist/plan/locked-hostnames.js +16 -3
- package/dist/plan/login-host.d.ts +9 -20
- package/dist/plan/login-host.js +10 -25
- package/dist/plan/mail-relay.d.ts +48 -29
- package/dist/plan/mail-relay.js +145 -68
- package/dist/plan/network-move.d.ts +20 -0
- package/dist/plan/network-move.js +41 -0
- package/dist/plan/oikos-ingress.d.ts +111 -0
- package/dist/plan/oikos-ingress.js +231 -0
- package/dist/plan/org-names.d.ts +8 -2
- package/dist/plan/org-names.js +57 -1
- package/dist/plan/own-diff.d.ts +9 -8
- package/dist/plan/own-diff.js +42 -34
- package/dist/plan/own-hostnames.d.ts +27 -0
- package/dist/plan/own-hostnames.js +24 -0
- package/dist/plan/own-limits.d.ts +198 -7
- package/dist/plan/own-limits.js +163 -32
- package/dist/plan/own-services.js +3 -1
- package/dist/plan/own-sfu.js +3 -2
- package/dist/plan/plan-apply.d.ts +47 -7
- package/dist/plan/plan-apply.js +184 -38
- package/dist/plan/platform-diff.d.ts +133 -17
- package/dist/plan/platform-diff.js +231 -68
- package/dist/plan/platform-org.d.ts +79 -10
- package/dist/plan/platform-org.js +93 -13
- package/dist/plan/platform-providers.d.ts +118 -73
- package/dist/plan/platform-providers.js +770 -231
- package/dist/plan/proxy-labels.d.ts +57 -11
- package/dist/plan/proxy-labels.js +94 -14
- package/dist/plan/run-options.d.ts +19 -0
- package/dist/plan/run-options.js +46 -0
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +76 -8
- package/dist/platform.d.ts +32 -0
- package/dist/platform.js +16 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +3 -1
- package/dist/slug.d.ts +3 -1
- package/dist/slug.js +25 -3
- package/dist/store/postgres.js +43 -1
- package/dist/store/schema.d.ts +36 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -3
- package/versions.digests.json +11 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { OWN_LIMITS_TIMEOUT_MS } from './own-limits.js';
|
|
2
|
+
/** The Docker network of the server's default destination: everything on the platform side. */
|
|
3
|
+
export const PLATFORM_NETWORK = 'coolify';
|
|
4
|
+
/**
|
|
5
|
+
* Whether a customer company gets its own `co-<slug>` network. Off for go-live: every company shares the
|
|
6
|
+
* platform network, because Archon (on it) calls each company's files, mail admin and jobs at internal
|
|
7
|
+
* addresses and Coolify cannot put Archon on a second network. Per-company networks return after go-live,
|
|
8
|
+
* once a design lists every call that crosses the split. `let` so tests can turn it on.
|
|
9
|
+
*/
|
|
10
|
+
export let COMPANY_NETWORKS = false;
|
|
11
|
+
/** Test-only: turns the per-company networks on or off. */
|
|
12
|
+
export function setCompanyNetworksForTests(on) {
|
|
13
|
+
COMPANY_NETWORKS = on;
|
|
14
|
+
}
|
|
15
|
+
/** A company runs on the platform network: the owner always, a customer while {@link COMPANY_NETWORKS} is off. */
|
|
16
|
+
export function onPlatformNetwork(ownerCompany) {
|
|
17
|
+
return ownerCompany === true || !COMPANY_NETWORKS;
|
|
18
|
+
}
|
|
19
|
+
/** Coolify's destination name for a company, so a listing says who owns it. */
|
|
20
|
+
export const COMPANY_DESTINATION_NAME_PREFIX = 'foundry:';
|
|
21
|
+
/** A Docker network name is at most 64 characters; keep room under it. */
|
|
22
|
+
const MAX_NETWORK_LENGTH = 60;
|
|
23
|
+
const NETWORK_NAME = /^[a-z0-9][a-z0-9_.-]*$/;
|
|
24
|
+
/** Docker's default address pools give about 30 user networks; stop a little short. */
|
|
25
|
+
export const DEFAULT_MAX_DESTINATIONS = 25;
|
|
26
|
+
/** The company's own Docker network: `co-<slug>`. */
|
|
27
|
+
export function companyNetworkName(slug) {
|
|
28
|
+
const network = `co-${slug}`;
|
|
29
|
+
return validateNetworkName(network);
|
|
30
|
+
}
|
|
31
|
+
/** Refuses a name Docker or this module cannot take; the message names no value. */
|
|
32
|
+
export function validateNetworkName(network) {
|
|
33
|
+
if (typeof network !== 'string' || network.length === 0)
|
|
34
|
+
throw new Error('destination: the network name is empty');
|
|
35
|
+
if (network.length > MAX_NETWORK_LENGTH)
|
|
36
|
+
throw new Error(`destination: the network name is over ${MAX_NETWORK_LENGTH} characters`);
|
|
37
|
+
if (!NETWORK_NAME.test(network))
|
|
38
|
+
throw new Error('destination: the network name has a character outside a-z, 0-9, _, . and -');
|
|
39
|
+
return network;
|
|
40
|
+
}
|
|
41
|
+
/** A server already holds as many networks as the capacity gate allows. */
|
|
42
|
+
export class DestinationCapacityError extends Error {
|
|
43
|
+
limit;
|
|
44
|
+
constructor(limit) {
|
|
45
|
+
super(`destination: the server already has ${limit} destinations; raise Docker's address pools`);
|
|
46
|
+
this.limit = limit;
|
|
47
|
+
this.name = 'DestinationCapacityError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function readList(body) {
|
|
51
|
+
if (!Array.isArray(body))
|
|
52
|
+
throw new Error('destination: the destination list is not an array');
|
|
53
|
+
const out = [];
|
|
54
|
+
for (const row of body) {
|
|
55
|
+
const r = row;
|
|
56
|
+
if (r && typeof r.uuid === 'string' && typeof r.network === 'string')
|
|
57
|
+
out.push({ uuid: r.uuid, network: r.network });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
async function bounded(work, what) {
|
|
62
|
+
let timer;
|
|
63
|
+
try {
|
|
64
|
+
return await Promise.race([
|
|
65
|
+
work,
|
|
66
|
+
new Promise((_, reject) => {
|
|
67
|
+
timer = setTimeout(() => reject(new Error(`destination: ${what} timed out`)), OWN_LIMITS_TIMEOUT_MS);
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** The uuid of the server's destination on `network`, or undefined. Creates nothing. */
|
|
76
|
+
export async function findDestination(coolify, serverUuid, network) {
|
|
77
|
+
const path = `/servers/${encodeURIComponent(serverUuid)}/destinations`;
|
|
78
|
+
const body = await bounded(coolify.raw('GET', path), 'listing the destinations');
|
|
79
|
+
// A listing that is not an array names no destination: the create goes out as it was.
|
|
80
|
+
if (!Array.isArray(body))
|
|
81
|
+
return undefined;
|
|
82
|
+
return readList(body).find((r) => r.network === network)?.uuid;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The uuid of the server's destination on `network`; creates it when absent.
|
|
86
|
+
* Coolify makes the Docker network and attaches its proxy to it when the
|
|
87
|
+
* destination is created. Each call has a 60 second timeout. A create that
|
|
88
|
+
* fails (Coolify answers 409 when the network already exists) is followed by
|
|
89
|
+
* one more listing; found there, that uuid is used, so two runs at once and a
|
|
90
|
+
* re-run after a crash converge on one destination. `maxDestinations` stops a
|
|
91
|
+
* create on a server that already holds that many (`DestinationCapacityError`,
|
|
92
|
+
* before anything is created).
|
|
93
|
+
*/
|
|
94
|
+
export async function ensureDestination(coolify, serverUuid, network, options = {}) {
|
|
95
|
+
validateNetworkName(network);
|
|
96
|
+
const path = `/servers/${encodeURIComponent(serverUuid)}/destinations`;
|
|
97
|
+
const list = async () => readList(await bounded(coolify.raw('GET', path), 'listing the destinations'));
|
|
98
|
+
const found = (rows) => rows.find((r) => r.network === network)?.uuid;
|
|
99
|
+
const rows = await list();
|
|
100
|
+
const have = found(rows);
|
|
101
|
+
if (have)
|
|
102
|
+
return have;
|
|
103
|
+
const limit = options.maxDestinations ?? DEFAULT_MAX_DESTINATIONS;
|
|
104
|
+
if (rows.length >= limit)
|
|
105
|
+
throw new DestinationCapacityError(limit);
|
|
106
|
+
try {
|
|
107
|
+
const created = (await bounded(coolify.raw('POST', path, {
|
|
108
|
+
network,
|
|
109
|
+
name: options.name ?? `${COMPANY_DESTINATION_NAME_PREFIX}${network}`,
|
|
110
|
+
}), 'creating the destination'));
|
|
111
|
+
if (created && typeof created.uuid === 'string')
|
|
112
|
+
return created.uuid;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Fall through to one more listing: a 409 means someone else made it.
|
|
116
|
+
const again = found(await list());
|
|
117
|
+
if (again)
|
|
118
|
+
return again;
|
|
119
|
+
throw new Error('destination: creating the destination failed');
|
|
120
|
+
}
|
|
121
|
+
const after = found(await list());
|
|
122
|
+
if (after)
|
|
123
|
+
return after;
|
|
124
|
+
throw new Error('destination: the created destination is not in the listing');
|
|
125
|
+
}
|
|
126
|
+
const CREATES = [
|
|
127
|
+
'createGithubAppApplication',
|
|
128
|
+
'createDockerImageApplication',
|
|
129
|
+
'createDockerfileApplication',
|
|
130
|
+
'createPostgresDatabase',
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* The Coolify port with `destination_uuid` added to the four create calls
|
|
134
|
+
* (the other calls pass through). `resolve` gives the destination of the
|
|
135
|
+
* input's `server_uuid`; it runs once per server for this port. A create that
|
|
136
|
+
* already names a destination keeps it. A `resolve` that gives nothing leaves
|
|
137
|
+
* the create as it was. Composes with `limited`: `limited(placed(port, ...))`.
|
|
138
|
+
*/
|
|
139
|
+
export function placed(coolify, resolve) {
|
|
140
|
+
const cache = new Map();
|
|
141
|
+
const destinationOf = (serverUuid) => {
|
|
142
|
+
let hit = cache.get(serverUuid);
|
|
143
|
+
if (!hit) {
|
|
144
|
+
hit = resolve(serverUuid);
|
|
145
|
+
cache.set(serverUuid, hit);
|
|
146
|
+
hit.catch(() => cache.delete(serverUuid));
|
|
147
|
+
}
|
|
148
|
+
return hit;
|
|
149
|
+
};
|
|
150
|
+
const over = {};
|
|
151
|
+
for (const method of CREATES) {
|
|
152
|
+
const create = coolify[method];
|
|
153
|
+
if (!create)
|
|
154
|
+
continue;
|
|
155
|
+
over[method] = async (input) => {
|
|
156
|
+
if (input.destination_uuid)
|
|
157
|
+
return create.call(coolify, input);
|
|
158
|
+
const destination = await destinationOf(input.server_uuid);
|
|
159
|
+
return create.call(coolify, (destination ? { ...input, destination_uuid: destination } : input));
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return new Proxy(coolify, {
|
|
163
|
+
get(target, prop) {
|
|
164
|
+
if (typeof prop === 'string' && prop in over)
|
|
165
|
+
return over[prop];
|
|
166
|
+
const value = Reflect.get(target, prop, target);
|
|
167
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* `placed` on the platform's destination (Docker network `coolify`) of each
|
|
173
|
+
* server. Once a server has a company destination, a create without
|
|
174
|
+
* `destination_uuid` is refused by Coolify, so every platform create names the
|
|
175
|
+
* default one. A server whose listing has no `coolify` destination, and a port
|
|
176
|
+
* with no `raw`, get the create unchanged.
|
|
177
|
+
*/
|
|
178
|
+
export function placedOnPlatform(coolify) {
|
|
179
|
+
const { raw } = coolify;
|
|
180
|
+
if (typeof raw !== 'function')
|
|
181
|
+
return coolify;
|
|
182
|
+
return placed(coolify, (serverUuid) => findDestination({ raw: raw.bind(coolify) }, serverUuid, PLATFORM_NETWORK));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `placed`, with the destination chosen by the step now running: `scope` gives a key (the company) and
|
|
186
|
+
* the resolver for it, or nothing, and a create then goes out as it was. One `placed` wrapper is made
|
|
187
|
+
* per key, so its answers are never shared between companies.
|
|
188
|
+
*/
|
|
189
|
+
export function placedByScope(coolify, scope) {
|
|
190
|
+
const perKey = new Map();
|
|
191
|
+
const over = {};
|
|
192
|
+
for (const method of CREATES) {
|
|
193
|
+
const create = coolify[method];
|
|
194
|
+
if (!create)
|
|
195
|
+
continue;
|
|
196
|
+
over[method] = async (input) => {
|
|
197
|
+
const current = scope();
|
|
198
|
+
if (!current)
|
|
199
|
+
return create.call(coolify, input);
|
|
200
|
+
let wrapper = perKey.get(current.key);
|
|
201
|
+
if (!wrapper) {
|
|
202
|
+
wrapper = placed(coolify, current.resolve);
|
|
203
|
+
perKey.set(current.key, wrapper);
|
|
204
|
+
}
|
|
205
|
+
const made = wrapper[method];
|
|
206
|
+
return made.call(wrapper, input);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return new Proxy(coolify, {
|
|
210
|
+
get(target, prop) {
|
|
211
|
+
if (typeof prop === 'string' && prop in over)
|
|
212
|
+
return over[prop];
|
|
213
|
+
const value = Reflect.get(target, prop, target);
|
|
214
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
package/dist/plan/diff.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface DraftStep {
|
|
|
41
41
|
* New ones go here, not in new positional arguments (rule 7).
|
|
42
42
|
*/
|
|
43
43
|
export interface PlanOrgOptions {
|
|
44
|
+
/** The platform's edge has the `oikos.host` zone: a company without a domain waits on it for its own services. */
|
|
45
|
+
oikosZoneReady?: boolean;
|
|
44
46
|
/** #267: the executor was built with a `loginHostBindings` port. */
|
|
45
47
|
loginHostBindings?: boolean;
|
|
46
48
|
/** A live read said the org's login host serves OIDC discovery as `https://<host>`. Absent: not read, so Agora keeps the platform issuer. */
|
|
@@ -73,6 +75,14 @@ export interface PlanOrgOptions {
|
|
|
73
75
|
ownSfuKeysHeld?: boolean;
|
|
74
76
|
/** The relay is declared and has applied a route list; an own org's mail server waits for it. */
|
|
75
77
|
mailRelayApplied?: boolean;
|
|
78
|
+
/** The host port an own org's mail server publishes for the relay (`plan-apply.ts` allocates it). */
|
|
79
|
+
ownMailSmtpPort?: number;
|
|
80
|
+
/**
|
|
81
|
+
* The relay has applied a route for this org's mail domain. `false` holds the org's `coolify:mail`
|
|
82
|
+
* step: it publishes the MX, and until the relay routes the domain senders get a permanent 5xx.
|
|
83
|
+
* Absent counts as held.
|
|
84
|
+
*/
|
|
85
|
+
mailRelayRoute?: boolean;
|
|
76
86
|
/** The host confirmed the ports of an own org's media server slot are open on the server. */
|
|
77
87
|
sfuPortsOpen?: boolean;
|
|
78
88
|
/** What the executor's own files dependencies supply: valid keys worker settings, and a bucket port. Absent counts as neither. */
|
|
@@ -88,6 +98,12 @@ export interface PlanOrgOptions {
|
|
|
88
98
|
r2Keys: boolean;
|
|
89
99
|
storeR2Credentials: boolean;
|
|
90
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* F6: the uuid of the platform's backup storage on Coolify. Absent: the host
|
|
103
|
+
* wired no backups, so a company's databases get no backup step. `null`: wired,
|
|
104
|
+
* but the storage is not registered, so each database is reported blocked.
|
|
105
|
+
*/
|
|
106
|
+
backupStorageUuid?: string | null;
|
|
91
107
|
}
|
|
92
108
|
export interface PlanDetail {
|
|
93
109
|
orgId: OrgId;
|
|
@@ -157,14 +173,7 @@ export declare function toPlan(detail: PlanDetail): Plan;
|
|
|
157
173
|
* against this seam.
|
|
158
174
|
*/
|
|
159
175
|
export declare function computePlanDetail(org: DeclaredOrg, live: LiveResource[], recordedIds?: Readonly<Record<string, string>>, agoraHost?: AgoraHostReading, jobsAvailable?: boolean, platformReadiness?: OrgPlatformReadiness, options?: PlanOrgOptions): PlanDetail;
|
|
160
|
-
/**
|
|
161
|
-
* #488: the bare hostname out of an issuer URL (`https://<hostname>`, the
|
|
162
|
-
* only shape {@link agoraHostValuesFromPlatform} or a host-supplied
|
|
163
|
-
* `AgoraHostPort` ever hands back), or `null` when there is no issuer to
|
|
164
|
-
* read one from. What {@link loginHostnameFor} compares the org's own
|
|
165
|
-
* computed `auth.<domain>` against, so the platform's own org never plans
|
|
166
|
-
* its login host equal to the shared ZITADEL instance's own.
|
|
167
|
-
*/
|
|
176
|
+
/** True when `org` is the platform's owner company. */
|
|
168
177
|
export declare function isPlatformOwnOrg(org: Pick<DeclaredOrg, 'id'>, platformOwnerOrgId: string | null): boolean;
|
|
169
178
|
/**
|
|
170
179
|
* What {@link planOrgPostgres} reports about the org's one shared Postgres
|