@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
|
@@ -18,6 +18,8 @@ export interface PinnedVersions {
|
|
|
18
18
|
busybox: string;
|
|
19
19
|
/** The sign-in database's image (#746). Its major version is the data directory's format: a bump past 16 needs a dump and restore, not an image change. */
|
|
20
20
|
postgres: string;
|
|
21
|
+
/** The load balancer of a blue/green app (`plan/blue-green.ts`): Traefik with a file provider. */
|
|
22
|
+
traefik: string;
|
|
21
23
|
}
|
|
22
24
|
export declare function loadVersions(): PinnedVersions;
|
|
23
25
|
export declare const VERSIONS: PinnedVersions;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,12 @@ export { StaleLeaseError } from './store.js';
|
|
|
16
16
|
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
17
17
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
18
18
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
19
|
+
export type { Color, ReleaseAction, ReleaseSignal, ReleaseState, WindowState, } from './release/types.js';
|
|
20
|
+
export type { ReleaseStore } from './release/state.js';
|
|
21
|
+
export { readReleaseState, requestReleaseAction } from './release/state.js';
|
|
22
|
+
export { createPostgresReleaseStore } from './store/postgres.js';
|
|
19
23
|
export type { FoundryStoreDb } from './store/postgres.js';
|
|
20
|
-
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
24
|
+
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, foundryRequestedChanges, type AuditEventRow, type DeclaredOrgRow, type NewDeclaredOrgRow, type AppliedStateRow, type NewAppliedStateRow, type AppliedVersionRow, type NewAppliedVersionRow, type DeclaredPlatformRow, type NewDeclaredPlatformRow, type RequestedChangeRow, type NewRequestedChangeRow, } from './store/schema.js';
|
|
21
25
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, StepAction, PlannedStep, StepSystem, PlanDetail, OrgPostgresPlan, PlatformPlanDetail, MailRoute, AdoptCandidate, AdoptInput, AdoptPlan, AdoptSnapshot, AuditActor, AuditLog, AuthzGate, StepExecutor, StepDestroyer, DestroyTarget, StepExecutionContext, PlatformStepExecutor, PlatformStepExecutionContext, FoundryPlanApplyOptions, FoundryApplyOptions, UnconfirmedUpgrade, } from './plan/index.js';
|
|
22
26
|
export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
|
|
23
27
|
export { AdoptRefusedError } from './plan/index.js';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,9 @@ export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
|
10
10
|
export { StaleLeaseError } from './store.js';
|
|
11
11
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
12
12
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
13
|
-
export {
|
|
13
|
+
export { readReleaseState, requestReleaseAction } from './release/state.js';
|
|
14
|
+
export { createPostgresReleaseStore } from './store/postgres.js';
|
|
15
|
+
export { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryAuditEvents, foundryDeclaredPlatform, foundryReleaseRequests, foundryRequestedChanges, } from './store/schema.js';
|
|
14
16
|
export { COMPANY_DESTINATION_NAME_PREFIX, COMPANY_NETWORKS, DEFAULT_MAX_DESTINATIONS, DestinationCapacityError, PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, placed, placedOnPlatform, validateNetworkName, } from './plan/index.js';
|
|
15
17
|
export { AdoptRefusedError } from './plan/index.js';
|
|
16
18
|
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- @wtfalch/foundry: an operator's release action (promote, roll back, clear a
|
|
2
|
+
-- held digest, decide at the cap) waiting for the release runner. The runner
|
|
3
|
+
-- marks a row consumed in one statement, so each request is applied once.
|
|
4
|
+
-- Additive: a new table, no existing row touched. Copies 0036_applied_versions
|
|
5
|
+
-- (an appended table with no foreign key, because `org_id` may be `platform`).
|
|
6
|
+
-- Mirrors src/store/schema.ts's `foundryReleaseRequests`.
|
|
7
|
+
|
|
8
|
+
create table if not exists foundry_release_requests (
|
|
9
|
+
id bigserial primary key,
|
|
10
|
+
org_id text not null,
|
|
11
|
+
entry_key text not null,
|
|
12
|
+
action jsonb not null,
|
|
13
|
+
actor text not null,
|
|
14
|
+
created_at timestamptz not null default now(),
|
|
15
|
+
consumed_at timestamptz
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
create index if not exists foundry_release_requests_open_idx
|
|
19
|
+
on foundry_release_requests (org_id, entry_key, id)
|
|
20
|
+
where consumed_at is null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- @wtfalch/foundry: `archon_blue_green` is `platform.archon.blueGreen` as JSON ({ steps?, stepHoldMinutes? });
|
|
2
|
+
-- null (the default) keeps platform-archon on one app. Copies 0052_platform_mail_relay.sql: one
|
|
3
|
+
-- nullable column added with `if not exists`, so it is safe to run at container start on live rows.
|
|
4
|
+
-- Mirrors src/store/schema.ts's `foundryDeclaredPlatform` table addition.
|
|
5
|
+
|
|
6
|
+
alter table foundry_declared_platform
|
|
7
|
+
add column if not exists archon_blue_green jsonb;
|
package/dist/org.d.ts
CHANGED
|
@@ -355,6 +355,17 @@ export interface HostingEntry {
|
|
|
355
355
|
* is the org's own domain.
|
|
356
356
|
*/
|
|
357
357
|
worker?: WorkerConfig;
|
|
358
|
+
/**
|
|
359
|
+
* Blue/green releases (docs/adr/0001-blue-green-releases.md). Presence turns it on: foundry runs the app as a
|
|
360
|
+
* blue and a green copy behind a load balancer on every server it manages, and the tunnel routes to the balancer.
|
|
361
|
+
*/
|
|
362
|
+
blueGreen?: BlueGreenConfig;
|
|
363
|
+
}
|
|
364
|
+
/** The traffic steps (percent to the new color) and the hold at each step. Both default in `plan/blue-green.ts`. */
|
|
365
|
+
export interface BlueGreenConfig {
|
|
366
|
+
/** Ascending, each 1..100, ending at 100. */
|
|
367
|
+
steps?: number[];
|
|
368
|
+
stepHoldMinutes?: number;
|
|
358
369
|
}
|
|
359
370
|
/** One named volume on a hosting entry's app. */
|
|
360
371
|
export interface EntryVolume {
|
|
@@ -552,6 +563,8 @@ export declare function validateHosting(hosting: HostingConfig, names?: {
|
|
|
552
563
|
domain: string | null;
|
|
553
564
|
locked: ReadonlySet<string>;
|
|
554
565
|
}): string | null;
|
|
566
|
+
/** `blueGreen`: steps ascending, each a whole number 1..100, the last 100; a hold of 1..1440 minutes. */
|
|
567
|
+
export declare function validateBlueGreen(value: unknown, at: string): string | null;
|
|
555
568
|
export declare function validateDomain(domain: string): string | null;
|
|
556
569
|
/**
|
|
557
570
|
* A broker entry name foundry writes itself: Archon's, and any company's Boule.
|
package/dist/org.js
CHANGED
|
@@ -351,6 +351,8 @@ const VOLUME_MOUNT_PATH_MAX = 255;
|
|
|
351
351
|
const REPO = /^wtfalch\/[A-Za-z0-9._-]+$/;
|
|
352
352
|
// The env names `pushAppEnv` and `pushDatabaseUrl` set themselves.
|
|
353
353
|
const FOUNDRY_ENV_NAME = /^(APP_URL|LOG_DIR|OWNER_USER_ID|AUTH_.*|DATABASE_URL.*)$/;
|
|
354
|
+
// Set only on the colors of a blueGreen entry; any other entry may declare them.
|
|
355
|
+
const BLUE_GREEN_ENV_NAME = /^REPORTING_(COLOR|RELEASE|HEALTH_TOKEN)$/;
|
|
354
356
|
const SECRET_ENV_NAME = /_(KEY|SECRET|TOKEN|PASSWORD)$/;
|
|
355
357
|
const HELD_SECRETS = "declare it as { from: 'heldSecret' }, not a plain value";
|
|
356
358
|
/** The rules for one {@link HostingEntry}; `null` when it is valid. */
|
|
@@ -392,11 +394,19 @@ function validateHostingEntry(entry, enabled) {
|
|
|
392
394
|
const invalidVolumes = validateEntryVolumes(entry, at);
|
|
393
395
|
if (invalidVolumes)
|
|
394
396
|
return invalidVolumes;
|
|
397
|
+
const invalidBlueGreen = validateBlueGreen(entry.blueGreen, at);
|
|
398
|
+
if (invalidBlueGreen)
|
|
399
|
+
return invalidBlueGreen;
|
|
400
|
+
if (entry.blueGreen !== undefined &&
|
|
401
|
+
entry.key !== APP_ENTRY_KEY &&
|
|
402
|
+
!entry.hostnames.some((name) => name !== '*')) {
|
|
403
|
+
return `${at} blueGreen needs a public hostname: the release check reaches each color through it`;
|
|
404
|
+
}
|
|
395
405
|
for (const [name, value] of Object.entries(entry.env)) {
|
|
396
406
|
if (!name) {
|
|
397
407
|
return `${at} env needs a non-empty env var name`;
|
|
398
408
|
}
|
|
399
|
-
if (FOUNDRY_ENV_NAME.test(name)) {
|
|
409
|
+
if (FOUNDRY_ENV_NAME.test(name) || (entry.blueGreen && BLUE_GREEN_ENV_NAME.test(name))) {
|
|
400
410
|
return `${at} env '${name}' is set by foundry and cannot be declared`;
|
|
401
411
|
}
|
|
402
412
|
if (typeof value !== 'string') {
|
|
@@ -424,6 +434,33 @@ function validateHostingEntry(entry, enabled) {
|
|
|
424
434
|
}
|
|
425
435
|
return null;
|
|
426
436
|
}
|
|
437
|
+
/** `blueGreen`: steps ascending, each a whole number 1..100, the last 100; a hold of 1..1440 minutes. */
|
|
438
|
+
export function validateBlueGreen(value, at) {
|
|
439
|
+
if (value === undefined)
|
|
440
|
+
return null;
|
|
441
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
442
|
+
return `${at} blueGreen must be an object`;
|
|
443
|
+
}
|
|
444
|
+
const { steps, stepHoldMinutes } = value;
|
|
445
|
+
if (steps !== undefined) {
|
|
446
|
+
if (!Array.isArray(steps) || steps.length === 0 || steps.length > 20) {
|
|
447
|
+
return `${at} blueGreen steps must be a list of 1 to 20 percentages`;
|
|
448
|
+
}
|
|
449
|
+
if (!steps.every((n) => Number.isInteger(n) && n >= 1 && n <= 100)) {
|
|
450
|
+
return `${at} blueGreen steps must be whole numbers from 1 to 100`;
|
|
451
|
+
}
|
|
452
|
+
if (!steps.every((n, i) => i === 0 || n > steps[i - 1])) {
|
|
453
|
+
return `${at} blueGreen steps must be ascending`;
|
|
454
|
+
}
|
|
455
|
+
if (steps[steps.length - 1] !== 100)
|
|
456
|
+
return `${at} blueGreen steps must end at 100`;
|
|
457
|
+
}
|
|
458
|
+
if (stepHoldMinutes !== undefined &&
|
|
459
|
+
!(Number.isInteger(stepHoldMinutes) && stepHoldMinutes >= 1 && stepHoldMinutes <= 1440)) {
|
|
460
|
+
return `${at} blueGreen stepHoldMinutes must be a whole number from 1 to 1440`;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
427
464
|
/** `volumes`: a bounded list of named volumes at distinct absolute paths, none on the log volume's path or name,
|
|
428
465
|
* on `/app` or under a system path. */
|
|
429
466
|
function validateEntryVolumes(entry, at) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type BlueGreenApplyDeps } from './blue-green-apply.js';
|
|
2
|
+
import { type BlueGreenSubject } from './blue-green.js';
|
|
3
|
+
import type { DeployHealthRuntime } from './deploy-health.js';
|
|
4
|
+
import type { PlatformTunnelPort } from './platform-providers.js';
|
|
5
|
+
export interface ArchonBlueGreenRun {
|
|
6
|
+
subject: BlueGreenSubject;
|
|
7
|
+
/** The owner company's id: the health token's held ref is `sharedSecretRef(ownerOrgId, ...)` like an entry's. */
|
|
8
|
+
ownerOrgId: string;
|
|
9
|
+
desired: {
|
|
10
|
+
primary: string;
|
|
11
|
+
};
|
|
12
|
+
recorded(): Readonly<Record<string, string>>;
|
|
13
|
+
record(key: string, value: string): void;
|
|
14
|
+
persist(): Promise<void>;
|
|
15
|
+
coolify: BlueGreenApplyDeps['coolify'] & {
|
|
16
|
+
listServers(): Promise<Array<{
|
|
17
|
+
uuid: string;
|
|
18
|
+
is_usable: boolean;
|
|
19
|
+
is_reachable: boolean;
|
|
20
|
+
}>>;
|
|
21
|
+
findOrCreateEnvironment(projectUuid: string, name: string): Promise<{
|
|
22
|
+
environment: {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
uuid: string;
|
|
26
|
+
};
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
heldKeys: {
|
|
30
|
+
resolve(ref: string): Promise<string | null>;
|
|
31
|
+
put(ref: string, value: string): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
/** Registers a value for redaction before it is written anywhere. */
|
|
34
|
+
secret: (value: string) => void;
|
|
35
|
+
/** Archon's Coolify project, its blue app and its tunnel, all recorded by the archon step. */
|
|
36
|
+
projectUuid: string;
|
|
37
|
+
blueAppId: string;
|
|
38
|
+
tunnelId: string;
|
|
39
|
+
hostname: string;
|
|
40
|
+
tunnels: Pick<PlatformTunnelPort, 'putTunnelConfig'>;
|
|
41
|
+
runtime: DeployHealthRuntime;
|
|
42
|
+
sleep: (ms: number) => Promise<void>;
|
|
43
|
+
logDir: string;
|
|
44
|
+
isRunning: (status: string | undefined) => boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
|
|
48
|
+
* blue; green, the balancer and stage are new apps beside it on the primary server. The tunnel moves to the
|
|
49
|
+
* balancer last, only after every part of the structure is applied and running, so a first apply never has the
|
|
50
|
+
* tunnel point at a balancer that does not answer. A failure halfway leaves the apps already made, recorded by
|
|
51
|
+
* id; the structure is recorded as applied only after its last part, so the next apply repeats from there and
|
|
52
|
+
* the tunnel stays on the app until then.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyArchonBlueGreen(run: ArchonBlueGreenRun): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { sharedSecretRef } from '../org.js';
|
|
3
|
+
import { applyBlueGreenServer } from './blue-green-apply.js';
|
|
4
|
+
import { ARCHON_BG_ORIGIN_SCOPE_KEY, DEFAULT_WEIGHTS, PRIMARY_SERVER_SCOPE_KEY, bgKey, healthTokenName, lbOrigin, splitServers, } from './blue-green.js';
|
|
5
|
+
/**
|
|
6
|
+
* platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
|
|
7
|
+
* blue; green, the balancer and stage are new apps beside it on the primary server. The tunnel moves to the
|
|
8
|
+
* balancer last, only after every part of the structure is applied and running, so a first apply never has the
|
|
9
|
+
* tunnel point at a balancer that does not answer. A failure halfway leaves the apps already made, recorded by
|
|
10
|
+
* id; the structure is recorded as applied only after its last part, so the next apply repeats from there and
|
|
11
|
+
* the tunnel stays on the app until then.
|
|
12
|
+
*/
|
|
13
|
+
export async function applyArchonBlueGreen(run) {
|
|
14
|
+
const { subject } = run;
|
|
15
|
+
const id = subject.id;
|
|
16
|
+
const label = `coolify:archon-bluegreen:${id}`;
|
|
17
|
+
const { up } = splitServers((await run.coolify.listServers()).map((s) => ({
|
|
18
|
+
uuid: s.uuid,
|
|
19
|
+
usable: s.is_usable,
|
|
20
|
+
reachable: s.is_reachable,
|
|
21
|
+
})));
|
|
22
|
+
const primary = run.recorded()[PRIMARY_SERVER_SCOPE_KEY] ?? up[0];
|
|
23
|
+
if (!primary || !up.includes(primary) || primary !== run.desired.primary) {
|
|
24
|
+
throw new Error(`${label}: the plan's primary server is not the usable recorded one`);
|
|
25
|
+
}
|
|
26
|
+
if (!run.recorded()[PRIMARY_SERVER_SCOPE_KEY]) {
|
|
27
|
+
run.record(PRIMARY_SERVER_SCOPE_KEY, primary);
|
|
28
|
+
await run.persist();
|
|
29
|
+
}
|
|
30
|
+
if (!run.recorded()[bgKey.weights(id)]) {
|
|
31
|
+
run.record(bgKey.weights(id), DEFAULT_WEIGHTS);
|
|
32
|
+
await run.persist();
|
|
33
|
+
}
|
|
34
|
+
const { environment } = await run.coolify.findOrCreateEnvironment(run.projectUuid, 'production');
|
|
35
|
+
const ref = sharedSecretRef(run.ownerOrgId, healthTokenName(id));
|
|
36
|
+
const deps = {
|
|
37
|
+
coolify: run.coolify,
|
|
38
|
+
recorded: run.recorded,
|
|
39
|
+
record: run.record,
|
|
40
|
+
persist: run.persist,
|
|
41
|
+
projectUuid: run.projectUuid,
|
|
42
|
+
environment,
|
|
43
|
+
healthToken: async () => {
|
|
44
|
+
const held = await run.heldKeys.resolve(ref);
|
|
45
|
+
if (held) {
|
|
46
|
+
run.secret(held);
|
|
47
|
+
return held;
|
|
48
|
+
}
|
|
49
|
+
const minted = randomBytes(32).toString('base64url');
|
|
50
|
+
run.secret(minted);
|
|
51
|
+
await run.heldKeys.put(ref, minted);
|
|
52
|
+
return minted;
|
|
53
|
+
},
|
|
54
|
+
secret: run.secret,
|
|
55
|
+
runtime: run.runtime,
|
|
56
|
+
sleep: run.sleep,
|
|
57
|
+
label,
|
|
58
|
+
primaryBlueAppId: run.blueAppId,
|
|
59
|
+
logDir: run.logDir,
|
|
60
|
+
isRunning: run.isRunning,
|
|
61
|
+
};
|
|
62
|
+
await applyBlueGreenServer(deps, subject, { uuid: primary, primaryUuid: primary });
|
|
63
|
+
// The structure is applied and every part ran: now, and only now, the tunnel moves.
|
|
64
|
+
const origin = lbOrigin(subject);
|
|
65
|
+
if (run.recorded()[ARCHON_BG_ORIGIN_SCOPE_KEY] !== origin) {
|
|
66
|
+
await run.tunnels.putTunnelConfig(run.tunnelId, {
|
|
67
|
+
ingress: [{ hostname: run.hostname, service: origin }, { service: 'http_status:404' }],
|
|
68
|
+
});
|
|
69
|
+
run.record(ARCHON_BG_ORIGIN_SCOPE_KEY, origin);
|
|
70
|
+
await run.persist();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type BlueGreenSubject } from './blue-green.js';
|
|
2
|
+
import { ensureConnectorsOnServers } from './cloudflared-connector.js';
|
|
3
|
+
import { type DeployHealthRuntime } from './deploy-health.js';
|
|
4
|
+
import type { TunnelPort } from './edge-tunnels.js';
|
|
5
|
+
import type { CoolifyPort } from './executor.js';
|
|
6
|
+
/** The Coolify calls this module makes. */
|
|
7
|
+
type Coolify = Pick<CoolifyPort, 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'updateApplication' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'deployAndWait' | 'listStorages' | 'createPersistentStorage' | 'createFileStorage' | 'updateFileStorage' | 'requireGithubApp' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw'> & {
|
|
8
|
+
listApplicationDeployments?: CoolifyPort['listApplicationDeployments'];
|
|
9
|
+
};
|
|
10
|
+
export interface BlueGreenApplyDeps {
|
|
11
|
+
coolify: Coolify;
|
|
12
|
+
/** The ids recorded so far, and how to record one. */
|
|
13
|
+
recorded: () => Readonly<Record<string, string>>;
|
|
14
|
+
record: (key: string, value: string) => void;
|
|
15
|
+
persist: () => Promise<void>;
|
|
16
|
+
projectUuid: string;
|
|
17
|
+
environment: {
|
|
18
|
+
id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
uuid: string;
|
|
21
|
+
};
|
|
22
|
+
/** The entry's one health token: held once, handed to the env writes below and nowhere else. */
|
|
23
|
+
healthToken: () => Promise<string>;
|
|
24
|
+
/** Registers a value for redaction before it is written anywhere. */
|
|
25
|
+
secret: (value: string) => void;
|
|
26
|
+
runtime: DeployHealthRuntime;
|
|
27
|
+
sleep: (ms: number) => Promise<void>;
|
|
28
|
+
/** The step's name, for errors. */
|
|
29
|
+
label: string;
|
|
30
|
+
/** The app id of the blue copy `coolify:app` made on the primary. */
|
|
31
|
+
primaryBlueAppId: string;
|
|
32
|
+
logDir: string;
|
|
33
|
+
isRunning: (status: string | undefined) => boolean;
|
|
34
|
+
}
|
|
35
|
+
/** What writing a balancer's config needs; {@link BlueGreenApplyDeps} and the release runner both have it. */
|
|
36
|
+
export interface LbConfigDeps {
|
|
37
|
+
coolify: Pick<Coolify, 'listStorages' | 'createFileStorage' | 'updateFileStorage'>;
|
|
38
|
+
recorded: () => Readonly<Record<string, string>>;
|
|
39
|
+
record: (key: string, value: string) => void;
|
|
40
|
+
persist: () => Promise<void>;
|
|
41
|
+
label: string;
|
|
42
|
+
}
|
|
43
|
+
/** Replaces the host in a connection URL; `null` when it is not one. Never throws a value. */
|
|
44
|
+
export declare function withDatabaseHost(url: string, host: string): string | null;
|
|
45
|
+
/** `postgres://user:password@host/db` for a stage runtime role: the password is minted by the caller. */
|
|
46
|
+
export declare function runtimeDatabaseUrl(ownerUrl: string, role: string, password: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Applies one server's blue/green structure for `subject`: green (and blue, off the primary) beside the
|
|
49
|
+
* primary's blue, the balancer with the recorded weights, and on the primary the stage app. Every color
|
|
50
|
+
* gets the primary blue's env (the database host rewritten off the primary) and its three reporting
|
|
51
|
+
* values. A failure halfway leaves the apps already made, recorded by id; the next run finds them and
|
|
52
|
+
* does the rest. Nothing is recorded as applied until every part is done.
|
|
53
|
+
*/
|
|
54
|
+
export declare function applyBlueGreenServer(deps: BlueGreenApplyDeps, subject: BlueGreenSubject, server: {
|
|
55
|
+
uuid: string;
|
|
56
|
+
primaryUuid: string;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
/** Writes the balancer's config with the recorded weights and records them for this server. */
|
|
59
|
+
export declare function applyBlueGreenWeights(deps: LbConfigDeps, subject: BlueGreenSubject, server: {
|
|
60
|
+
uuid: string;
|
|
61
|
+
primaryUuid: string;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
/** A connector for the org's tunnel on each given server; the primary's is the existing one. */
|
|
64
|
+
export declare function applyBlueGreenConnectors(input: {
|
|
65
|
+
coolify: Parameters<typeof ensureConnectorsOnServers>[0]['coolify'];
|
|
66
|
+
tunnels: Pick<TunnelPort, 'getTunnelToken'>;
|
|
67
|
+
tunnelId: string;
|
|
68
|
+
baseAppName: string;
|
|
69
|
+
baseScopeKey: string;
|
|
70
|
+
primaryUuid: string;
|
|
71
|
+
serverUuids: readonly string[];
|
|
72
|
+
projectUuid: string;
|
|
73
|
+
environment: {
|
|
74
|
+
id: number;
|
|
75
|
+
name: string;
|
|
76
|
+
uuid: string;
|
|
77
|
+
};
|
|
78
|
+
recorded: () => Readonly<Record<string, string>>;
|
|
79
|
+
record: (key: string, value: string) => void;
|
|
80
|
+
persist: () => Promise<void>;
|
|
81
|
+
}): Promise<void>;
|
|
82
|
+
export {};
|