@wtfalch/foundry 0.62.0 → 0.63.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/declare.js +4 -1
- package/dist/bootstrap/versions.d.ts +2 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -1
- package/dist/migrations/0067_requested_changes_kosmos.sql +6 -0
- package/dist/migrations/0068_platform_registry.sql +17 -0
- package/dist/native-build.d.ts +7 -27
- package/dist/native-build.js +7 -27
- package/dist/plan/archon-blue-green.d.ts +5 -0
- package/dist/plan/archon-blue-green.js +2 -0
- package/dist/plan/blue-green-apply.d.ts +5 -0
- package/dist/plan/blue-green-apply.js +197 -22
- package/dist/plan/blue-green.d.ts +61 -2
- package/dist/plan/blue-green.js +80 -6
- package/dist/plan/boule.d.ts +1 -1
- package/dist/plan/boule.js +2 -0
- package/dist/plan/broker-entries.d.ts +9 -0
- package/dist/plan/broker-entries.js +29 -0
- package/dist/plan/diff.d.ts +5 -0
- package/dist/plan/diff.js +4 -0
- package/dist/plan/edge-access.d.ts +2 -3
- package/dist/plan/edge-access.js +7 -0
- package/dist/plan/executor.d.ts +71 -1
- package/dist/plan/executor.js +426 -24
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/jobs-scheduler-http.d.ts +2 -0
- package/dist/plan/jobs-scheduler-http.js +11 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +51 -1
- package/dist/plan/plan-apply.js +298 -25
- package/dist/plan/platform-diff.d.ts +6 -0
- package/dist/plan/platform-diff.js +74 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-providers.d.ts +46 -1
- package/dist/plan/platform-providers.js +156 -1
- package/dist/plan/platform-registry-keys.d.ts +58 -0
- package/dist/plan/platform-registry-keys.js +69 -0
- package/dist/plan/platform-registry.d.ts +67 -0
- package/dist/plan/platform-registry.js +298 -0
- package/dist/plan/release-host.d.ts +31 -2
- package/dist/plan/release-host.js +114 -6
- package/dist/plan/stage-copy-host.d.ts +104 -0
- package/dist/plan/stage-copy-host.js +259 -0
- package/dist/plan/stage-copy.js +44 -0
- package/dist/plan/stage-env.d.ts +9 -0
- package/dist/plan/stage-env.js +38 -0
- package/dist/plan/stage-guards.d.ts +57 -1
- package/dist/plan/stage-guards.js +106 -0
- package/dist/plan/stage-isolation.js +56 -0
- package/dist/plan/stage-services.d.ts +93 -0
- package/dist/plan/stage-services.js +153 -0
- package/dist/plan/stage-signin.d.ts +152 -0
- package/dist/plan/stage-signin.js +133 -0
- package/dist/plan/stage-sink.d.ts +2 -0
- package/dist/plan/stage-sink.js +6 -1
- package/dist/platform.d.ts +7 -1
- package/dist/platform.js +1 -0
- package/dist/release/decide.d.ts +12 -0
- package/dist/release/decide.js +37 -2
- package/dist/release/run.d.ts +71 -1
- package/dist/release/run.js +92 -8
- package/dist/release/state.d.ts +14 -1
- package/dist/release/state.js +43 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/release/types.js +2 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +11 -0
- package/dist/store/postgres.js +8 -0
- package/dist/store/schema.d.ts +51 -0
- package/dist/store/schema.js +4 -0
- package/package.json +3 -2
- package/versions.digests.json +1 -0
- package/versions.json +1 -0
|
@@ -53,6 +53,7 @@ const SERVICES_ENABLED_BY_DEFAULT = new Set([
|
|
|
53
53
|
'archon',
|
|
54
54
|
'edge',
|
|
55
55
|
'hc',
|
|
56
|
+
'registry',
|
|
56
57
|
]);
|
|
57
58
|
/**
|
|
58
59
|
* The services every company deploys its own copy of. Their platform entries
|
|
@@ -153,7 +154,9 @@ export function declaredPlatformFor(apex, serverIpv4) {
|
|
|
153
154
|
? DEFAULT_MAIL_IMAGE_TAG
|
|
154
155
|
: key === 'hc'
|
|
155
156
|
? DEFAULT_HC_IMAGE_TAG
|
|
156
|
-
:
|
|
157
|
+
: key === 'registry'
|
|
158
|
+
? VERSIONS.registry
|
|
159
|
+
: DEFAULT_GIT_REF,
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
162
|
return {
|
|
@@ -12,6 +12,8 @@ export interface PinnedVersions {
|
|
|
12
12
|
cloudflared: string;
|
|
13
13
|
stalwart: string;
|
|
14
14
|
hc: string;
|
|
15
|
+
/** The platform's image registry (`registry:3`, Docker's `distribution`). */
|
|
16
|
+
registry: string;
|
|
15
17
|
/** The mail relay's base image (an exact Alpine release); Postfix comes from its package repository. */
|
|
16
18
|
alpine: string;
|
|
17
19
|
/** A static busybox, copied into the MAS image so its health check has an HTTP client. */
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export type { SlugRejectionReason, SlugRejection } from './slug.js';
|
|
|
15
15
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
16
16
|
export type { Checkpoint, LastApplied, AppliedState, DestroyRecord, DestroyStepRecord, } from './state.js';
|
|
17
17
|
export type { PlanStep, UnmanagedItem, PlanNote, Plan, VersionChangeItem } from './plan.js';
|
|
18
|
-
export type { NativeBuildDispatchDeps
|
|
19
|
-
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild,
|
|
18
|
+
export type { NativeBuildDispatchDeps } from './native-build.js';
|
|
19
|
+
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, } from './native-build.js';
|
|
20
20
|
export type { AppliedVersion, AppliedVersionsStore, DeclaredOrgStore, HeldLease, MutationLease, PlatformStore, RequestedChangeStore, StaleUpdate, UpdateOptions, } from './store.js';
|
|
21
21
|
export { StaleLeaseError } from './store.js';
|
|
22
22
|
export type { PlanApply, DestroyProgress } from './apply.js';
|
|
@@ -40,6 +40,8 @@ export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError
|
|
|
40
40
|
export type { StageCopyInput, StageCopyPort, StageCopyProduction, StageCopyRunRecord, UndoStageCopyInput, } from './plan/index.js';
|
|
41
41
|
export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './plan/index.js';
|
|
42
42
|
export type { Runner, StageCopyPortDeps } from './plan/index.js';
|
|
43
|
+
export type { PlatformRegistryDeps, PlatformRegistryRuntime } from './plan/index.js';
|
|
44
|
+
export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './plan/index.js';
|
|
43
45
|
export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
|
|
44
46
|
export type { OrgPostgresMigrationConfirmation } from './plan/index.js';
|
|
45
47
|
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
@@ -59,6 +61,8 @@ export type { PlanOwnSfuOptions, SfuAppDeps, SfuAppInput } from './plan/index.js
|
|
|
59
61
|
export { OWN_SFU_BLOCKED_KEY, OWN_SFU_FIREWALL_KEY, planOwnSfu, provisionSfuApp, sfuConfig, sfuCompose, } from './plan/index.js';
|
|
60
62
|
export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, KeysRegistrarPort, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, LoginHostDiscoveryPort, } from './plan/index.js';
|
|
61
63
|
export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
|
|
64
|
+
export type { StageProbePort, ReleasePreviewMembers } from './plan/stage-guards.js';
|
|
65
|
+
export type { StageCopyHostConfig } from './plan/stage-copy-host.js';
|
|
62
66
|
export { NO_DOMAIN_ZONE, entryMachinePaths, ownHostnames, privatePageGateKey, workspaceHost, } from './plan/index.js';
|
|
63
67
|
export type { PrivatePageScope } from './plan/index.js';
|
|
64
68
|
export type { HostedLabelRefusal } from './hosted-label.js';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export const PINNED_VERSIONS = VERSIONS;
|
|
|
10
10
|
export { buildJsonSchema, parseBuildJson, BuildJsonError } from './build-json.js';
|
|
11
11
|
export { REQUESTABLE_FIELDS, classifyChange, RequestedChangeError } from './requested-changes.js';
|
|
12
12
|
export { validateSlug, slugFromName, RESERVED_SLUGS } from './slug.js';
|
|
13
|
-
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild,
|
|
13
|
+
export { NATIVE_BUILD_REPO, NATIVE_BUILD_WORKFLOW_FILE, NATIVE_BUILD_WORKFLOW_REF, NATIVE_BUILD_DISPATCH_TIMEOUT_MS, NATIVE_BUILD_KEY_ENV, NATIVE_BUILD_KEY_BYTES, NativeBuildDispatchError, dispatchNativeBuild, } from './native-build.js';
|
|
14
14
|
export { StaleLeaseError } from './store.js';
|
|
15
15
|
export { createPostgresAppliedVersionsStore, createPostgresDeclaredOrgStore, createPostgresPlatformStore, createPostgresRequestedChangeStore, DeclaredOrgError, PlatformError, } from './store/postgres.js';
|
|
16
16
|
export { createPostgresAuditLog } from './store/audit.js';
|
|
@@ -24,6 +24,7 @@ export { DumpError, dumpNow } from './plan/index.js';
|
|
|
24
24
|
export { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_IMAGE_REF, stageMailEnv, stageSinkAppName, } from './plan/index.js';
|
|
25
25
|
export { STAGE_COPY_EXCLUDED_TABLES, StageCopyFailedError, StageCopyRefusedError, stageCopy, tableExcluded, undoStageCopy, } from './plan/index.js';
|
|
26
26
|
export { StageRestoreError, StageRestoreRefusedError, createStageCopyPort, execRunner, } from './plan/index.js';
|
|
27
|
+
export { PLATFORM_REGISTRY_PULL_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_PASSWORD_REF, PLATFORM_REGISTRY_PUSH_USER, platformRegistryPushCredential, } from './plan/index.js';
|
|
27
28
|
export { InvalidMigrationConfirmationError, ORG_POSTGRES_MIGRATION_CONFIRMED_SCOPE_KEY, OrgPostgresMigrationBackupMissingError, OrgPostgresMigrationNotConfirmedError, } from './plan/index.js';
|
|
28
29
|
export { NetworkMoveBackupMissingError, UpgradeBackupMissingError } from './plan/index.js';
|
|
29
30
|
export { createEdgeAccess, createEdgeDns, edgeDnsRecordOwners, createEdgeRateLimits, createEdgeTunnels, createEdgeWorkers, createEdgeZones, } from './plan/index.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- @wtfalch/foundry: a company's Boule can request `kosmos` (turn `org.kosmos.enabled` on or off).
|
|
2
|
+
alter table foundry_requested_changes
|
|
3
|
+
drop constraint if exists foundry_requested_changes_field_check;
|
|
4
|
+
alter table foundry_requested_changes
|
|
5
|
+
add constraint foundry_requested_changes_field_check
|
|
6
|
+
check (field in ('calls', 'customers', 'storageLimit', 'aiLimit', 'domain', 'mailTier', 'app', 'build', 'bouleApps', 'kosmos'));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- @wtfalch/foundry: the platform's image registry (`registry:3`), declared with the rest of the
|
|
2
|
+
-- platform. Mirrors `src/store/schema.ts`'s `foundryDeclaredPlatform` additions. Off for the
|
|
3
|
+
-- platform that already exists: foundry deploys nothing new until it is declared on. Backfills
|
|
4
|
+
-- `registry_hostname` the way `0047_platform_coolify.sql` backfilled `coolify_hostname`: the
|
|
5
|
+
-- column must not be empty (`src/platform.ts`'s `validatePlatform`).
|
|
6
|
+
|
|
7
|
+
alter table foundry_declared_platform
|
|
8
|
+
add column if not exists registry_enabled boolean not null default false,
|
|
9
|
+
add column if not exists registry_hostname text,
|
|
10
|
+
add column if not exists registry_git_ref text not null default '';
|
|
11
|
+
|
|
12
|
+
update foundry_declared_platform
|
|
13
|
+
set registry_hostname = 'registry.' || apex
|
|
14
|
+
where registry_hostname is null;
|
|
15
|
+
|
|
16
|
+
alter table foundry_declared_platform
|
|
17
|
+
alter column registry_hostname set not null;
|
package/dist/native-build.d.ts
CHANGED
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
* workflow, and the key Boule presents when it asks the host (Archon) to
|
|
8
8
|
* dispatch it.
|
|
9
9
|
*
|
|
10
|
-
* Neither function here is wired into `plan/diff.ts` or `plan/executor.ts`:
|
|
11
10
|
* `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
|
|
12
|
-
* route, not foundry's apply loop
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* route, not foundry's apply loop. The key itself is minted by
|
|
12
|
+
* `plan/executor.ts`'s `coolifyBoule` (`ensureNativeBuildKey`), reusing the
|
|
13
|
+
* same `mintIfAbsent` path every other per-company Boule secret goes
|
|
14
|
+
* through (foundry#970) -- this file keeps only the env name and the byte
|
|
15
|
+
* length, not a second mint function (foundry#957 review note 2 and
|
|
16
|
+
* thermo note 1: the earlier `mintNativeBuildKey` was a third, buggy copy
|
|
17
|
+
* of that same check).
|
|
19
18
|
*/
|
|
20
19
|
export declare const NATIVE_BUILD_REPO = "wtfalch/shell";
|
|
21
20
|
export declare const NATIVE_BUILD_WORKFLOW_FILE = "build-app.yml";
|
|
@@ -49,22 +48,3 @@ export declare function dispatchNativeBuild(deps: NativeBuildDispatchDeps, run:
|
|
|
49
48
|
/** The env name Boule presents this key under, to Archon's native-build route. */
|
|
50
49
|
export declare const NATIVE_BUILD_KEY_ENV = "NATIVE_BUILD_KEY";
|
|
51
50
|
export declare const NATIVE_BUILD_KEY_BYTES = 32;
|
|
52
|
-
/** The one thing {@link mintNativeBuildKey} needs from Coolify: a live app's env, to check whether it already has a key. */
|
|
53
|
-
export interface NativeBuildKeyEnvPort {
|
|
54
|
-
listEnvs(appId: string): Promise<ReadonlyArray<{
|
|
55
|
-
key: string;
|
|
56
|
-
value: string;
|
|
57
|
-
is_preview: boolean;
|
|
58
|
-
}>>;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Mints the per-company key Boule presents to Archon's native-build route
|
|
62
|
-
* (estate#262), the same way every other per-company key in this codebase is
|
|
63
|
-
* minted (estate#207): a random value, written to Boule's env once and never
|
|
64
|
-
* rotated, kept unchanged on every later apply once it is there. Returns
|
|
65
|
-
* `null` when Boule's env already holds one -- the caller then writes
|
|
66
|
-
* nothing, so the live value stands. `onSecret`, when given, is told the
|
|
67
|
-
* minted value so the caller can keep it out of its own logs and error
|
|
68
|
-
* messages.
|
|
69
|
-
*/
|
|
70
|
-
export declare function mintNativeBuildKey(coolify: NativeBuildKeyEnvPort, appId: string, onSecret?: (value: string) => void): Promise<string | null>;
|
package/dist/native-build.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
1
|
/**
|
|
3
2
|
* `wtfalch/shell`'s `build-app.yml`: the one GitHub Actions workflow that
|
|
4
3
|
* builds and signs a company's native app shell (estate
|
|
@@ -8,15 +7,14 @@ import { randomBytes } from 'node:crypto';
|
|
|
8
7
|
* workflow, and the key Boule presents when it asks the host (Archon) to
|
|
9
8
|
* dispatch it.
|
|
10
9
|
*
|
|
11
|
-
* Neither function here is wired into `plan/diff.ts` or `plan/executor.ts`:
|
|
12
10
|
* `dispatchNativeBuild` runs inside Archon's own `/api/native/builds`
|
|
13
|
-
* route, not foundry's apply loop
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
11
|
+
* route, not foundry's apply loop. The key itself is minted by
|
|
12
|
+
* `plan/executor.ts`'s `coolifyBoule` (`ensureNativeBuildKey`), reusing the
|
|
13
|
+
* same `mintIfAbsent` path every other per-company Boule secret goes
|
|
14
|
+
* through (foundry#970) -- this file keeps only the env name and the byte
|
|
15
|
+
* length, not a second mint function (foundry#957 review note 2 and
|
|
16
|
+
* thermo note 1: the earlier `mintNativeBuildKey` was a third, buggy copy
|
|
17
|
+
* of that same check).
|
|
20
18
|
*/
|
|
21
19
|
export const NATIVE_BUILD_REPO = 'wtfalch/shell';
|
|
22
20
|
export const NATIVE_BUILD_WORKFLOW_FILE = 'build-app.yml';
|
|
@@ -72,21 +70,3 @@ export async function dispatchNativeBuild(deps, run) {
|
|
|
72
70
|
/** The env name Boule presents this key under, to Archon's native-build route. */
|
|
73
71
|
export const NATIVE_BUILD_KEY_ENV = 'NATIVE_BUILD_KEY';
|
|
74
72
|
export const NATIVE_BUILD_KEY_BYTES = 32;
|
|
75
|
-
/**
|
|
76
|
-
* Mints the per-company key Boule presents to Archon's native-build route
|
|
77
|
-
* (estate#262), the same way every other per-company key in this codebase is
|
|
78
|
-
* minted (estate#207): a random value, written to Boule's env once and never
|
|
79
|
-
* rotated, kept unchanged on every later apply once it is there. Returns
|
|
80
|
-
* `null` when Boule's env already holds one -- the caller then writes
|
|
81
|
-
* nothing, so the live value stands. `onSecret`, when given, is told the
|
|
82
|
-
* minted value so the caller can keep it out of its own logs and error
|
|
83
|
-
* messages.
|
|
84
|
-
*/
|
|
85
|
-
export async function mintNativeBuildKey(coolify, appId, onSecret) {
|
|
86
|
-
const envs = await coolify.listEnvs(appId);
|
|
87
|
-
if (envs.some((e) => e.key === NATIVE_BUILD_KEY_ENV && !e.is_preview))
|
|
88
|
-
return null;
|
|
89
|
-
const value = randomBytes(NATIVE_BUILD_KEY_BYTES).toString('base64url');
|
|
90
|
-
onSecret?.(value);
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
@@ -2,6 +2,7 @@ import { type BlueGreenApplyDeps } from './blue-green-apply.js';
|
|
|
2
2
|
import { type BlueGreenSubject } from './blue-green.js';
|
|
3
3
|
import type { DeployHealthRuntime } from './deploy-health.js';
|
|
4
4
|
import type { PlatformTunnelPort } from './platform-providers.js';
|
|
5
|
+
import type { StageGuardDeps } from './stage-guards.js';
|
|
5
6
|
export interface ArchonBlueGreenRun {
|
|
6
7
|
subject: BlueGreenSubject;
|
|
7
8
|
/** The owner company's id: the health token's held ref is `sharedSecretRef(ownerOrgId, ...)` like an entry's. */
|
|
@@ -42,6 +43,10 @@ export interface ArchonBlueGreenRun {
|
|
|
42
43
|
sleep: (ms: number) => Promise<void>;
|
|
43
44
|
logDir: string;
|
|
44
45
|
isRunning: (status: string | undefined) => boolean;
|
|
46
|
+
/** Stage's probe port and Access gate, the same guards an entry's stage gets. Absent: the guards read "not run". */
|
|
47
|
+
stageGuards?: StageGuardDeps;
|
|
48
|
+
/** The guard names the plan counted (`desired.guards`); they go into the applied hash. */
|
|
49
|
+
guardInputs?: readonly string[];
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* platform-archon on the blue/green path (`platform.archon.blueGreen`): the archon app keeps its name and is
|
|
@@ -70,6 +70,8 @@ export async function applyArchonBlueGreen(run) {
|
|
|
70
70
|
generate: () => randomBytes(32).toString('hex'),
|
|
71
71
|
secret: run.secret,
|
|
72
72
|
}),
|
|
73
|
+
...(run.stageGuards ? { stageGuards: run.stageGuards } : {}),
|
|
74
|
+
...(run.guardInputs ? { guardInputs: run.guardInputs } : {}),
|
|
73
75
|
actionsKey: async () => {
|
|
74
76
|
const key = await mintActionsKey({
|
|
75
77
|
id,
|
|
@@ -54,6 +54,11 @@ export interface BlueGreenApplyDeps {
|
|
|
54
54
|
* guard unrecorded ("not run", never passed), so `stageGuardsReady` stays false and the copy does not start.
|
|
55
55
|
*/
|
|
56
56
|
stageGuards?: StageGuardDeps;
|
|
57
|
+
/**
|
|
58
|
+
* The guard names the plan counted ({@link stageGuardInputs}, from `desired.guards`). They go into the
|
|
59
|
+
* applied structure hash, so the next plan sees the same hash until a guard becomes possible.
|
|
60
|
+
*/
|
|
61
|
+
guardInputs?: readonly string[];
|
|
57
62
|
/**
|
|
58
63
|
* The company the stage mail sink belongs to (its name is `<slug>-stage-sink`). Absent: no sink is made and
|
|
59
64
|
* stage holds no mail env at all, so the copy stays refused.
|
|
@@ -10,6 +10,8 @@ import { remoteDatabaseAddress, remoteDatabaseHost } from './remote-database.js'
|
|
|
10
10
|
import { STAGE_SIGN_IN_NAMES, buildStageEnv } from './stage-env.js';
|
|
11
11
|
import { buildStageProbes, runStageProbes, stageProbesRecord, } from './stage-guards.js';
|
|
12
12
|
import { assertStageIsolated, envClass, stageIsolationHash, } from './stage-isolation.js';
|
|
13
|
+
import { StageServiceError, ensureStageServices } from './stage-services.js';
|
|
14
|
+
import { STAGE_SIGNIN_NONE, StageSignInError, ensureStagePublicClient, ensureStageSignIn, } from './stage-signin.js';
|
|
13
15
|
import { STAGE_SINK_APPLIED_SCOPE_KEY, STAGE_SINK_APP_SCOPE_KEY, STAGE_SINK_HTTP_PORT, STAGE_SINK_IMAGE_TAG, STAGE_SINK_REPOSITORY, stageMailEnv, stageSinkAppName, } from './stage-sink.js';
|
|
14
16
|
import { readWorkspaceCommit, wsKey } from './workspace.js';
|
|
15
17
|
/** How long one Coolify call of the sink may take. */
|
|
@@ -95,7 +97,10 @@ export async function applyBlueGreenServer(deps, subject, server) {
|
|
|
95
97
|
await deps.persist();
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
|
|
100
|
+
deps.record(bgKey.applied(id, server.uuid), structureHash(subject, {
|
|
101
|
+
primary: isPrimary,
|
|
102
|
+
...(deps.guardInputs ? { guards: deps.guardInputs } : {}),
|
|
103
|
+
}));
|
|
99
104
|
await deps.persist();
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
@@ -481,30 +486,92 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
481
486
|
const stageHas = new Map((await coolify.listEnvs(appId)).filter((e) => !e.is_preview).map((e) => [e.key, e.value]));
|
|
482
487
|
const mailChanged = [...mail, ...signInEnv].some((e) => stageHas.get(e.key) !== e.value);
|
|
483
488
|
stageEnv.push(...mail);
|
|
484
|
-
const
|
|
489
|
+
const stageSurfaceOf = () => ({
|
|
485
490
|
env: stageEnv.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
486
491
|
resources: stageResources(subject),
|
|
492
|
+
});
|
|
493
|
+
const prodSurface = {
|
|
494
|
+
env: [...prodEnv, ...greenEnv]
|
|
495
|
+
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
496
|
+
.map((e) => ({ name: e.key, class: envClass(e.key), value: e.value })),
|
|
497
|
+
resources: prodResources(subject),
|
|
498
|
+
internalHosts: Object.values(prodResources(subject)),
|
|
487
499
|
};
|
|
488
|
-
const
|
|
500
|
+
const sinkMail = Object.fromEntries(mail.map((e) => [e.key, e.value]));
|
|
501
|
+
const signInIsolation = deps.signIn
|
|
502
|
+
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
503
|
+
: {};
|
|
504
|
+
const isolationViolations = (surface) => assertStageIsolated({
|
|
489
505
|
kind,
|
|
490
|
-
prod:
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
resources: prodResources(subject),
|
|
495
|
-
internalHosts: Object.values(prodResources(subject)),
|
|
496
|
-
},
|
|
497
|
-
stage: stageSurface,
|
|
498
|
-
sinkMail: Object.fromEntries(mail.map((e) => [e.key, e.value])),
|
|
499
|
-
...(deps.signIn
|
|
500
|
-
? { signIn: { customers: deps.signIn.customers, live: deps.signIn.live } }
|
|
501
|
-
: {}),
|
|
506
|
+
prod: prodSurface,
|
|
507
|
+
stage: surface,
|
|
508
|
+
sinkMail,
|
|
509
|
+
...signInIsolation,
|
|
502
510
|
});
|
|
511
|
+
// The env written this run, and the env stage already holds (put there by an earlier apply, an older
|
|
512
|
+
// builder or a hand): a forbidden name or a production value already on the live stage app is refused too.
|
|
513
|
+
// A name this run writes again is replaced, so only the other live names are checked here.
|
|
514
|
+
const written = new Set(stageEnv.map((e) => e.key));
|
|
515
|
+
const liveStage = {
|
|
516
|
+
env: [...stageHas]
|
|
517
|
+
.filter(([name, value]) => value !== undefined && !written.has(name))
|
|
518
|
+
.map(([name, value]) => ({ name, class: envClass(name), value })),
|
|
519
|
+
resources: {},
|
|
520
|
+
};
|
|
521
|
+
let stageSurface = stageSurfaceOf();
|
|
522
|
+
const violations = [
|
|
523
|
+
...new Set([
|
|
524
|
+
...isolationViolations(stageSurface),
|
|
525
|
+
...assertStageIsolated({
|
|
526
|
+
kind,
|
|
527
|
+
prod: prodSurface,
|
|
528
|
+
stage: liveStage,
|
|
529
|
+
sinkMail,
|
|
530
|
+
...signInIsolation,
|
|
531
|
+
}),
|
|
532
|
+
]),
|
|
533
|
+
].sort();
|
|
503
534
|
if (violations.length > 0) {
|
|
504
535
|
// Stage fails closed for itself: nothing is written to it, production's structure goes on.
|
|
505
536
|
await stageFailed(deps, subject, `blocked:${violations.join(',')}`);
|
|
506
537
|
return;
|
|
507
538
|
}
|
|
539
|
+
// Stage's own files, ai and jobs organisations and keys, for the services production's app uses. They are made
|
|
540
|
+
// before the probes, which need the keys; nothing is written to the stage app until the probes were rejected.
|
|
541
|
+
const prodValues = new Map(prodEnv
|
|
542
|
+
.filter((e) => !e.is_preview && e.value !== undefined)
|
|
543
|
+
.map((e) => [e.key, e.value]));
|
|
544
|
+
let servicesRecord;
|
|
545
|
+
try {
|
|
546
|
+
const made = await ensureStageServices({
|
|
547
|
+
id: subject.id,
|
|
548
|
+
kind,
|
|
549
|
+
prod: prodValues,
|
|
550
|
+
port: deps.stageGuards?.services,
|
|
551
|
+
recorded: deps.recorded,
|
|
552
|
+
record: deps.record,
|
|
553
|
+
persist: deps.persist,
|
|
554
|
+
secret: deps.secret,
|
|
555
|
+
log: (line) => console.info(`${deps.label}: ${line}`),
|
|
556
|
+
});
|
|
557
|
+
stageEnv.push(...made.env);
|
|
558
|
+
servicesRecord = made.record;
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
// A StageServiceError holds ids and refs only; any other cause can echo a credential.
|
|
562
|
+
if (error instanceof StageServiceError)
|
|
563
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
564
|
+
await stageFailed(deps, subject, 'blocked:services', error);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (servicesRecord !== 'none') {
|
|
568
|
+
stageSurface = stageSurfaceOf();
|
|
569
|
+
const withServices = isolationViolations(stageSurface);
|
|
570
|
+
if (withServices.length > 0) {
|
|
571
|
+
await stageFailed(deps, subject, `blocked:${withServices.join(',')}`);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
508
575
|
// Stage's credentials are tried against production before they are written; one that production accepts
|
|
509
576
|
// blocks stage (nothing is written to it), and what was tried is recorded by name so the copy slice sees it.
|
|
510
577
|
const stageValues = new Map((await coolify.listEnvs(appId))
|
|
@@ -512,7 +579,7 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
512
579
|
.map((e) => [e.key, e.value]));
|
|
513
580
|
for (const e of stageEnv)
|
|
514
581
|
stageValues.set(e.key, e.value);
|
|
515
|
-
|
|
582
|
+
let isolationHash = stageIsolationHash(stageSurface);
|
|
516
583
|
let probeResult;
|
|
517
584
|
try {
|
|
518
585
|
probeResult = await runStageProbes(buildStageProbes({
|
|
@@ -524,9 +591,12 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
524
591
|
? { liveCustomersOrgId: deps.signIn.live.customersOrgId }
|
|
525
592
|
: {}),
|
|
526
593
|
...(deps.signIn ? { signInRedirectUri: deps.signIn.redirectUri } : {}),
|
|
527
|
-
prod: new Map(
|
|
528
|
-
|
|
529
|
-
|
|
594
|
+
prod: new Map([
|
|
595
|
+
...prodValues,
|
|
596
|
+
...(deps.stageGuards?.services?.prodJobsProjectId
|
|
597
|
+
? [['JOBS_PROJECT_ID', deps.stageGuards.services.prodJobsProjectId]]
|
|
598
|
+
: []),
|
|
599
|
+
]),
|
|
530
600
|
}));
|
|
531
601
|
}
|
|
532
602
|
catch (error) {
|
|
@@ -534,7 +604,7 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
534
604
|
await stageFailed(deps, subject, 'blocked:database', error);
|
|
535
605
|
return;
|
|
536
606
|
}
|
|
537
|
-
|
|
607
|
+
let probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
538
608
|
if (probeResult.failed.length > 0) {
|
|
539
609
|
deps.record(bgKey.stageProbes(subject.id), probes.value);
|
|
540
610
|
await stageFailed(deps, subject, `blocked:${probeResult.failed.join(',')}`);
|
|
@@ -544,9 +614,108 @@ export async function ensureStage(deps, subject, serverUuid) {
|
|
|
544
614
|
await stageFailed(deps, subject, 'blocked:gate');
|
|
545
615
|
return;
|
|
546
616
|
}
|
|
617
|
+
// Stage's own sign-in: made only now, after the isolation check, the probes and the gate. Its credentials are
|
|
618
|
+
// added to the env and the isolation check runs once more over the whole of it.
|
|
619
|
+
const signIn = deps.stageGuards?.signIn;
|
|
620
|
+
let signInGuard;
|
|
621
|
+
if (signIn?.required === true && signIn.public === true) {
|
|
622
|
+
// A static SPA (Agora): a client and nothing secret. Its config is built from production's with the stage
|
|
623
|
+
// client id, then the isolation check runs once more over the whole env.
|
|
624
|
+
try {
|
|
625
|
+
const made = await ensureStagePublicClient({
|
|
626
|
+
id: subject.id,
|
|
627
|
+
port: signIn,
|
|
628
|
+
recorded: deps.recorded,
|
|
629
|
+
record: deps.record,
|
|
630
|
+
persist: deps.persist,
|
|
631
|
+
});
|
|
632
|
+
const rebuilt = buildStageEnv({
|
|
633
|
+
kind,
|
|
634
|
+
appUrl: stageAppUrl(subject),
|
|
635
|
+
logDir: deps.logDir,
|
|
636
|
+
database,
|
|
637
|
+
prod: prodEnv,
|
|
638
|
+
existing: [],
|
|
639
|
+
mint: () => '',
|
|
640
|
+
stageClientId: made.clientId,
|
|
641
|
+
});
|
|
642
|
+
const config = rebuilt.env.find((e) => e.key === 'AGORA_CONFIG');
|
|
643
|
+
if (config)
|
|
644
|
+
stageEnv.push(config);
|
|
645
|
+
signInGuard = made.guard;
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (error instanceof StageSignInError)
|
|
649
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
650
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
651
|
+
await stageFailed(deps, subject, 'blocked:signin', error);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
for (const e of stageEnv)
|
|
655
|
+
stageValues.set(e.key, e.value);
|
|
656
|
+
stageSurface = stageSurfaceOf();
|
|
657
|
+
const again = isolationViolations(stageSurface);
|
|
658
|
+
if (again.length > 0) {
|
|
659
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
660
|
+
await stageFailed(deps, subject, `blocked:${again.join(',')}`);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
isolationHash = stageIsolationHash(stageSurface);
|
|
664
|
+
probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
665
|
+
}
|
|
666
|
+
else if (signIn?.required === true) {
|
|
667
|
+
try {
|
|
668
|
+
const made = await ensureStageSignIn({
|
|
669
|
+
id: subject.id,
|
|
670
|
+
port: signIn,
|
|
671
|
+
recorded: deps.recorded,
|
|
672
|
+
record: deps.record,
|
|
673
|
+
persist: deps.persist,
|
|
674
|
+
secret: deps.secret,
|
|
675
|
+
generate: () => randomBytes(32).toString('base64url'),
|
|
676
|
+
sleep: deps.sleep,
|
|
677
|
+
});
|
|
678
|
+
stageEnv.push(...made.env);
|
|
679
|
+
signInGuard = made.guard;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
// A StageSignInError holds ids and refs only; any other cause can echo a credential.
|
|
683
|
+
if (error instanceof StageSignInError)
|
|
684
|
+
console.error(`${deps.label}: ${error.message}`);
|
|
685
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
686
|
+
await stageFailed(deps, subject, 'blocked:signin', error);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (!stageEnv.some((e) => e.key === 'AUTH_COOKIE_SECRET')) {
|
|
690
|
+
// A cookie secret of stage's own, kept across applies; never a copy of production's.
|
|
691
|
+
const have = stageHas.get('AUTH_COOKIE_SECRET') || randomBytes(32).toString('base64url');
|
|
692
|
+
deps.secret(have);
|
|
693
|
+
stageEnv.push({ key: 'AUTH_COOKIE_SECRET', value: have });
|
|
694
|
+
}
|
|
695
|
+
for (const e of stageEnv)
|
|
696
|
+
stageValues.set(e.key, e.value);
|
|
697
|
+
stageSurface = stageSurfaceOf();
|
|
698
|
+
const again = isolationViolations(stageSurface);
|
|
699
|
+
if (again.length > 0) {
|
|
700
|
+
deps.record(bgKey.stageSignIn(subject.id), '');
|
|
701
|
+
await stageFailed(deps, subject, `blocked:${again.join(',')}`);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
isolationHash = stageIsolationHash(stageSurface);
|
|
705
|
+
probes = stageProbesRecord(probeResult, isolationHash, stageValues);
|
|
706
|
+
}
|
|
707
|
+
else if (signIn?.required === false) {
|
|
708
|
+
signInGuard = STAGE_SIGNIN_NONE;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
console.info(`${deps.label}: stage sign-in was not set up: the host gave no sign-in for this subject, so ${bgKey.stageSignIn(subject.id)} stays unrecorded`);
|
|
712
|
+
}
|
|
547
713
|
await upsertEnv(deps, appId, stageEnv);
|
|
714
|
+
if (signInGuard)
|
|
715
|
+
deps.record(bgKey.stageSignIn(subject.id), signInGuard);
|
|
548
716
|
if (mail.length > 0)
|
|
549
717
|
deps.record(bgKey.stageSink(subject.id), 'applied');
|
|
718
|
+
deps.record(bgKey.stageServices(subject.id), servicesRecord);
|
|
550
719
|
deps.record(bgKey.stageIsolation(subject.id), isolationHash);
|
|
551
720
|
deps.record(bgKey.stageProbes(subject.id), probes.value);
|
|
552
721
|
await deps.persist();
|
|
@@ -589,8 +758,11 @@ async function ensureStageGate(deps, subject) {
|
|
|
589
758
|
if (!gate || deps.recorded()[bgKey.stageGate(subject.id)])
|
|
590
759
|
return true;
|
|
591
760
|
let id;
|
|
761
|
+
let membersHash;
|
|
592
762
|
try {
|
|
593
|
-
|
|
763
|
+
const created = await gate.create();
|
|
764
|
+
id = created.id;
|
|
765
|
+
membersHash = created.membersHash;
|
|
594
766
|
}
|
|
595
767
|
catch (error) {
|
|
596
768
|
// An Access error can echo the request, which names the people allowed in: only its name is logged.
|
|
@@ -598,6 +770,8 @@ async function ensureStageGate(deps, subject) {
|
|
|
598
770
|
return false;
|
|
599
771
|
}
|
|
600
772
|
deps.record(bgKey.stageGate(subject.id), id);
|
|
773
|
+
if (membersHash)
|
|
774
|
+
deps.record(bgKey.stageGateMembers(subject.id), membersHash);
|
|
601
775
|
await deps.persist();
|
|
602
776
|
return true;
|
|
603
777
|
}
|
|
@@ -607,7 +781,8 @@ async function stageAppId(deps, subject, name, serverUuid) {
|
|
|
607
781
|
name,
|
|
608
782
|
server: serverUuid,
|
|
609
783
|
branch: subject.branch,
|
|
610
|
-
|
|
784
|
+
// Stage deploys only when a person promotes a commit to it (`deploy_stage`); a merge never reaches it.
|
|
785
|
+
autoDeploy: false,
|
|
611
786
|
});
|
|
612
787
|
}
|
|
613
788
|
function isNotFound(error) {
|
|
@@ -29,6 +29,8 @@ export declare const bgKey: {
|
|
|
29
29
|
digest: (id: string, color: Color) => string;
|
|
30
30
|
weights: (id: string) => string;
|
|
31
31
|
stage: (id: string) => string;
|
|
32
|
+
/** The last `deploy_stage`: `<ISO time>|ok|<commit>` or `<ISO time>|failed|<step>`. */
|
|
33
|
+
stageDeploy: (id: string) => string;
|
|
32
34
|
window: (id: string) => string;
|
|
33
35
|
held: (id: string, digest: string) => string;
|
|
34
36
|
/** The weights the load balancer on `server` was last written with. */
|
|
@@ -55,8 +57,29 @@ export declare const bgKey: {
|
|
|
55
57
|
stageIsolation: (id: string) => string;
|
|
56
58
|
/** The id of the Access gate over the stage host: staff only, no bypass, no service-token rule. */
|
|
57
59
|
stageGate: (id: string) => string;
|
|
60
|
+
/** The hash of the sorted email list last written to the stage gate (owners plus the people holding `releases:preview`). */
|
|
61
|
+
stageGateMembers: (id: string) => string;
|
|
58
62
|
/** `ok:<hash>` once every apply-time probe was rejected; `notrun:<names>` or `failed:<names>` otherwise. */
|
|
59
63
|
stageProbes: (id: string) => string;
|
|
64
|
+
/** Stage's own sign-in client: its ZITADEL app id, and its client id (public, not a secret). */
|
|
65
|
+
stageClient: (id: string) => string;
|
|
66
|
+
stageClientId: (id: string) => string;
|
|
67
|
+
/** The held ref of stage's app key, recorded and persisted as soon as it is minted (no `:`: undelivered). */
|
|
68
|
+
stageSignInKey: (id: string) => string;
|
|
69
|
+
/** `{name, organizationId}` of stage's broker entry, so org destroy can remove it. No key. */
|
|
70
|
+
stageBroker: (id: string) => string;
|
|
71
|
+
/** `ok:<hash of the client, app and broker entry ids>` or `none`; the copy waits for it. */
|
|
72
|
+
stageSignIn: (id: string) => string;
|
|
73
|
+
/** `none` (production's app uses no files, ai or jobs) or `ok:<hash>` once every service it uses is held by stage or listed absent. */
|
|
74
|
+
stageServices: (id: string) => string;
|
|
75
|
+
/** `ok:<hash of the ids>` or `absent:<service>:<reason>` for one service (`files`, `ai` or `jobs`). */
|
|
76
|
+
stageService: (id: string, service: string) => string;
|
|
77
|
+
/** The id of stage's own organisation (files, ai) or project (jobs). An id, not a secret. */
|
|
78
|
+
stageServiceOrg: (id: string, service: string) => string;
|
|
79
|
+
/** The name stage's project or organisation was made under, when the service is deleted by name (jobs). */
|
|
80
|
+
stageServiceName: (id: string, service: string) => string;
|
|
81
|
+
/** `{ids, refs}` of stage's keys in that service: key ids and held refs, never a value. */
|
|
82
|
+
stageServiceKeys: (id: string, service: string) => string;
|
|
60
83
|
/** `applied` once the stage app holds the sink's mail env. The copy refuses without it. */
|
|
61
84
|
stageSink: (id: string) => string;
|
|
62
85
|
/** `<ISO time>|<dump execution id>|ok`, written by a finished copy of production into stage. */
|
|
@@ -293,9 +316,38 @@ export interface BlueGreenPlanStep {
|
|
|
293
316
|
/** A short hash of what decides the shape of a server's apps, so a change replans the server. */
|
|
294
317
|
export declare function structureHash(subject: BlueGreenSubject, input: {
|
|
295
318
|
primary: boolean;
|
|
319
|
+
guards?: readonly string[];
|
|
296
320
|
}): string;
|
|
297
|
-
/**
|
|
298
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The kind of stage app a subject has: Boule and Agora by `builtin` (a hosting entry that happens to be keyed
|
|
323
|
+
* `boule` is still a customer app), platform-archon by its id, else a customer app (kosmos included: it has no lists of its own).
|
|
324
|
+
*/
|
|
325
|
+
export declare const stageKindOf: (subject: Pick<BlueGreenSubject, "id" | "builtin">) => StageKind;
|
|
326
|
+
/** What decides which stage guards can be made. Each is a fact the planner and the executor both hold. */
|
|
327
|
+
export interface StageGuardFacts {
|
|
328
|
+
/** The host supplied the probe port. */
|
|
329
|
+
probesPort: boolean;
|
|
330
|
+
/** The host has an Access port. */
|
|
331
|
+
accessPort: boolean;
|
|
332
|
+
/** The platform's recorded Access provider. */
|
|
333
|
+
providerId?: string | null | undefined;
|
|
334
|
+
/** The domain the stage host is under. */
|
|
335
|
+
domain?: string | null | undefined;
|
|
336
|
+
/** The company's sign-in organisation, the claim the gate requires. */
|
|
337
|
+
claimValue?: string | null | undefined;
|
|
338
|
+
/** How many owner emails the gate admits. */
|
|
339
|
+
emails: number;
|
|
340
|
+
/** A `hosting.access` rule already names the stage host: no gate is made over it. */
|
|
341
|
+
hostRuled: boolean;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The stage guards that can be made now, by name. The names go into the applied structure hash, so a guard
|
|
345
|
+
* that becomes possible after the first apply changes the hash and plans the structure step once more.
|
|
346
|
+
*/
|
|
347
|
+
export declare function stageGuardInputs(facts: StageGuardFacts): string[];
|
|
348
|
+
/** The guard names as the plan's `desired.guards` carries them, and back. */
|
|
349
|
+
export declare const formatGuards: (guards: readonly string[]) => string;
|
|
350
|
+
export declare const parseGuards: (value: unknown) => string[];
|
|
299
351
|
/** The names stage can collide on. */
|
|
300
352
|
export declare function stageResources(subject: BlueGreenSubject): Record<string, string>;
|
|
301
353
|
/** The names of the subject's live parts: stage must equal none of them. */
|
|
@@ -322,6 +374,11 @@ export interface PlanBlueGreenInput {
|
|
|
322
374
|
accessProviderId?: string | null;
|
|
323
375
|
/** The company's stage mail sink is recorded and its live app is stopped or gone (read from Coolify at plan time). */
|
|
324
376
|
stageSinkDown?: boolean;
|
|
377
|
+
/** Which stage-guard ports the host has. Absent: neither, so no guard input is counted. */
|
|
378
|
+
stageGuardPorts?: {
|
|
379
|
+
probes: boolean;
|
|
380
|
+
access: boolean;
|
|
381
|
+
};
|
|
325
382
|
}
|
|
326
383
|
/**
|
|
327
384
|
* Plans the blue/green parts of an org: per usable server, one structure step (the apps, the balancer, the
|
|
@@ -371,6 +428,8 @@ export declare function planArchonBlueGreen(input: {
|
|
|
371
428
|
servers: readonly BlueGreenServer[] | undefined;
|
|
372
429
|
/** Why the executor cannot run it (no way to hold the health token), or absent. */
|
|
373
430
|
executorBlocked?: string | undefined;
|
|
431
|
+
/** The stage guards that can be made now ({@link stageGuardInputs}); they go into the applied hash. */
|
|
432
|
+
guards?: readonly string[];
|
|
374
433
|
}): {
|
|
375
434
|
step: BlueGreenPlanStep | null;
|
|
376
435
|
unmanaged: UnmanagedLine[];
|