@wtfalch/foundry 0.42.1 → 0.43.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/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/migrations/0055_release_requests.sql +20 -0
- package/dist/migrations/0056_platform_archon_blue_green.sql +7 -0
- package/dist/org.d.ts +13 -0
- package/dist/org.js +38 -1
- package/dist/plan/archon-blue-green.d.ts +54 -0
- package/dist/plan/archon-blue-green.js +72 -0
- package/dist/plan/blue-green-apply.d.ts +82 -0
- package/dist/plan/blue-green-apply.js +360 -0
- package/dist/plan/blue-green.d.ts +223 -0
- package/dist/plan/blue-green.js +444 -0
- package/dist/plan/cloudflared-connector.d.ts +26 -0
- package/dist/plan/cloudflared-connector.js +37 -0
- package/dist/plan/diff.d.ts +6 -0
- package/dist/plan/diff.js +31 -2
- package/dist/plan/executor.d.ts +34 -0
- package/dist/plan/executor.js +244 -9
- package/dist/plan/plan-apply.d.ts +51 -0
- package/dist/plan/plan-apply.js +222 -0
- package/dist/plan/platform-diff.d.ts +9 -0
- package/dist/plan/platform-diff.js +24 -1
- package/dist/plan/platform-providers.d.ts +15 -1
- package/dist/plan/platform-providers.js +90 -5
- package/dist/plan/proxy-labels.d.ts +2 -2
- package/dist/plan/proxy-labels.js +12 -4
- package/dist/plan/release-host.d.ts +59 -0
- package/dist/plan/release-host.js +239 -0
- package/dist/plan/remote-database.d.ts +124 -0
- package/dist/plan/remote-database.js +373 -0
- package/dist/platform.d.ts +11 -1
- package/dist/platform.js +4 -1
- package/dist/release/decide.d.ts +95 -0
- package/dist/release/decide.js +242 -0
- package/dist/release/health.d.ts +48 -0
- package/dist/release/health.js +61 -0
- package/dist/release/run.d.ts +40 -0
- package/dist/release/run.js +56 -0
- package/dist/release/state.d.ts +64 -0
- package/dist/release/state.js +126 -0
- package/dist/release/types.d.ts +54 -0
- package/dist/release/types.js +4 -0
- package/dist/store/postgres.d.ts +8 -0
- package/dist/store/postgres.js +56 -3
- package/dist/store/schema.d.ts +154 -0
- package/dist/store/schema.js +12 -0
- package/package.json +1 -1
- package/versions.digests.json +2 -1
- package/versions.json +2 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ReleaseDeps } from '../release/run.js';
|
|
2
|
+
import type { ReleaseStore } from '../release/state.js';
|
|
3
|
+
import { type BlueGreenSubject } from './blue-green.js';
|
|
4
|
+
import { type DeployHealthRuntime } from './deploy-health.js';
|
|
5
|
+
import type { CoolifyPort } from './executor.js';
|
|
6
|
+
/** The recorded ids a release run reads and writes: an org's apply context, or the platform's. */
|
|
7
|
+
export interface ReleaseContext {
|
|
8
|
+
recordedIds(): Readonly<Record<string, string>>;
|
|
9
|
+
recordScopeId(key: string, id: string): void;
|
|
10
|
+
forgetId(key: string): void;
|
|
11
|
+
persist(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** What a run needs besides the runner's own deps: stage, and the balancers' weights on every server. */
|
|
14
|
+
export interface ReleaseHost {
|
|
15
|
+
deps: ReleaseDeps;
|
|
16
|
+
/** Records stage's newest finished build as `bg.<id>.stage`. A failed read changes nothing. */
|
|
17
|
+
refreshStage(): Promise<void>;
|
|
18
|
+
/** Writes the recorded weights to the balancer on every server that has one. */
|
|
19
|
+
writeWeights(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface ReleaseHostInput {
|
|
22
|
+
subject: BlueGreenSubject;
|
|
23
|
+
context: ReleaseContext;
|
|
24
|
+
coolify: Pick<CoolifyPort, 'getApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'deployAndWait' | 'raw' | 'listStorages' | 'createFileStorage' | 'updateFileStorage'> & {
|
|
25
|
+
listApplicationDeployments?: CoolifyPort['listApplicationDeployments'];
|
|
26
|
+
};
|
|
27
|
+
/** Reads the entry's health token (`sharedSecretRef(org.id, healthTokenName(id))`). Never mints. */
|
|
28
|
+
healthToken: () => Promise<string | null>;
|
|
29
|
+
/** Registers a value for redaction before it is written anywhere. */
|
|
30
|
+
secret: (value: string) => void;
|
|
31
|
+
primaryUuid: string;
|
|
32
|
+
/** The Coolify app of the blue copy on the primary (the entry's `coolify:app`, or the running archon app). */
|
|
33
|
+
primaryBlueAppId: string;
|
|
34
|
+
runtime: DeployHealthRuntime;
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
/** Extends the mutation lease; called before each outside call that can take long. */
|
|
37
|
+
renew: () => Promise<void>;
|
|
38
|
+
label: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function createReleaseHost(input: ReleaseHostInput): ReleaseHost;
|
|
41
|
+
/**
|
|
42
|
+
* One release run for one subject, writing into `context` (the live recorded ids of the run that holds
|
|
43
|
+
* the mutation lease, so no stale copy is ever written back). Seeds `live` and the weights on the first run
|
|
44
|
+
* after the structure is applied. The weights the runner decided reach every server's balancer before this
|
|
45
|
+
* returns; a balancer that fails leaves `bg.<id>.lb.<server>` behind the weights, and the next run
|
|
46
|
+
* (like the next apply, through its `bluegreen-lb` step) writes it again.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runSubjectRelease(input: {
|
|
49
|
+
store: Pick<ReleaseStore, 'openRequests' | 'consumeRequests'>;
|
|
50
|
+
scopeOwnerId: string;
|
|
51
|
+
subject: BlueGreenSubject;
|
|
52
|
+
host: ReleaseHost;
|
|
53
|
+
context: ReleaseContext;
|
|
54
|
+
now: Date;
|
|
55
|
+
primaryUuid: string;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
blocked: string[];
|
|
58
|
+
refused: string[];
|
|
59
|
+
}>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { checkColorAddress, readReleaseHealth } from '../release/health.js';
|
|
2
|
+
import { runRelease } from '../release/run.js';
|
|
3
|
+
import { applyBlueGreenWeights } from './blue-green-apply.js';
|
|
4
|
+
import { DEFAULT_WEIGHTS, LB_COLOR_HEADER, LB_PIN_HEADER, bgKey, formatWeights, parseWeights, reportingEnv, } from './blue-green.js';
|
|
5
|
+
import { waitForNewDeployment } from './deploy-health.js';
|
|
6
|
+
import { isNotFoundError } from './executor.js';
|
|
7
|
+
const COMMIT = /^[0-9a-f]{40}$/;
|
|
8
|
+
/** Coolify reports a restarting container as `restarting:...`; a crash loop is that. */
|
|
9
|
+
function statusOf(status) {
|
|
10
|
+
if (status?.startsWith('running'))
|
|
11
|
+
return 'running';
|
|
12
|
+
if (status?.startsWith('restarting'))
|
|
13
|
+
return 'crash_loop';
|
|
14
|
+
return 'other';
|
|
15
|
+
}
|
|
16
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
export function createReleaseHost(input) {
|
|
18
|
+
const { subject, context, coolify } = input;
|
|
19
|
+
const id = subject.id;
|
|
20
|
+
const recorded = () => context.recordedIds();
|
|
21
|
+
/** Every Coolify app of `color`, one per server that has the structure applied. */
|
|
22
|
+
const colorApps = (color) => {
|
|
23
|
+
const ids = recorded();
|
|
24
|
+
const apps = [];
|
|
25
|
+
if (color === 'blue')
|
|
26
|
+
apps.push(input.primaryBlueAppId);
|
|
27
|
+
const prefix = `bg.${id}.app.`;
|
|
28
|
+
for (const key of Object.keys(ids).sort()) {
|
|
29
|
+
if (!key.startsWith(prefix) || !key.endsWith(`.${color}`))
|
|
30
|
+
continue;
|
|
31
|
+
const value = ids[key];
|
|
32
|
+
if (value)
|
|
33
|
+
apps.push(value);
|
|
34
|
+
}
|
|
35
|
+
return apps;
|
|
36
|
+
};
|
|
37
|
+
const primaryColorApp = (color) => color === 'blue' ? input.primaryBlueAppId : recorded()[bgKey.app(id, input.primaryUuid, color)];
|
|
38
|
+
/**
|
|
39
|
+
* Where the runner reaches a color: the entry's first public hostname, pinned to the color by the balancer's
|
|
40
|
+
* header router. The balancer routes the pinned request to that color whatever its weight.
|
|
41
|
+
*/
|
|
42
|
+
const addressOf = (color) => {
|
|
43
|
+
const hostname = subject.hostnames.find((name) => !name.startsWith('*'));
|
|
44
|
+
if (!hostname)
|
|
45
|
+
throw new Error(`${input.label}: the app has no public hostname to check through`);
|
|
46
|
+
const pinKey = recorded()[bgKey.pinKey(id)];
|
|
47
|
+
if (!pinKey)
|
|
48
|
+
throw new Error(`${input.label}: the balancer's pin key is not recorded`);
|
|
49
|
+
return {
|
|
50
|
+
address: `https://${hostname}`,
|
|
51
|
+
headers: { [LB_COLOR_HEADER]: color, [LB_PIN_HEADER]: pinKey },
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** The color a health read goes to: the one being released, else the live one. */
|
|
55
|
+
const healthColor = () => {
|
|
56
|
+
try {
|
|
57
|
+
const window = JSON.parse(recorded()[bgKey.window(id)] ?? '{}');
|
|
58
|
+
if (window.release?.to === 'blue' || window.release?.to === 'green')
|
|
59
|
+
return window.release.to;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A damaged window reads as no release; the runner rewrites it.
|
|
63
|
+
}
|
|
64
|
+
return recorded()[bgKey.live(id)] === 'green' ? 'green' : 'blue';
|
|
65
|
+
};
|
|
66
|
+
const deployTo = async (color, digest) => {
|
|
67
|
+
if (!COMMIT.test(digest))
|
|
68
|
+
throw new Error(`${input.label}: the release is not a built commit`);
|
|
69
|
+
const token = await input.healthToken();
|
|
70
|
+
if (!token)
|
|
71
|
+
throw new Error(`${input.label}: the health token is not held`);
|
|
72
|
+
input.secret(token);
|
|
73
|
+
const apps = colorApps(color);
|
|
74
|
+
if (apps.length === 0)
|
|
75
|
+
throw new Error(`${input.label}: ${color} has no app recorded`);
|
|
76
|
+
for (const appId of apps) {
|
|
77
|
+
await input.renew();
|
|
78
|
+
await coolify.updateApplication(appId, { git_commit_sha: digest });
|
|
79
|
+
try {
|
|
80
|
+
await coolify.bulkUpsertEnvs(appId, reportingEnv({ color, release: digest, healthToken: token }).map((e) => ({
|
|
81
|
+
...e,
|
|
82
|
+
literal: true,
|
|
83
|
+
})));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// A Coolify error can echo the request body, which holds the token.
|
|
87
|
+
throw new Error(`${input.label}: writing env to app ${appId} failed`);
|
|
88
|
+
}
|
|
89
|
+
await waitForNewDeployment(coolify, appId, { kind: 'deployment-only' }, input.runtime, input.runtime.sleep ?? defaultSleep, `${input.label} ${color}`, `${color} stays as it was`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const stop = async (color) => {
|
|
93
|
+
for (const appId of colorApps(color)) {
|
|
94
|
+
await input.renew();
|
|
95
|
+
try {
|
|
96
|
+
await coolify.raw('POST', `/applications/${appId}/stop`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!isNotFoundError(error))
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const deps = {
|
|
105
|
+
async readHealth(range) {
|
|
106
|
+
await input.renew();
|
|
107
|
+
const token = await input.healthToken();
|
|
108
|
+
if (!token)
|
|
109
|
+
return null;
|
|
110
|
+
input.secret(token);
|
|
111
|
+
return readReleaseHealth({
|
|
112
|
+
...addressOf(healthColor()),
|
|
113
|
+
token,
|
|
114
|
+
...range,
|
|
115
|
+
...(input.fetch ? { fetchImpl: input.fetch } : {}),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
async observeColor(color) {
|
|
119
|
+
await input.renew();
|
|
120
|
+
const appId = primaryColorApp(color);
|
|
121
|
+
if (!appId)
|
|
122
|
+
return { status: 'other', address: 'unchecked' };
|
|
123
|
+
const status = statusOf((await coolify.getApplication(appId)).status);
|
|
124
|
+
if (status !== 'running')
|
|
125
|
+
return { status, address: 'unchecked' };
|
|
126
|
+
return {
|
|
127
|
+
status,
|
|
128
|
+
address: await checkColorAddress({
|
|
129
|
+
...addressOf(color),
|
|
130
|
+
healthPath: subject.healthPath,
|
|
131
|
+
...(input.fetch ? { fetchImpl: input.fetch } : {}),
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
async effect(effect) {
|
|
136
|
+
if (effect.kind === 'deploy')
|
|
137
|
+
await deployTo(effect.color, effect.digest);
|
|
138
|
+
else
|
|
139
|
+
await stop(effect.color);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
deps,
|
|
144
|
+
async refreshStage() {
|
|
145
|
+
const stageId = recorded()[bgKey.app(id, input.primaryUuid, 'stage')];
|
|
146
|
+
if (!stageId || !coolify.listApplicationDeployments)
|
|
147
|
+
return;
|
|
148
|
+
let rows;
|
|
149
|
+
try {
|
|
150
|
+
rows = await coolify.listApplicationDeployments(stageId, 5);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// The next run reads it again; a stale stage digest only delays a promote.
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const built = rows.find((row) => row.status === 'finished' && COMMIT.test(row.commit ?? ''));
|
|
157
|
+
if (built?.commit && recorded()[bgKey.stage(id)] !== built.commit) {
|
|
158
|
+
context.recordScopeId(bgKey.stage(id), built.commit);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
async writeWeights() {
|
|
162
|
+
const pattern = new RegExp(`^bg\\.${id}\\.app\\.([^.]+)\\.lb$`);
|
|
163
|
+
const servers = Object.keys(recorded())
|
|
164
|
+
.map((key) => pattern.exec(key)?.[1])
|
|
165
|
+
.filter((uuid) => Boolean(uuid));
|
|
166
|
+
for (const uuid of servers) {
|
|
167
|
+
await input.renew();
|
|
168
|
+
await applyBlueGreenWeights({
|
|
169
|
+
coolify,
|
|
170
|
+
recorded,
|
|
171
|
+
record: (key, value) => context.recordScopeId(key, value),
|
|
172
|
+
persist: () => context.persist(),
|
|
173
|
+
label: input.label,
|
|
174
|
+
}, subject, { uuid, primaryUuid: input.primaryUuid });
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Whether any server's balancer was last written with weights other than `weights`. */
|
|
180
|
+
function balancerLags(ids, id, weights) {
|
|
181
|
+
const pattern = new RegExp(`^bg\\.${id}\\.app\\.([^.]+)\\.lb$`);
|
|
182
|
+
return Object.keys(ids).some((key) => {
|
|
183
|
+
const uuid = pattern.exec(key)?.[1];
|
|
184
|
+
return uuid !== undefined && ids[bgKey.lb(id, uuid)] !== weights;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* One release run for one subject, writing into `context` (the live recorded ids of the run that holds
|
|
189
|
+
* the mutation lease, so no stale copy is ever written back). Seeds `live` and the weights on the first run
|
|
190
|
+
* after the structure is applied. The weights the runner decided reach every server's balancer before this
|
|
191
|
+
* returns; a balancer that fails leaves `bg.<id>.lb.<server>` behind the weights, and the next run
|
|
192
|
+
* (like the next apply, through its `bluegreen-lb` step) writes it again.
|
|
193
|
+
*/
|
|
194
|
+
export async function runSubjectRelease(input) {
|
|
195
|
+
const { subject, context } = input;
|
|
196
|
+
const id = subject.id;
|
|
197
|
+
const ids = () => context.recordedIds();
|
|
198
|
+
if (!ids()[bgKey.applied(id, input.primaryUuid)])
|
|
199
|
+
return { blocked: [], refused: [] };
|
|
200
|
+
if (!ids()[bgKey.live(id)]) {
|
|
201
|
+
context.recordScopeId(bgKey.live(id), 'blue');
|
|
202
|
+
if (!ids()[bgKey.weights(id)])
|
|
203
|
+
context.recordScopeId(bgKey.weights(id), DEFAULT_WEIGHTS);
|
|
204
|
+
await context.persist();
|
|
205
|
+
}
|
|
206
|
+
await input.host.refreshStage();
|
|
207
|
+
const before = ids()[bgKey.weights(id)];
|
|
208
|
+
const result = await runRelease({
|
|
209
|
+
store: {
|
|
210
|
+
getScopeIds: async () => ({ ...ids() }),
|
|
211
|
+
addRequest: async () => {
|
|
212
|
+
throw new Error('the release runner takes requests; it does not add them');
|
|
213
|
+
},
|
|
214
|
+
openRequests: (query) => input.store.openRequests(query),
|
|
215
|
+
consumeRequests: async () => {
|
|
216
|
+
throw new Error('the release runner consumes requests after it persists');
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
orgId: input.scopeOwnerId,
|
|
220
|
+
entryKey: id,
|
|
221
|
+
entry: subject.config,
|
|
222
|
+
now: input.now,
|
|
223
|
+
deps: input.host.deps,
|
|
224
|
+
});
|
|
225
|
+
for (const [key, value] of Object.entries(result.writes)) {
|
|
226
|
+
if (value === null)
|
|
227
|
+
context.forgetId(key);
|
|
228
|
+
else
|
|
229
|
+
context.recordScopeId(key, value);
|
|
230
|
+
}
|
|
231
|
+
await context.persist();
|
|
232
|
+
await input.store.consumeRequests(result.requestIds);
|
|
233
|
+
const after = parseWeights(ids()[bgKey.weights(id)]);
|
|
234
|
+
// Also when a balancer lags the weights (an earlier write failed), so the next tick retries it.
|
|
235
|
+
if (after && (formatWeights(after) !== before || balancerLags(ids(), id, formatWeights(after)))) {
|
|
236
|
+
await input.host.writeWeights();
|
|
237
|
+
}
|
|
238
|
+
return { blocked: result.blocked, refused: result.refused };
|
|
239
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
import { type BlueGreenServer } from './blue-green.js';
|
|
3
|
+
import type { DraftStep } from './diff.js';
|
|
4
|
+
import type { AccessPort } from './edge-access.js';
|
|
5
|
+
import type { DnsPort } from './edge-dns.js';
|
|
6
|
+
import type { TunnelIngress, TunnelPort } from './edge-tunnels.js';
|
|
7
|
+
import type { ZonePort } from './edge-zones.js';
|
|
8
|
+
import type { CoolifyPort } from './executor.js';
|
|
9
|
+
/**
|
|
10
|
+
* A blue/green app on a server other than the database's reaches its one database through a
|
|
11
|
+
* Cloudflare tunnel TCP route behind an Access service token (William, 2026-10-15). The tunnel has a
|
|
12
|
+
* `tcp://<org>-db:5432` rule on an internal hostname; Access lets only the service token in; a
|
|
13
|
+
* `cloudflared access tcp` sidecar on each other server listens on 5432 under the name
|
|
14
|
+
* {@link remoteDatabaseHost}.
|
|
15
|
+
*/
|
|
16
|
+
export declare const REMOTE_DATABASE_PORT = 5432;
|
|
17
|
+
/** The sidecar's name on the Docker network of `serverUuid`, or `null` on the database's own server. */
|
|
18
|
+
export declare function remoteDatabaseHost(entryKey: string, serverUuid: string, primaryUuid: string): string | null;
|
|
19
|
+
/** The `host:port` a color on a server other than the database's connects to. */
|
|
20
|
+
export declare const remoteDatabaseAddress: (host: string) => string;
|
|
21
|
+
/** What the host knows when it plans. Absent from the plan options: the host cannot do this, nothing is planned. */
|
|
22
|
+
export interface RemoteDatabaseFacts {
|
|
23
|
+
/** Every usable, reachable Coolify server. */
|
|
24
|
+
serverUuids: readonly string[];
|
|
25
|
+
/** The server that holds the database. */
|
|
26
|
+
primaryUuid: string | null;
|
|
27
|
+
/** The internal zone's apex at the platform's edge; `null` when it has none. */
|
|
28
|
+
zone: string | null;
|
|
29
|
+
/** The executor has Access, tunnel, DNS and zone ports. */
|
|
30
|
+
access: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The facts for {@link planRemoteDatabase}, from what the planner already read. The zone is the one the org's
|
|
34
|
+
* tunnel names live in (`org.domain`); `access` says the host can reach Access, tunnels, DNS and zones.
|
|
35
|
+
*/
|
|
36
|
+
export declare function remoteDatabaseFactsFrom(input: {
|
|
37
|
+
org: DeclaredOrg;
|
|
38
|
+
servers: readonly BlueGreenServer[];
|
|
39
|
+
recorded: Readonly<Record<string, string>>;
|
|
40
|
+
access: boolean;
|
|
41
|
+
}): RemoteDatabaseFacts;
|
|
42
|
+
/** One TCP rule: the hostname under the internal zone and the database it reaches. */
|
|
43
|
+
export interface TcpRoute {
|
|
44
|
+
hostname: string;
|
|
45
|
+
service: string;
|
|
46
|
+
}
|
|
47
|
+
/** The scope ids one entry's step keeps. None holds a secret. */
|
|
48
|
+
export declare const remoteDatabaseKeys: (entryKey: string) => {
|
|
49
|
+
/** The service token's id, from its mint until its secret reached every sidecar. No `:`. */
|
|
50
|
+
undelivered: string;
|
|
51
|
+
/** The service token's id once delivered. */
|
|
52
|
+
token: string;
|
|
53
|
+
access: string;
|
|
54
|
+
route: string;
|
|
55
|
+
applied: string;
|
|
56
|
+
sidecar: (serverUuid: string) => string;
|
|
57
|
+
};
|
|
58
|
+
/** The tunnel's HTTP rules as the org tunnel step last wrote them (JSON), so a later write keeps them. */
|
|
59
|
+
export declare const TUNNEL_HTTP_INGRESS_SCOPE_KEY = "cloudflare_tunnel_http_ingress";
|
|
60
|
+
/** Every TCP route recorded for the org, in key order. */
|
|
61
|
+
export declare function recordedTcpRoutes(recorded: Readonly<Record<string, string>>): TcpRoute[];
|
|
62
|
+
/** The HTTP rules recorded by the tunnel step, catch-all included; `[]` if none or unreadable. */
|
|
63
|
+
export declare function recordedHttpIngress(recorded: Readonly<Record<string, string>>): TunnelIngress[];
|
|
64
|
+
/**
|
|
65
|
+
* The tunnel's whole ingress: the TCP rules first (their names are exact), then every HTTP rule, the
|
|
66
|
+
* catch-all last. `putTunnelConfig` replaces the whole list, so every writer builds it through this.
|
|
67
|
+
*/
|
|
68
|
+
export declare function mergeTcpIngress(http: TunnelIngress[], tcp: TcpRoute[]): TunnelIngress[];
|
|
69
|
+
/** What the step carries to the executor (`action.desired`). No secret. */
|
|
70
|
+
export interface RemoteDatabaseDesired {
|
|
71
|
+
entry: string;
|
|
72
|
+
app: string;
|
|
73
|
+
hostname: string;
|
|
74
|
+
/** The recorded id of the org's database; the executor reads its container name from the live database. */
|
|
75
|
+
database: string;
|
|
76
|
+
primary: string;
|
|
77
|
+
servers: string[];
|
|
78
|
+
zone: string;
|
|
79
|
+
}
|
|
80
|
+
/** Whether the route step has been applied for `serverUuid`: its marker names the server. */
|
|
81
|
+
export declare function remoteDatabaseAppliedFor(recorded: Readonly<Record<string, string>>, entryKey: string, serverUuid: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Plans the remote database route for the org's blue/green database entry. One server, or no facts,
|
|
84
|
+
* plans nothing. A missing zone, port, tunnel or primary is a blocked line (rule 2), and so is a token
|
|
85
|
+
* that was minted and never delivered (rule 1): it is never minted again.
|
|
86
|
+
*/
|
|
87
|
+
export declare function planRemoteDatabase(org: DeclaredOrg, facts: RemoteDatabaseFacts | undefined, recorded: Readonly<Record<string, string>>): {
|
|
88
|
+
step: DraftStep | null;
|
|
89
|
+
blocked: {
|
|
90
|
+
description: string;
|
|
91
|
+
key: string;
|
|
92
|
+
} | null;
|
|
93
|
+
};
|
|
94
|
+
/** The Dockerfile of the sidecar: the pinned `cloudflared`, listening on 5432 for the Access-protected route. */
|
|
95
|
+
export declare function remoteDatabaseDockerfile(hostname: string): string;
|
|
96
|
+
export interface RemoteDatabaseDeps {
|
|
97
|
+
tunnels: Pick<TunnelPort, 'putTunnelConfig'>;
|
|
98
|
+
access: Pick<AccessPort, 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'findServiceTokenByName' | 'createServiceToken'>;
|
|
99
|
+
dns: Pick<DnsPort, 'findDnsRecord' | 'createDnsRecord'>;
|
|
100
|
+
zones: Pick<ZonePort, 'findZoneByApex'>;
|
|
101
|
+
coolify: Pick<CoolifyPort, 'getApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'findOrCreateEnvironment'>;
|
|
102
|
+
/** Registers a value for redaction before it is written anywhere. */
|
|
103
|
+
secret: (value: string) => void;
|
|
104
|
+
}
|
|
105
|
+
export interface RemoteDatabaseRun {
|
|
106
|
+
org: DeclaredOrg;
|
|
107
|
+
desired: RemoteDatabaseDesired;
|
|
108
|
+
/** The database's container name on the Docker network, as the app's `DATABASE_URL` names it. */
|
|
109
|
+
databaseHost: string;
|
|
110
|
+
projectUuid: string;
|
|
111
|
+
recorded(): Readonly<Record<string, string>>;
|
|
112
|
+
record(key: string, value: string): void;
|
|
113
|
+
persist(): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Applies one entry's route. The Access app and its service-token policy come first, so the tunnel route
|
|
117
|
+
* and the DNS record never exist without the gate in front of them. The service token is minted once
|
|
118
|
+
* (rule 1): its id is recorded and persisted before delivery, the secret goes only to the sidecars' env
|
|
119
|
+
* (rule 6), the delivered id replaces it, and the applied marker is the last write. A failure halfway
|
|
120
|
+
* leaves a repair for the next run: the Access app, sidecars, route and DNS are found again; a minted
|
|
121
|
+
* token is never minted again. A sidecar made after the mint takes its secret from an existing sidecar's
|
|
122
|
+
* env. Every outside call is bounded by the ports' own timeouts (edge SDK and Coolify client).
|
|
123
|
+
*/
|
|
124
|
+
export declare function applyRemoteDatabase(deps: RemoteDatabaseDeps, run: RemoteDatabaseRun): Promise<void>;
|