@fabrika/provider-zerops 0.8.2 → 0.9.0-cloudflare.2
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/package.json +16 -14
- package/src/api/client.ts +6 -0
- package/src/api/types.ts +12 -0
- package/src/cli.ts +11 -0
- package/src/collaborators.ts +0 -14
- package/src/control.ts +33 -57
- package/src/execution.ts +113 -102
- package/src/installation/admin.ts +1 -1
- package/src/installation/deploy-options.ts +1 -2
- package/src/installation/deploy.ts +41 -75
- package/src/installation/generated/platform-console-schema.ts +3 -0
- package/src/installation/generated/platform-proxy-manifest.ts +21 -1
- package/src/installation/guide.md +9 -6
- package/src/installation/hosts.ts +1 -7
- package/src/installation/index.ts +0 -13
- package/src/installation/init-options.ts +2 -1
- package/src/installation/install-environment.ts +42 -106
- package/src/installation/install-options.ts +3 -1
- package/src/installation/install-secrets.ts +24 -68
- package/src/installation/install.ts +8 -4
- package/src/installation/sidecar.ts +5 -26
- package/src/installation/topology/setups.ts +5 -0
- package/src/installation/upgrade-options.ts +2 -2
- package/src/managed-env.ts +1 -174
- package/src/manifest.ts +11 -14
- package/src/namespace/claims.ts +56 -1
- package/src/namespace/errors.ts +1 -1
- package/src/namespace/manifest.ts +6 -19
- package/src/namespace/provisioning.ts +15 -3
- package/src/namespace/proxy-attempt.ts +43 -2
- package/src/namespace/proxy-reads.ts +12 -22
- package/src/namespace-cli.ts +152 -0
- package/src/plan.ts +2 -7
- package/src/provider.ts +128 -126
- package/src/proxy-manifest-carrier.ts +18 -0
- package/src/types.ts +0 -2
- package/types/src/api/client.d.ts +5 -0
- package/types/src/api/errors.d.ts +24 -0
- package/types/src/api/polling.d.ts +41 -0
- package/types/src/api/readers.d.ts +1115 -0
- package/types/src/api/service-env-limit.d.ts +17 -0
- package/types/src/api/service-env-response.d.ts +23 -0
- package/types/src/api/types.d.ts +687 -0
- package/types/src/api.d.ts +6 -0
- package/types/src/authoring.d.ts +9 -0
- package/types/src/cli-args.d.ts +19 -0
- package/types/src/cli.d.ts +13 -0
- package/types/src/collaborators.d.ts +16 -0
- package/types/src/compile.d.ts +91 -0
- package/types/src/control.d.ts +40 -0
- package/types/src/database-budget.d.ts +38 -0
- package/types/src/descriptor.d.ts +28 -0
- package/types/src/execution.d.ts +125 -0
- package/types/src/exposure.d.ts +47 -0
- package/types/src/index.d.ts +31 -0
- package/types/src/installation/admin-options.d.ts +24 -0
- package/types/src/installation/admin.d.ts +35 -0
- package/types/src/installation/checker-release.d.ts +12 -0
- package/types/src/installation/deploy-options.d.ts +37 -0
- package/types/src/installation/deploy.d.ts +136 -0
- package/types/src/installation/generated/platform-console-schema.d.ts +2 -0
- package/types/src/installation/generated/platform-proxy-manifest.d.ts +2 -0
- package/types/src/installation/hosts.d.ts +34 -0
- package/types/src/installation/index.d.ts +7 -0
- package/types/src/installation/init-options.d.ts +38 -0
- package/types/src/installation/init.d.ts +60 -0
- package/types/src/installation/install-collaborators.d.ts +24 -0
- package/types/src/installation/install-environment.d.ts +63 -0
- package/types/src/installation/install-options.d.ts +49 -0
- package/types/src/installation/install-project.d.ts +13 -0
- package/types/src/installation/install-provisioning.d.ts +58 -0
- package/types/src/installation/install-secrets.d.ts +17 -0
- package/types/src/installation/install-token.d.ts +10 -0
- package/types/src/installation/install.d.ts +23 -0
- package/types/src/installation/installation-source.d.ts +8 -0
- package/types/src/installation/log.d.ts +44 -0
- package/types/src/installation/machine-key.d.ts +19 -0
- package/types/src/installation/placement-options.d.ts +11 -0
- package/types/src/installation/platform-archive.d.ts +7 -0
- package/types/src/installation/platform-source.d.ts +8 -0
- package/types/src/installation/platform-upload.d.ts +3 -0
- package/types/src/installation/sidecar.d.ts +49 -0
- package/types/src/installation/source-options.d.ts +1 -0
- package/types/src/installation/topology/artifacts.d.ts +8 -0
- package/types/src/installation/topology/index.d.ts +5 -0
- package/types/src/installation/topology/installation-schemas.d.ts +2242 -0
- package/types/src/installation/topology/invariants.d.ts +23 -0
- package/types/src/installation/topology/setups.d.ts +59 -0
- package/types/src/installation/topology/topology.d.ts +153 -0
- package/types/src/installation/topology/validate.d.ts +11 -0
- package/types/src/installation/upgrade-options.d.ts +26 -0
- package/types/src/installation/upgrade-output.d.ts +6 -0
- package/types/src/installation/upgrade.d.ts +88 -0
- package/types/src/managed-env.d.ts +24 -0
- package/types/src/manifest.d.ts +204 -0
- package/types/src/namespace/claims.d.ts +16 -0
- package/types/src/namespace/codec.d.ts +334 -0
- package/types/src/namespace/constants.d.ts +25 -0
- package/types/src/namespace/errors.d.ts +15 -0
- package/types/src/namespace/manifest.d.ts +4 -0
- package/types/src/namespace/operator.d.ts +12 -0
- package/types/src/namespace/provisioning.d.ts +4 -0
- package/types/src/namespace/proxy-attempt.d.ts +39 -0
- package/types/src/namespace/proxy-reads.d.ts +22 -0
- package/types/src/namespace/proxy-secrets.d.ts +8 -0
- package/types/src/namespace/proxy-source.d.ts +74 -0
- package/types/src/namespace/topology.d.ts +9 -0
- package/types/src/namespace/types.d.ts +101 -0
- package/types/src/namespace/update.d.ts +6 -0
- package/types/src/namespace/validation.d.ts +12 -0
- package/types/src/namespace-cli.d.ts +2 -0
- package/types/src/namespace.d.ts +9 -0
- package/types/src/plan.d.ts +11 -0
- package/types/src/provider.d.ts +63 -0
- package/types/src/proxy-manifest-carrier.d.ts +24 -0
- package/types/src/schema.generated.d.ts +376 -0
- package/types/src/service-names.d.ts +4 -0
- package/types/src/service-references.d.ts +13 -0
- package/types/src/source/constants.d.ts +6 -0
- package/types/src/source/port.d.ts +60 -0
- package/types/src/source/types.d.ts +4 -0
- package/types/src/source/validation.d.ts +19 -0
- package/types/src/source.d.ts +2 -0
- package/types/src/types.d.ts +119 -0
- package/types/src/version-attempt.d.ts +8 -0
- package/src/installation/mcp.ts +0 -102
- package/src/installation/proxy-manifest.ts +0 -214
- package/src/installation/secrets.ts +0 -78
package/src/execution.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
// runs. Of the authored setup only `build.base`, `build.os` and the SPA's `build.buildCommands` survive; the rest of
|
|
3
3
|
// the build, the deploy gate and the whole `run` section are generated here. PURE — no network, no filesystem.
|
|
4
4
|
|
|
5
|
+
import { type ContractRecipe, contractRecipe } from '@fabrika/app/build'
|
|
5
6
|
import {
|
|
6
7
|
type ContractArtifact,
|
|
8
|
+
DEV_LAUNCHER_BOOTED_FILE,
|
|
9
|
+
DEV_LAUNCHER_CONFIG_FILE,
|
|
10
|
+
DEV_LAUNCHER_MODULE,
|
|
11
|
+
type DevLauncherConfig,
|
|
7
12
|
FABRIKA_CONTRACT_DIGEST,
|
|
8
13
|
FABRIKA_SOURCE_MAP_UPLOAD,
|
|
9
14
|
FABRIKA_SOURCE_MAP_UPLOAD_KEY,
|
|
10
15
|
type FrameworkVariableName,
|
|
11
16
|
} from '@fabrika/auth-core'
|
|
12
17
|
import type { OperationsAssetRoot, OperationsSourceMapBuildUpload } from '@fabrika/operations-contract/releases'
|
|
13
|
-
import {
|
|
18
|
+
import type { JsonValue } from '@fabrika/provider-contract'
|
|
14
19
|
import packageManifest from '../package.json'
|
|
15
20
|
import { renderYaml } from './compile'
|
|
16
21
|
import { parseZeropsDescriptor, type PreparedZeropsDescriptor, selectZeropsDescriptorSetup } from './descriptor'
|
|
@@ -29,8 +34,7 @@ export const ZEROPS_APPLICATION_RUNTIME = 'alpine/bun@1.3'
|
|
|
29
34
|
*/
|
|
30
35
|
export const FABRIKA_CHECK_VERSION: string = packageManifest.version
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
export const PLATFORM_TYPESCRIPT_VERSION = '5.9.3'
|
|
37
|
+
export { PLATFORM_TYPESCRIPT_VERSION } from '@fabrika/app/build'
|
|
34
38
|
|
|
35
39
|
/** System packages an application may add to its runtime image, installed by the generated `run.prepareCommands`. */
|
|
36
40
|
export const ZEROPS_RUNTIME_PACKAGES = ['ffmpeg', 'ghostscript', 'imagemagick', 'poppler-utils', 'vips'] as const
|
|
@@ -42,12 +46,6 @@ export interface ZeropsContractJob {
|
|
|
42
46
|
readonly schedule: string
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
/** The framework's file scan job (`@fabrika/app`'s `FILE_SCAN_JOB`), run every minute when a collection scans. */
|
|
46
|
-
export const ZEROPS_FILE_SCAN_JOB = 'fabrika-file-scan'
|
|
47
|
-
/** The framework's file sweep job (`@fabrika/app`'s `FILE_SWEEP_JOB` and its schedule), run when the contract has files. */
|
|
48
|
-
export const ZEROPS_FILE_SWEEP_JOB = 'fabrika-file-sweep'
|
|
49
|
-
const FILE_SWEEP_SCHEDULE = '41 * * * *'
|
|
50
|
-
|
|
51
49
|
/**
|
|
52
50
|
* The RAM in GB a container of a scanning application starts with: one `clamscan` over a batch peaks at about 1 GB
|
|
53
51
|
* resident while it loads its signatures (measured with ClamAV 1.4), beside the application's own process.
|
|
@@ -107,13 +105,26 @@ export interface ZeropsLockedExecution {
|
|
|
107
105
|
readonly runtimePackages: readonly ZeropsRuntimePackage[]
|
|
108
106
|
/** Where the build uploads its source maps, without the bearer; null when Control minted no upload for the run. */
|
|
109
107
|
readonly sourceMaps: OperationsSourceMapBuildUpload | null
|
|
108
|
+
/**
|
|
109
|
+
* Present for an environment of kind `dev` (ADR-0049): `run.start` runs the dev launcher instead of the serving
|
|
110
|
+
* entry. The environment's kind selects it; an application never authors it.
|
|
111
|
+
*/
|
|
112
|
+
readonly dev?: ZeropsDevExecution
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ZeropsDevExecution {
|
|
116
|
+
/** The commit the bootstrap run builds: what a new container holds before the launcher's first sync. */
|
|
117
|
+
readonly head: string
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
/** The application ids the generated scripts may name: the contract's own id rule, so a script needs no quoting. */
|
|
113
|
-
export
|
|
121
|
+
export { CONTRACT_APP_ID_PATTERN as LOCKED_APP_ID_PATTERN } from '@fabrika/app/build'
|
|
114
122
|
|
|
115
123
|
export const CONTRACT_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/
|
|
116
124
|
|
|
125
|
+
/** A literal path of unreserved characters: it is written into the generated probes unquoted by any shell. */
|
|
126
|
+
export const READINESS_PATH_PATTERN = /^(?:\/[A-Za-z0-9_~-][A-Za-z0-9._~-]*)+$/
|
|
127
|
+
|
|
117
128
|
const byName = <T extends { readonly name: string }>(left: T, right: T): number => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
|
|
118
129
|
|
|
119
130
|
/**
|
|
@@ -241,31 +252,6 @@ export function lockedAuthoredBuild(entry: { readonly [key: string]: JsonValue }
|
|
|
241
252
|
|
|
242
253
|
const SOURCE_ROOT = '/build/source'
|
|
243
254
|
const RUNTIME_ROOT = '/var/www'
|
|
244
|
-
const FABRIKA_BIN = './node_modules/.bin/fabrika'
|
|
245
|
-
const MIGRATE_SCRIPT = 'fabrika/migrate.generated.sh'
|
|
246
|
-
const VERIFY_SCRIPT = 'fabrika/verify.generated.sh'
|
|
247
|
-
const START_SCRIPT = 'fabrika/start.generated.sh'
|
|
248
|
-
const PLATFORM_TSCONFIG = 'fabrika/tsconfig.platform.json'
|
|
249
|
-
|
|
250
|
-
/** The compiler options of the platform's type check; the application's own `tsconfig.json` and its `paths` are not read. */
|
|
251
|
-
const PLATFORM_TSCONFIG_CONTENTS = JSON.stringify({
|
|
252
|
-
compilerOptions: {
|
|
253
|
-
target: 'ES2022',
|
|
254
|
-
module: 'ESNext',
|
|
255
|
-
moduleResolution: 'bundler',
|
|
256
|
-
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
|
257
|
-
types: ['bun'],
|
|
258
|
-
strict: true,
|
|
259
|
-
noUncheckedIndexedAccess: true,
|
|
260
|
-
noImplicitOverride: true,
|
|
261
|
-
forceConsistentCasingInFileNames: true,
|
|
262
|
-
skipLibCheck: true,
|
|
263
|
-
resolveJsonModule: true,
|
|
264
|
-
isolatedModules: true,
|
|
265
|
-
noEmit: true,
|
|
266
|
-
},
|
|
267
|
-
include: ['../fabrika.contract.ts', '../src', 'entry.generated.ts'],
|
|
268
|
-
})
|
|
269
255
|
|
|
270
256
|
/** One `printf` writing `lines` to `path`. Each line is single-quoted, so it may hold neither `'` nor an expansion. */
|
|
271
257
|
const writeFileCommand = (path: string, lines: readonly string[]): string => {
|
|
@@ -275,39 +261,6 @@ const writeFileCommand = (path: string, lines: readonly string[]): string => {
|
|
|
275
261
|
return `printf '%s\\n' ${lines.map((line) => `'${line}'`).join(' ')} > ${path}`
|
|
276
262
|
}
|
|
277
263
|
|
|
278
|
-
const migrateScript = (appId: string): readonly string[] => [
|
|
279
|
-
'#!/bin/sh',
|
|
280
|
-
'set -eu',
|
|
281
|
-
`cd ${RUNTIME_ROOT}`,
|
|
282
|
-
`FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db migrate`,
|
|
283
|
-
]
|
|
284
|
-
|
|
285
|
-
// The committed schema types must match the migrated schema and every generated isolation test must pass on a throwaway
|
|
286
|
-
// schema. The build has no database, so they run at container start, after the migrations, as init commands: a failed
|
|
287
|
-
// init command stops the activation, and the readiness window opens only with `run.start`, after the init commands (both
|
|
288
|
-
// verified live), so a run as long as the CRM's ~3 minutes of generated cases does not race it.
|
|
289
|
-
const verifyScript = (appId: string): readonly string[] => [
|
|
290
|
-
'#!/bin/sh',
|
|
291
|
-
'set -eu',
|
|
292
|
-
`cd ${RUNTIME_ROOT}`,
|
|
293
|
-
`FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db codegen --verify`,
|
|
294
|
-
`FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db test-isolation`,
|
|
295
|
-
]
|
|
296
|
-
|
|
297
|
-
const startScript: readonly string[] = ['#!/bin/sh', 'set -eu', `cd ${RUNTIME_ROOT}`, 'exec bun fabrika/entry.generated.ts']
|
|
298
|
-
|
|
299
|
-
/** The scripts the build writes and every container's init commands run, in this order. */
|
|
300
|
-
const initScripts = (execution: ZeropsLockedExecution): readonly { readonly path: string; readonly lines: readonly string[] }[] =>
|
|
301
|
-
execution.databaseUrl === null
|
|
302
|
-
? []
|
|
303
|
-
: [{ path: MIGRATE_SCRIPT, lines: migrateScript(execution.appId) }, { path: VERIFY_SCRIPT, lines: verifyScript(execution.appId) }]
|
|
304
|
-
|
|
305
|
-
const INSTALL = 'bun install --frozen-lockfile --ignore-scripts'
|
|
306
|
-
|
|
307
|
-
// After every check: a map is uploaded only for a build that can deploy. A failed upload never fails the build; the
|
|
308
|
-
// provider reads the state from the report line.
|
|
309
|
-
const sourceMapUpload = (release: string): string => `cd /tmp && bun x --bun -p @fabrika/check@${release} fabrika-source-maps ${SOURCE_ROOT} || true`
|
|
310
|
-
|
|
311
264
|
/**
|
|
312
265
|
* The upload's build variables: the destination and asset roots as a literal, and the bearer lifted from the service
|
|
313
266
|
* variable the deploy wrote sensitive, so the descriptor never holds it.
|
|
@@ -319,26 +272,81 @@ const sourceMapBuildVariables = (upload: OperationsSourceMapBuildUpload): Record
|
|
|
319
272
|
return { [FABRIKA_SOURCE_MAP_UPLOAD]: value, [FABRIKA_SOURCE_MAP_UPLOAD_KEY]: `\${RUNTIME_${FABRIKA_SOURCE_MAP_UPLOAD_KEY}}` }
|
|
320
273
|
}
|
|
321
274
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
275
|
+
// The readiness check is retried no sooner than every 10 s (the platform's floor), and a server is rarely listening at
|
|
276
|
+
// the first attempt, one second after `run.start`. So one attempt waits for the health endpoint itself, for less than
|
|
277
|
+
// the retry period; it passes only on a 2xx, like the `httpGet` it replaces. A dev container also passes once its launcher
|
|
278
|
+
// reported the boot, served or failed: the launcher keeps running and reports a failed migration with its remedy.
|
|
279
|
+
const readinessCommand = (path: string, dev: boolean): string => {
|
|
280
|
+
if (!READINESS_PATH_PATTERN.test(path)) throw new Error('zerops: a readiness path must be a literal path of unreserved characters')
|
|
281
|
+
const url = `http://127.0.0.1:${ZEROPS_APPLICATION_PORT}${path}`
|
|
282
|
+
const booted = dev ? `if (await Bun.file('${RUNTIME_ROOT}/${DEV_LAUNCHER_BOOTED_FILE}').exists()) process.exit(0); ` : ''
|
|
283
|
+
return `bun -e "const end = Date.now() + 9000; while (Date.now() < end) { ${booted}try { if ((await fetch('${url}', { signal: AbortSignal.timeout(2000) })).ok) process.exit(0) } catch {} await Bun.sleep(200) } process.exit(1)"`
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** `@fabrika/app/build`'s recipe at this installation's paths; the dev launcher checks in the runtime root. */
|
|
287
|
+
const recipeOf = (execution: ZeropsLockedExecution, sourceRoot: string = SOURCE_ROOT): ContractRecipe =>
|
|
288
|
+
contractRecipe({
|
|
289
|
+
appId: execution.appId,
|
|
290
|
+
checkRelease: execution.checkRelease,
|
|
291
|
+
sourceRoot,
|
|
292
|
+
runtimeRoot: RUNTIME_ROOT,
|
|
293
|
+
// The build has no database; a container's init commands run the migrations and schema checks, and the readiness
|
|
294
|
+
// window opens only with `run.start`, after them (both verified live), so the CRM's ~3 minutes does not race it.
|
|
295
|
+
// A dev container has none: its init would run before Control says whether a reset is pending, over the schema the
|
|
296
|
+
// previous container left, so its launcher migrates and verifies after that answer.
|
|
297
|
+
database: execution.databaseUrl !== null && execution.dev === undefined,
|
|
298
|
+
sourceMaps: execution.sourceMaps !== null,
|
|
299
|
+
jobs: execution.contract.jobs,
|
|
300
|
+
files: execution.contract.files === true,
|
|
301
|
+
scan: execution.contract.scan === true,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// A dev environment starts the launcher of the application's own framework release, which runs the serving entry as its
|
|
305
|
+
// child. Double quotes inside: a generated file line may hold no single quote.
|
|
306
|
+
const devStartScript: readonly string[] = [
|
|
307
|
+
'#!/bin/sh',
|
|
308
|
+
'set -eu',
|
|
309
|
+
`cd ${RUNTIME_ROOT}`,
|
|
310
|
+
`exec bun -e "await import(\\"${DEV_LAUNCHER_MODULE}\\").then((dev) => dev.runDevLauncher(process.env))"`,
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
/** One command writing `content` to `path` through base64: the content may hold quotes and expansions, the command none. */
|
|
314
|
+
const writeBase64Command = (path: string, content: string): string => {
|
|
315
|
+
const encoded = btoa(String.fromCharCode(...new TextEncoder().encode(content)))
|
|
316
|
+
return `printf '%s' '${encoded}' | base64 -d > ${path}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** What the dev launcher reads (`DEV_LAUNCHER_CONFIG_FILE`): the platform's, written after the checks. */
|
|
320
|
+
export function devLauncherConfig(execution: ZeropsLockedExecution, authoredSpa: readonly string[]): DevLauncherConfig {
|
|
321
|
+
const dev = execution.dev
|
|
322
|
+
if (dev === undefined) throw new Error('zerops: a dev launcher configuration needs a dev environment')
|
|
323
|
+
if (!/^[0-9a-f]{40}$/.test(dev.head)) throw new Error('zerops: a dev environment builds an exact commit')
|
|
324
|
+
const recipe = recipeOf(execution)
|
|
325
|
+
return {
|
|
326
|
+
format: 1,
|
|
327
|
+
appId: execution.appId,
|
|
328
|
+
head: dev.head,
|
|
329
|
+
port: ZEROPS_APPLICATION_PORT,
|
|
330
|
+
healthPath: execution.contract.readinessPath,
|
|
331
|
+
serve: recipe.serve,
|
|
332
|
+
migrate: recipe.migrate,
|
|
333
|
+
spaBuild: authoredSpa,
|
|
334
|
+
// The build's check chain, in its order, on what the container holds.
|
|
335
|
+
checks: recipeOf(execution, RUNTIME_ROOT).checks,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The generated build commands, in order; the authored SPA commands run before all of them. */
|
|
340
|
+
export function generatedBuildCommands(execution: ZeropsLockedExecution, authoredSpa: readonly string[] = []): readonly string[] {
|
|
341
|
+
const recipe = recipeOf(execution)
|
|
342
|
+
return recipe.build.flatMap((step) => {
|
|
343
|
+
if ('run' in step) return [step.run]
|
|
344
|
+
if (execution.dev === undefined || step.write !== recipe.start) return [writeFileCommand(step.write, step.lines)]
|
|
345
|
+
return [
|
|
346
|
+
writeFileCommand(recipe.start, devStartScript),
|
|
347
|
+
writeBase64Command(DEV_LAUNCHER_CONFIG_FILE, JSON.stringify(devLauncherConfig(execution, authoredSpa))),
|
|
348
|
+
]
|
|
349
|
+
})
|
|
342
350
|
}
|
|
343
351
|
|
|
344
352
|
const sortedUnique = (values: readonly string[]): string[] => [...new Set(values)].sort()
|
|
@@ -354,16 +362,11 @@ const storageVariables = (hostname: string): Record<StorageVariable, string> =>
|
|
|
354
362
|
})
|
|
355
363
|
|
|
356
364
|
/** The crontab lines that run a job of the contract or of the framework, each on one container. */
|
|
357
|
-
const jobCrontab = (
|
|
358
|
-
|
|
359
|
-
...(contract.files === true
|
|
360
|
-
? [{ timing: FILE_SWEEP_SCHEDULE, command: `${FABRIKA_BIN} job run ${ZEROPS_FILE_SWEEP_JOB}`, allContainers: false }]
|
|
361
|
-
: []),
|
|
362
|
-
...(contract.scan === true ? [{ timing: '* * * * *', command: `${FABRIKA_BIN} job run ${ZEROPS_FILE_SCAN_JOB}`, allContainers: false }] : []),
|
|
363
|
-
]
|
|
365
|
+
const jobCrontab = (execution: ZeropsLockedExecution): { readonly timing: string; readonly command: string; readonly allContainers: boolean }[] =>
|
|
366
|
+
recipeOf(execution).jobs.map((job) => ({ timing: job.schedule, command: job.command, allContainers: false }))
|
|
364
367
|
|
|
365
368
|
/** How many job processes the crontab may run at once, each opening the application's database. */
|
|
366
|
-
export const zeropsJobProcesses = (
|
|
369
|
+
export const zeropsJobProcesses = (execution: ZeropsLockedExecution): number => jobCrontab(execution).length
|
|
367
370
|
|
|
368
371
|
/** The commands every container runs before `run.start`, in order; any failure stops the activation. */
|
|
369
372
|
export function generatedInitCommands(execution: ZeropsLockedExecution): string[] {
|
|
@@ -371,7 +374,7 @@ export function generatedInitCommands(execution: ZeropsLockedExecution): string[
|
|
|
371
374
|
// A container starts from the prepared image, whose signatures are as old as its build; a failed refresh leaves
|
|
372
375
|
// them to the daily one, and the scanner refuses signatures older than three days, so the scan fails closed.
|
|
373
376
|
...(execution.contract.scan === true ? [`${FRESHCLAM} || echo 'fabrika: freshclam did not refresh the malware signatures'`] : []),
|
|
374
|
-
...
|
|
377
|
+
...recipeOf(execution).init.map((path) => `sh ${RUNTIME_ROOT}/${path}`),
|
|
375
378
|
]
|
|
376
379
|
}
|
|
377
380
|
|
|
@@ -396,7 +399,7 @@ export function renderLockedSetup(
|
|
|
396
399
|
const scan = contract.scan === true
|
|
397
400
|
const packages = sortedUnique([...execution.runtimePackages, ...(scan ? [SCANNER_PACKAGE] : [])])
|
|
398
401
|
// Every container keeps its own signatures; one scan pass at a time holds the job's lease.
|
|
399
|
-
const crontab = [...jobCrontab(
|
|
402
|
+
const crontab = [...jobCrontab(execution), ...(scan ? [{ timing: FRESHCLAM_SCHEDULE, command: FRESHCLAM, allContainers: true }] : [])]
|
|
400
403
|
const initCommands = generatedInitCommands(execution)
|
|
401
404
|
const prepareCommands = packages.length === 0 ? [] : [`sudo apk add --no-cache ${packages.join(' ')}`, ...(scan ? [FRESHCLAM] : [])]
|
|
402
405
|
return {
|
|
@@ -404,21 +407,29 @@ export function renderLockedSetup(
|
|
|
404
407
|
build: {
|
|
405
408
|
base: authored.base ?? [ZEROPS_APPLICATION_RUNTIME],
|
|
406
409
|
...(authored.os === null ? {} : { os: authored.os }),
|
|
407
|
-
buildCommands: [...authored.commands, ...generatedBuildCommands(execution)],
|
|
410
|
+
buildCommands: [...authored.commands, ...generatedBuildCommands(execution, authored.commands)],
|
|
408
411
|
...(execution.sourceMaps === null ? {} : { envVariables: sourceMapBuildVariables(execution.sourceMaps) }),
|
|
409
412
|
deployFiles: ['./'],
|
|
410
413
|
cache: ['node_modules'],
|
|
411
414
|
},
|
|
412
415
|
deploy: {
|
|
413
|
-
readinessCheck: {
|
|
416
|
+
readinessCheck: {
|
|
417
|
+
exec: { command: readinessCommand(contract.readinessPath, execution.dev !== undefined) },
|
|
418
|
+
retryPeriod: '10s',
|
|
419
|
+
failureTimeout: '180s',
|
|
420
|
+
},
|
|
414
421
|
},
|
|
415
422
|
run: {
|
|
416
423
|
base: ZEROPS_APPLICATION_RUNTIME,
|
|
417
424
|
...(prepareCommands.length === 0 ? {} : { prepareCommands }),
|
|
418
425
|
...(initCommands.length === 0 ? {} : { initCommands }),
|
|
419
|
-
start: `sh ${RUNTIME_ROOT}/${
|
|
426
|
+
start: `sh ${RUNTIME_ROOT}/${recipeOf(execution).start}`,
|
|
420
427
|
ports: [{ port: ZEROPS_APPLICATION_PORT, httpSupport: true }],
|
|
421
|
-
|
|
428
|
+
// A dev container keeps its launcher running while the application it respawns fails: Zerops restarting it
|
|
429
|
+
// would only boot the same failing head again.
|
|
430
|
+
...(execution.dev === undefined
|
|
431
|
+
? { healthCheck: { httpGet: probe, execPeriod: '10s', disconnectTimeout: '30s', recoveryTimeout: '10s', failureTimeout: '120s' } }
|
|
432
|
+
: {}),
|
|
422
433
|
...(crontab.length === 0 ? {} : { crontab }),
|
|
423
434
|
envVariables,
|
|
424
435
|
},
|
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
ensureFirstAdministrator,
|
|
16
16
|
type FirstAdministratorApi,
|
|
17
17
|
type FirstAdministratorResult,
|
|
18
|
+
platformOrigin,
|
|
18
19
|
} from '@fabrika/installation-init'
|
|
19
20
|
import type { PlatformAdminInput } from './admin-options'
|
|
20
21
|
import { REISSUE_FLAG } from './admin-options'
|
|
21
|
-
import { platformOrigin } from './hosts'
|
|
22
22
|
import { MACHINE_KEY_NEXT_STEP_TITLE, machineKeyNextStepLines } from './machine-key'
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
// The command is unattended by construction: nothing here prompts, and a missing required value is an
|
|
13
13
|
// error naming both the flag and the variable that would supply it.
|
|
14
14
|
|
|
15
|
-
import { describeArgument, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
|
|
16
|
-
import { type McpClient, parseMcpClientsJson } from './mcp'
|
|
15
|
+
import { describeArgument, type McpClient, parseMcpClientsJson, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
|
|
17
16
|
import { parsePlatformHosts, parsePlatformScheme } from './placement-options'
|
|
18
17
|
import { readInstallationSourceDir } from './source-options'
|
|
19
18
|
|
|
@@ -37,11 +37,21 @@
|
|
|
37
37
|
// what "deploy" means, and the platform keeps the previous version serving whenever the new one fails its
|
|
38
38
|
// readiness gate.
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import { IAM_PROXY_SECRET_VARIABLE, readEnvironmentName } from '@fabrika/auth-core'
|
|
41
41
|
import { IAM_APP_ID, IAM_APP_SCHEMA } from '@fabrika/iam-contract'
|
|
42
|
+
import {
|
|
43
|
+
deployedServiceEnvironment,
|
|
44
|
+
IAM_OAUTH_CONFIG_VARIABLE,
|
|
45
|
+
type IamOAuthConfig,
|
|
46
|
+
platformIamOAuthConfig,
|
|
47
|
+
platformOrigin,
|
|
48
|
+
platformProxyAppFor,
|
|
49
|
+
type PlatformProxyComposition,
|
|
50
|
+
platformServiceTableDigest,
|
|
51
|
+
resolvePlatformProxyManifest,
|
|
52
|
+
} from '@fabrika/installation-init'
|
|
42
53
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
43
54
|
import { encodeProxyManifestJson, FABRIKA_PROXY_MANIFEST_JSON } from '@fabrika/proxy-core/manifest'
|
|
44
|
-
import { createHmac } from 'node:crypto'
|
|
45
55
|
import { assertServiceEnvValueFits } from '../api'
|
|
46
56
|
import {
|
|
47
57
|
enableSubdomainAccessWhenHttp,
|
|
@@ -64,7 +74,7 @@ import { assertCheckerPublished, type CheckerPublication } from './checker-relea
|
|
|
64
74
|
import type { PlatformDeployInput } from './deploy-options'
|
|
65
75
|
import { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
|
|
66
76
|
import { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
|
|
67
|
-
import { derivePlatformHosts,
|
|
77
|
+
import { derivePlatformHosts, type PlatformPlacement, ZEROPS_SUBDOMAIN_VARIABLE } from './hosts'
|
|
68
78
|
import { PRIVATE_IAM_UPSTREAM } from './install-environment'
|
|
69
79
|
import {
|
|
70
80
|
FABRIKA_ZEROPS_PROXY_SOURCE_JSON,
|
|
@@ -74,11 +84,9 @@ import {
|
|
|
74
84
|
validateInstallationSetups,
|
|
75
85
|
} from './installation-source'
|
|
76
86
|
import type { DeployLog } from './log'
|
|
77
|
-
import { type IamOAuthConfig, platformIamOAuthConfig } from './mcp'
|
|
78
87
|
import { archiveWithDataFile } from './platform-archive'
|
|
79
88
|
import type { PreparedPlatformSource } from './platform-source'
|
|
80
89
|
import { type PlatformUploadFetch, uploadPlatformArchive } from './platform-upload'
|
|
81
|
-
import { platformProxyAppFor, type PlatformProxyComposition, platformServiceTableDigest, resolvePlatformProxyManifest } from './proxy-manifest'
|
|
82
90
|
|
|
83
91
|
/**
|
|
84
92
|
* The four services this command touches, in the order it RESOLVES and WRITES them.
|
|
@@ -323,25 +331,14 @@ const resolvePlacement = (input: PlatformDeployInput, proxyEnv: LiveServiceEnv):
|
|
|
323
331
|
return { hosts: derivePlatformHosts(subdomains, listeners), scheme: input.scheme, zeropsSubdomain: true }
|
|
324
332
|
}
|
|
325
333
|
|
|
326
|
-
const IAM_OAUTH_CONFIG = 'FABRIKA_IAM_OAUTH_CONFIG'
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* The secret the platform proxy sends IAM's route, so IAM honours the client address and request id only from the proxy.
|
|
330
|
-
* Derived from the IAM admin key, which every deploy holds, so each deploy writes the same value and nothing stores or
|
|
331
|
-
* reads one back. Never from the proxy key: namespace proxies hold that one.
|
|
332
|
-
*/
|
|
333
|
-
const deriveIamProxySecret = (iamAdminKey: string): string =>
|
|
334
|
-
createHmac('sha256', iamAdminKey).update('fabrika-installation-proxy-secret:iam').digest('hex')
|
|
335
|
-
|
|
336
334
|
/** The credentials this command writes. Sensitive, so they cannot be compared and are rewritten on every deploy. */
|
|
337
335
|
const SENSITIVE_DEPLOY_KEYS: ReadonlySet<string> = new Set([IAM_PROXY_SECRET_VARIABLE])
|
|
338
336
|
|
|
339
337
|
/**
|
|
340
|
-
* The per-installation configuration each service needs, derived from the resolved hosts
|
|
338
|
+
* The per-installation configuration each service needs, derived from the resolved hosts: fabrika's
|
|
339
|
+
* (`deployedServiceEnvironment`), and control's Zerops variables after it.
|
|
341
340
|
*
|
|
342
341
|
* The namespace source default comes from the verified snapshot. Values are compared but never logged.
|
|
343
|
-
* IAM's OAuth configuration is written only when MCP authoring is enabled; without it the variable is
|
|
344
|
-
* left as it is, and the manifest — whose `/mcp` row then admits no OAuth token — is what turns MCP off.
|
|
345
342
|
*/
|
|
346
343
|
const desiredServiceEnv = (
|
|
347
344
|
environment: string,
|
|
@@ -350,63 +347,32 @@ const desiredServiceEnv = (
|
|
|
350
347
|
proxySource: ZeropsNamespaceProxySource,
|
|
351
348
|
oauthConfig: IamOAuthConfig | undefined,
|
|
352
349
|
controlTableDigest: string,
|
|
353
|
-
|
|
350
|
+
iamAdminKey: string,
|
|
354
351
|
): ReadonlyMap<PlatformDeployService, ReadonlyMap<string, string>> => {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
352
|
+
const fabrika = deployedServiceEnvironment({
|
|
353
|
+
environment,
|
|
354
|
+
scheme: placement.scheme,
|
|
355
|
+
hosts: placement.hosts,
|
|
356
|
+
iamUpstream: PRIVATE_IAM_UPSTREAM,
|
|
357
|
+
provisioningKey: iamAdminKey,
|
|
358
|
+
controlTableDigest,
|
|
359
|
+
...(oauthConfig === undefined ? {} : { oauthConfig }),
|
|
360
|
+
})
|
|
361
|
+
const control = new Map([
|
|
362
|
+
...fabrika.control,
|
|
363
|
+
[FABRIKA_ZEROPS_PROXY_SOURCE_JSON, serializeNamespaceProxySource(proxySource)],
|
|
364
|
+
// The public IAM origin an APPLICATION project's proxy reaches — there is no private
|
|
365
|
+
// network between projects (ADR-0006).
|
|
366
|
+
['FABRIKA_ZEROPS_PROXY_IAM_URL', platformOrigin(placement.scheme, placement.hosts.iam)],
|
|
367
|
+
// The installation's own project, which no deployment namespace may adopt: its proxy is the
|
|
368
|
+
// platform's enforcement point, and a namespace reconcile would rebuild it with only app members.
|
|
369
|
+
['FABRIKA_ZEROPS_PLATFORM_PROJECT_ID', projectId],
|
|
363
370
|
])
|
|
364
|
-
if (oauthConfig !== undefined) {
|
|
365
|
-
iam.set(IAM_OAUTH_CONFIG, JSON.stringify(oauthConfig))
|
|
366
|
-
}
|
|
367
371
|
return new Map<PlatformDeployService, ReadonlyMap<string, string>>([
|
|
368
|
-
['iam', iam],
|
|
369
|
-
[
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
['ENVIRONMENT', environment],
|
|
373
|
-
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
374
|
-
['FABRIKA_OPERATIONS_PUBLIC_HOST', placement.hosts.operations],
|
|
375
|
-
]),
|
|
376
|
-
],
|
|
377
|
-
[
|
|
378
|
-
'proxy',
|
|
379
|
-
new Map([
|
|
380
|
-
['ENVIRONMENT', environment],
|
|
381
|
-
// Compared byte-for-byte against a token's `iss`, so this is the PUBLIC issuer and not the
|
|
382
|
-
// private `iam:3000` the manifest dials — `readProxyEnv` canonicalizes it to an origin.
|
|
383
|
-
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
384
|
-
['FABRIKA_PROXY_SESSION_OWNER_JSON', JSON.stringify({ issuer: iamOrigin, upstream: PRIVATE_IAM_UPSTREAM })],
|
|
385
|
-
// Caddy sends it on the session owner's route. Until the proxy deploys, IAM keys every request on one shared
|
|
386
|
-
// bucket, which is what the balancer's address already does on Zerops.
|
|
387
|
-
[IAM_PROXY_SECRET_VARIABLE, iamProxySecret],
|
|
388
|
-
]),
|
|
389
|
-
],
|
|
390
|
-
[
|
|
391
|
-
'control',
|
|
392
|
-
new Map([
|
|
393
|
-
[FABRIKA_ZEROPS_PROXY_SOURCE_JSON, serializeNamespaceProxySource(proxySource)],
|
|
394
|
-
['ENVIRONMENT', environment],
|
|
395
|
-
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
396
|
-
// A bare host: `controlPublicOrigin` accepts either spelling.
|
|
397
|
-
['FABRIKA_CONTROL_DOMAIN', placement.hosts.control],
|
|
398
|
-
// The digest of the console table the proxy above publishes: Control picks its own table, MCP or not,
|
|
399
|
-
// by it, and refuses to start under another, so the two cannot disagree at runtime.
|
|
400
|
-
[FABRIKA_SERVICE_TABLE_DIGEST, controlTableDigest],
|
|
401
|
-
['OPERATIONS_ARTIFACT_ORIGIN', operationsOrigin],
|
|
402
|
-
// The public IAM origin an APPLICATION project's proxy reaches — there is no private
|
|
403
|
-
// network between projects (ADR-0006).
|
|
404
|
-
['FABRIKA_ZEROPS_PROXY_IAM_URL', iamOrigin],
|
|
405
|
-
// The installation's own project, which no deployment namespace may adopt: its proxy is the
|
|
406
|
-
// platform's enforcement point, and a namespace reconcile would rebuild it with only app members.
|
|
407
|
-
['FABRIKA_ZEROPS_PLATFORM_PROJECT_ID', projectId],
|
|
408
|
-
]),
|
|
409
|
-
],
|
|
372
|
+
['iam', fabrika.iam],
|
|
373
|
+
['operations', fabrika.operations],
|
|
374
|
+
['proxy', fabrika.proxy],
|
|
375
|
+
['control', control],
|
|
410
376
|
])
|
|
411
377
|
}
|
|
412
378
|
|
|
@@ -836,16 +802,16 @@ export const deployPreparedPlatform = async (
|
|
|
836
802
|
proxySource,
|
|
837
803
|
oauthConfig,
|
|
838
804
|
controlTableDigest,
|
|
839
|
-
|
|
805
|
+
input.iamAdminKey,
|
|
840
806
|
)
|
|
841
807
|
for (const hostname of PLATFORM_DEPLOY_ORDER) {
|
|
842
808
|
const service = serviceOf(services, hostname)
|
|
843
809
|
const live = hostname === PLATFORM_PROXY_SERVICE ? proxyEnv : await readServiceEnv(service.id, collaborators)
|
|
844
810
|
const keys = desired.get(hostname) ?? new Map<string, string>()
|
|
845
|
-
const liveOAuthConfig = live.get(
|
|
811
|
+
const liveOAuthConfig = live.get(IAM_OAUTH_CONFIG_VARIABLE)
|
|
846
812
|
const oauthStillSet = liveOAuthConfig !== undefined && (liveOAuthConfig.sensitive || liveOAuthConfig.content.trim() !== '')
|
|
847
813
|
if (hostname === 'iam' && oauthConfig === undefined && oauthStillSet) {
|
|
848
|
-
log.warn(`iam: MCP authoring is off but ${
|
|
814
|
+
log.warn(`iam: MCP authoring is off but ${IAM_OAUTH_CONFIG_VARIABLE} is still set; IAM keeps issuing MCP tokens until it is removed`)
|
|
849
815
|
}
|
|
850
816
|
const written = await applyServiceEnv(service, keys, live, input, collaborators)
|
|
851
817
|
log.info(
|
|
@@ -30,6 +30,9 @@ export const PLATFORM_CONSOLE_APP_SCHEMA: AppSchema = {
|
|
|
30
30
|
{ action: 'mail.read', description: 'List relayed mail and read its metadata' },
|
|
31
31
|
{ action: 'mail.content.read', description: 'Read the recipient, subject and bodies of relayed mail' },
|
|
32
32
|
{ action: 'mail.manage', description: 'Release or drop held mail and manage the mail rules' },
|
|
33
|
+
{ action: 'data.read', description: 'Read the sizes, counts, state and backup list of environment data' },
|
|
34
|
+
{ action: 'data.content.read', description: 'Read the row contents, exports and backups of every tenant (global grant only)' },
|
|
35
|
+
{ action: 'data.manage', description: 'Run maintenance, backups and restores, and recover an interrupted operation' },
|
|
33
36
|
{ action: 'operations.read', description: 'Read Operations errors and activity' },
|
|
34
37
|
{ action: 'operations.triage', description: 'Triage Operations errors' },
|
|
35
38
|
{ action: 'operations.manage', description: 'Manage Operations sources, alerts, and retention' },
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// `table` and registers the resource only on an installation that enables MCP authoring, and its
|
|
17
17
|
// `tableWithoutOAuth` otherwise.
|
|
18
18
|
|
|
19
|
-
import type { PlatformProxyManifestTemplate } from '
|
|
19
|
+
import type { PlatformProxyManifestTemplate } from '@fabrika/installation-init'
|
|
20
20
|
|
|
21
21
|
export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
22
22
|
apps: [
|
|
@@ -258,6 +258,16 @@ export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
|
258
258
|
admission: null,
|
|
259
259
|
oauth: null,
|
|
260
260
|
},
|
|
261
|
+
{
|
|
262
|
+
id: 'dev-source',
|
|
263
|
+
kind: 'protocol',
|
|
264
|
+
method: 'POST',
|
|
265
|
+
path: '/platform/source/*',
|
|
266
|
+
callers: [],
|
|
267
|
+
requirement: null,
|
|
268
|
+
admission: null,
|
|
269
|
+
oauth: null,
|
|
270
|
+
},
|
|
261
271
|
{
|
|
262
272
|
id: 'webhooks-root',
|
|
263
273
|
kind: 'service',
|
|
@@ -453,6 +463,16 @@ export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
|
453
463
|
admission: null,
|
|
454
464
|
oauth: null,
|
|
455
465
|
},
|
|
466
|
+
{
|
|
467
|
+
id: 'dev-source',
|
|
468
|
+
kind: 'protocol',
|
|
469
|
+
method: 'POST',
|
|
470
|
+
path: '/platform/source/*',
|
|
471
|
+
callers: [],
|
|
472
|
+
requirement: null,
|
|
473
|
+
admission: null,
|
|
474
|
+
oauth: null,
|
|
475
|
+
},
|
|
456
476
|
{
|
|
457
477
|
id: 'webhooks-root',
|
|
458
478
|
kind: 'service',
|
|
@@ -398,12 +398,15 @@ none, and `install` cannot be re-run.
|
|
|
398
398
|
2. Restart `iam` and `control`. Each reads the transport at boot. IAM refuses to start on an invalid one.
|
|
399
399
|
Control starts, logs a line that names the invalid variables, and refuses every deploy that needs the transport
|
|
400
400
|
until you fix them and restart it.
|
|
401
|
-
3. Redeploy every environment whose contract uses `installation` mail
|
|
402
|
-
|
|
403
|
-
`provider_unauthorized`.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
401
|
+
3. Redeploy every environment whose contract uses `installation` mail and whose application pins a framework release
|
|
402
|
+
older than 0.7. Such an application holds its own copy of the transport and gets the new one only at its next
|
|
403
|
+
deploy. Until then it sends with the old one, and a revoked key fails its mail with `provider_unauthorized`. An
|
|
404
|
+
application on 0.7 or newer sends through Control's relay, which uses the new transport once step 2 restarted
|
|
405
|
+
Control: it needs no redeploy.
|
|
406
|
+
|
|
407
|
+
Without a transport, a contract whose job declares `mailer: 'installation'` still deploys. On a release older than 0.7
|
|
408
|
+
its jobs see that mail is not available and leave their messages queued, and after step 3 the next run sends them. On
|
|
409
|
+
0.7 or newer the relay accepts and holds the messages, and Control sends them once it has a transport.
|
|
407
410
|
|
|
408
411
|
## When something fails
|
|
409
412
|
|
|
@@ -24,10 +24,7 @@
|
|
|
24
24
|
// never been deployed publishes none, so the variable names nothing and this refuses rather than
|
|
25
25
|
// guessing at a hostname.
|
|
26
26
|
|
|
27
|
-
import type { PlatformProxyService } from '
|
|
28
|
-
|
|
29
|
-
/** The public hostname of each platform service, without a scheme and without a port. */
|
|
30
|
-
export type PlatformHosts = Readonly<Record<PlatformProxyService, string>>
|
|
27
|
+
import type { PlatformHosts, PlatformProxyService } from '@fabrika/installation-init'
|
|
31
28
|
|
|
32
29
|
/** Everything the deploy needs to know about where this installation answers. */
|
|
33
30
|
export interface PlatformPlacement {
|
|
@@ -112,6 +109,3 @@ export const derivePlatformHosts = (subdomains: string, listeners: readonly Plat
|
|
|
112
109
|
}
|
|
113
110
|
return { iam, control, operations }
|
|
114
111
|
}
|
|
115
|
-
|
|
116
|
-
/** `https://<host>` — the origin a browser, a token's `iss` and a return-origin registry all use. */
|
|
117
|
-
export const platformOrigin = (scheme: 'http' | 'https', host: string): string => `${scheme}://${host}`
|
|
@@ -31,19 +31,6 @@ import { parsePlatformUpgradeArgs, UPGRADE_DRY_RUN_FLAG } from './upgrade-option
|
|
|
31
31
|
export { PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_PROXY_SERVICE, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
|
|
32
32
|
export { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
|
|
33
33
|
export { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
|
|
34
|
-
export { type IamOAuthConfig, type McpClient, parseMcpClientsJson, platformIamOAuthConfig } from './mcp'
|
|
35
|
-
export {
|
|
36
|
-
type PlatformOAuthResourceTemplate,
|
|
37
|
-
type PlatformProxyApp,
|
|
38
|
-
platformProxyAppFor,
|
|
39
|
-
type PlatformProxyComposition,
|
|
40
|
-
type PlatformProxyManifest,
|
|
41
|
-
type PlatformProxyManifestTemplate,
|
|
42
|
-
type PlatformProxyPlacement,
|
|
43
|
-
type PlatformProxyService,
|
|
44
|
-
platformServiceTableDigest,
|
|
45
|
-
resolvePlatformProxyManifest,
|
|
46
|
-
} from './proxy-manifest'
|
|
47
34
|
|
|
48
35
|
const EMAIL_PROVIDERS = Object.keys(builtinEmailProviders).join(', ')
|
|
49
36
|
/** One line per provider: its variables, each marked secret or optional where it is. */
|