@wtfalch/foundry 0.12.0 → 0.13.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.
@@ -1,252 +0,0 @@
1
- import { appStepKey } from './platform-diff.js';
2
- /** The `foundry_adoptions.resource_key` of the org's adopted tunnel: also its `serviceIds` key (#312). */
3
- export const adoptedTunnelKey = (org) => `cloudflare:tunnel:${org.id}`;
4
- /** The `foundry_adoptions.resource_key` of the org's adopted connector app (#312). */
5
- export const adoptedConnectorKey = (org) => `cloudflare:connector:${org.id}`;
6
- /** Every resource the org adopts, the database first, then the apps, the tunnel and its connector. */
7
- export function adoptedResources(org) {
8
- if (!org.hosting.enabled)
9
- return [];
10
- const database = org.hosting.adopt?.database;
11
- const resources = [];
12
- if (database) {
13
- resources.push({ resourceKey: `coolify:database:${org.id}`, kind: 'database', uuid: database });
14
- }
15
- for (const entry of org.hosting.entries) {
16
- if (!entry.adopt)
17
- continue;
18
- resources.push({
19
- resourceKey: `coolify:app:${appStepKey(org, entry.key)}`,
20
- kind: 'app',
21
- uuid: entry.adopt.app,
22
- entry,
23
- });
24
- }
25
- const { tunnel, connectorApp } = org.hosting.adopt ?? {};
26
- if (tunnel)
27
- resources.push({ resourceKey: adoptedTunnelKey(org), kind: 'tunnel', uuid: tunnel });
28
- if (connectorApp) {
29
- resources.push({
30
- resourceKey: adoptedConnectorKey(org),
31
- kind: 'connector',
32
- uuid: connectorApp,
33
- });
34
- }
35
- return resources;
36
- }
37
- /**
38
- * Every uuid the org declares as adopted, whether or not hosting is enabled
39
- * now: a uuid in this set is never stopped or deleted by foundry (#311).
40
- */
41
- export function declaredAdoptUuids(org) {
42
- const uuids = new Set();
43
- if (org.hosting.adopt?.database)
44
- uuids.add(org.hosting.adopt.database);
45
- if (org.hosting.adopt?.tunnel)
46
- uuids.add(org.hosting.adopt.tunnel);
47
- if (org.hosting.adopt?.connectorApp)
48
- uuids.add(org.hosting.adopt.connectorApp);
49
- for (const entry of org.hosting.entries)
50
- if (entry.adopt)
51
- uuids.add(entry.adopt.app);
52
- return uuids;
53
- }
54
- /**
55
- * A snapshot is ACTIVE when its row is `adopted` or `confirmed` and the uuid
56
- * it recorded is the uuid the org declares now (#311). Only then may foundry
57
- * write to the live resource.
58
- */
59
- export function isActiveSnapshot(row, uuid) {
60
- return (row !== null &&
61
- row !== undefined &&
62
- (row.state === 'adopted' || row.state === 'confirmed') &&
63
- row.settings.uuid === uuid);
64
- }
65
- /**
66
- * A row in `adopted`, `confirmed` or `restored` protects its key: foundry never
67
- * manages, stops or deletes that resource as its own. Only `released` lets go (#311).
68
- */
69
- export function protectsKey(row) {
70
- return row?.state === 'adopted' || row?.state === 'confirmed' || row?.state === 'restored';
71
- }
72
- /** Where the secret part of a snapshot lives in the `AdoptionVault`. */
73
- export function adoptionVaultRef(org, resource) {
74
- return `foundry:adopt:${org.id}:${resource.kind === 'app' ? `app:${resource.entry.key}` : resource.kind}`;
75
- }
76
- /** Coolify's field names for the non-secret settings an app snapshot keeps. */
77
- const APP_SETTING_FIELDS = [
78
- 'fqdn',
79
- 'git_branch',
80
- 'base_directory',
81
- 'publish_directory',
82
- 'build_pack',
83
- 'ports_exposes',
84
- 'health_check_enabled',
85
- 'health_check_path',
86
- 'health_check_port',
87
- 'health_check_method',
88
- 'health_check_return_code',
89
- 'health_check_scheme',
90
- 'health_check_interval',
91
- 'health_check_timeout',
92
- 'health_check_retries',
93
- 'health_check_start_period',
94
- 'custom_network_aliases',
95
- 'is_auto_deploy_enabled',
96
- 'limits_memory',
97
- 'limits_memory_swap',
98
- 'limits_memory_swappiness',
99
- 'limits_memory_reservation',
100
- 'limits_cpus',
101
- 'limits_cpuset',
102
- 'limits_cpu_shares',
103
- ];
104
- export function snapshotAppSettings(live) {
105
- return Object.fromEntries(APP_SETTING_FIELDS.map((field) => [field, live[field] ?? null]));
106
- }
107
- export const bareName = (name) => name.trim().replace(/^https?:\/\//, '');
108
- export const liveNames = (live) => typeof live.fqdn === 'string' ? live.fqdn.split(',').filter(Boolean).map(bareName) : [];
109
- /**
110
- * What an adopted entry changes on its live app: only a setting it declares
111
- * that differs from live. The entry cannot tell a declared `branch`,
112
- * `buildPack` or `autoDeploy` from its default, so those stay as they are; a
113
- * `null` folder or port and an empty name list declare nothing. Never
114
- * `custom_network_aliases`.
115
- */
116
- export function adoptedAppPatch(entry, names, live) {
117
- const patch = {};
118
- const folderField = entry.buildPack === 'static' ? 'publish_directory' : 'base_directory';
119
- if (entry.folder !== null && (live[folderField] ?? '/') !== `/${entry.folder}`) {
120
- patch[folderField] = `/${entry.folder}`;
121
- }
122
- if (entry.port !== null &&
123
- entry.buildPack !== 'static' &&
124
- `${live.ports_exposes}` !== `${entry.port}`) {
125
- patch.ports_exposes = String(entry.port);
126
- }
127
- if (names.length > 0 && [...names].sort().join() !== liveNames(live).sort().join()) {
128
- patch.domains = names.join(',');
129
- }
130
- return patch;
131
- }
132
- /** One line per setting the patch changes, old value beside new. */
133
- export function adoptedPatchDetails(patch, live) {
134
- return Object.entries(patch).map(([field, value]) => {
135
- const old = field === 'domains' ? live.fqdn : live[field];
136
- return `${field}: ${old ?? 'unset'} -> ${value}`;
137
- });
138
- }
139
- /**
140
- * The PATCH body that puts an app's snapshot settings back. A field is sent
141
- * only when the entry's declaration would have changed it from the snapshot
142
- * (`adoptedAppPatch`), so a setting foundry never touched, or a person edited
143
- * later, stays. With no entry, a field is sent when `live` differs from the
144
- * snapshot. Coolify reads `fqdn` but takes `domains`. A field the snapshot had
145
- * as null is cleared the way the plain app path clears it: a folder back to
146
- * `/`, domains to the empty list. A port is never cleared, as on that path.
147
- */
148
- export function restoreAppPatch(entry, names, settings, live) {
149
- const isStatic = (entry?.buildPack ?? settings.build_pack) === 'static';
150
- const folderField = isStatic ? 'publish_directory' : 'base_directory';
151
- const changed = new Set(Object.keys(entry ? adoptedAppPatch(entry, names, settings) : {}));
152
- if (!entry) {
153
- if ((live[folderField] ?? '/') !== (settings[folderField] ?? '/'))
154
- changed.add(folderField);
155
- if (`${live.ports_exposes}` !== `${settings.ports_exposes}`)
156
- changed.add('ports_exposes');
157
- if (liveNames(settings).sort().join() !== liveNames(live).sort().join())
158
- changed.add('domains');
159
- }
160
- const patch = {};
161
- if (changed.has(folderField))
162
- patch[folderField] = settings[folderField] ?? '/';
163
- if (changed.has('domains'))
164
- patch.domains = settings.fqdn ?? '';
165
- if (!isStatic && settings.ports_exposes != null && changed.has('ports_exposes')) {
166
- patch.ports_exposes = settings.ports_exposes;
167
- }
168
- return patch;
169
- }
170
- const isWildcard = (route) => route.hostname?.startsWith('*') ?? false;
171
- const isCatchAll = (route) => route !== undefined && route.hostname === undefined && route.path === undefined;
172
- /** A hostname as compared: lower case, no trailing dot (DNS names ignore both). */
173
- export const normalizeHostname = (hostname) => hostname.toLowerCase().replace(/\.$/, '');
174
- const sameName = (a, b) => a !== undefined && normalizeHostname(a) === normalizeHostname(b);
175
- const hasName = (names, hostname) => hostname !== undefined && [...names].some((name) => sameName(hostname, name));
176
- /** The route foundry writes for a name: nothing but the hostname and the service. */
177
- const isFoundryRoute = (route, service) => route.service === service && Object.keys(route).length === 2;
178
- /** Puts `back` before the first wildcard or hostname-less route of `rest`, so a person's order stays. */
179
- function putBack(rest, back) {
180
- const at = rest.findIndex((route) => route.hostname === undefined || isWildcard(route));
181
- return at < 0 ? [...rest, ...back] : [...rest.slice(0, at), ...back, ...rest.slice(at)];
182
- }
183
- /** A live route foundry wrote for a name it dropped: written, no longer declared, exactly foundry's own. */
184
- const isDroppedRoute = (route, declared, service, written) => hasName(written, route.hostname) &&
185
- !hasName(declared, route.hostname) &&
186
- isFoundryRoute(route, service);
187
- /**
188
- * What the merge leaves alone: every live route that is not for a declared
189
- * name and not foundry's own route for a name it dropped. Whatever its shape:
190
- * a hostname, only a `path`, or neither.
191
- */
192
- export function foreignRoutes(live, declared, service, written) {
193
- return live.filter((route) => !hasName(declared, route.hostname) && !isDroppedRoute(route, declared, service, written));
194
- }
195
- /** The declared names whose live routes the merge replaces: there are some, and they are not already foundry's one route. */
196
- export function replacedNames(live, declared, service) {
197
- return declared.filter((hostname) => {
198
- const same = live.filter((route) => sameName(route.hostname, hostname));
199
- const [only] = same;
200
- return only !== undefined && !(same.length === 1 && isFoundryRoute(only, service));
201
- });
202
- }
203
- /** The names foundry removes from `live`: written, no longer declared, still its own route. */
204
- export const removedNames = (live, declared, service, written) => [
205
- ...new Set(live
206
- .filter((route) => isDroppedRoute(route, declared, service, written))
207
- .map((route) => route.hostname)),
208
- ];
209
- /**
210
- * The ingress list an adopted tunnel gets (#312): the declared exact names,
211
- * then every foreign live route in its live order and unchanged, with the
212
- * snapshot's routes for a name foundry dropped put back among them, then the
213
- * declared wildcards, then the live catch-all as it is. Only a live list that
214
- * does not end in a catch-all gets `http_status:404`.
215
- */
216
- export function mergeIngress(live, declared, service, written, snapshot) {
217
- const dropped = removedNames(live, declared, service, written);
218
- const tail = live.at(-1);
219
- const foreign = foreignRoutes(live, declared, service, written);
220
- const catchAll = isCatchAll(tail) ? tail : { service: 'http_status:404' };
221
- const middle = isCatchAll(tail) ? foreign.slice(0, -1) : foreign;
222
- const back = snapshot.filter((route) => hasName(dropped, route.hostname));
223
- const route = (hostname) => ({ hostname, service });
224
- return [
225
- ...declared.filter((hostname) => !hostname.startsWith('*')).map(route),
226
- ...putBack(middle, back),
227
- ...declared.filter((hostname) => hostname.startsWith('*')).map(route),
228
- catchAll,
229
- ];
230
- }
231
- /**
232
- * The ingress list restore writes (#312): the live list without foundry's own
233
- * route for a name it wrote (`written` accumulates for the life of the
234
- * snapshot), plus the snapshot's routes for that name. A name with no live
235
- * route that is exactly foundry's is left alone, as is a route a person added
236
- * or edited, and the live catch-all. The `http_status:404` foundry appended is
237
- * removed only while it is still last and the snapshot had no catch-all; an
238
- * empty snapshot keeps the live catch-all, or gets one.
239
- */
240
- export function restoreIngress(live, snapshot, service, written) {
241
- const isOurs = (route) => hasName(written, route.hostname) && isFoundryRoute(route, service);
242
- const swapped = (route) => hasName(live.filter(isOurs).map((r) => r.hostname), route.hostname);
243
- const appended = snapshot.length > 0 &&
244
- !isCatchAll(snapshot.at(-1)) &&
245
- isCatchAll(live.at(-1)) &&
246
- live.at(-1)?.service === 'http_status:404';
247
- const rest = live.filter((route, index) => !isOurs(route) && !(appended && index === live.length - 1));
248
- const restored = putBack(rest, snapshot.filter(swapped));
249
- return snapshot.length === 0 && !isCatchAll(restored.at(-1))
250
- ? [...restored, { service: 'http_status:404' }]
251
- : restored;
252
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Where an adoption keeps the secret part of its snapshot (#311): the live
3
- * app's env values. The row in `foundry_adoptions` holds only the ref. Same
4
- * shape as the `heldKeys` port (`../bootstrap/bootstrap.ts`).
5
- */
6
- export interface AdoptionVault {
7
- put(ref: string, value: string): Promise<void>;
8
- resolve(ref: string): Promise<string | null>;
9
- }
@@ -1 +0,0 @@
1
- export {};