@fabrika/provider-zerops 0.8.1 → 0.9.0-cloudflare.1
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 +18 -0
- package/src/api/readers.ts +29 -0
- package/src/api/types.ts +64 -0
- package/src/api.ts +2 -0
- package/src/cli.ts +11 -0
- package/src/collaborators.ts +0 -14
- package/src/control.ts +35 -57
- package/src/execution.ts +113 -102
- package/src/index.ts +2 -0
- package/src/installation/admin.ts +1 -1
- package/src/installation/deploy-options.ts +1 -2
- package/src/installation/deploy.ts +225 -80
- 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 +48 -18
- package/src/installation/hosts.ts +1 -7
- package/src/installation/index.ts +13 -16
- package/src/installation/init-options.ts +2 -1
- package/src/installation/install-environment.ts +47 -108
- package/src/installation/install-options.ts +3 -1
- package/src/installation/install-secrets.ts +24 -68
- package/src/installation/install.ts +9 -4
- package/src/installation/sidecar.ts +5 -26
- package/src/installation/topology/setups.ts +15 -4
- package/src/installation/topology/topology.ts +31 -21
- 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 +24 -4
- package/src/namespace/proxy-attempt.ts +43 -2
- package/src/namespace/proxy-reads.ts +12 -22
- package/src/namespace/types.ts +2 -0
- 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
|
@@ -21,21 +21,35 @@
|
|
|
21
21
|
//
|
|
22
22
|
// It composes the proxy manifest and ships it in the proxy's build archive. It writes the exact namespace
|
|
23
23
|
// planning source default and the per-installation configuration derived from the resolved public hosts
|
|
24
|
-
// (each service's own origin, the origins it must know about, and the installation's environment name)
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// (each service's own origin, the origins it must know about, and the installation's environment name),
|
|
25
|
+
// and the platform database's backup schedule. **It writes one credential, and only a derived one**: the
|
|
26
|
+
// secret the proxy proves its requests to IAM with, HMAC-SHA256 of a fixed label keyed by the IAM admin key,
|
|
27
|
+
// written sensitive to `proxy` and `iam` and never read back. Every other secret an installation holds is
|
|
28
|
+
// `platform install`'s to place. The two credentials this command HOLDS are the Zerops access token and the
|
|
29
|
+
// IAM admin key, both from the environment, neither ever logged.
|
|
28
30
|
//
|
|
29
31
|
// ── Idempotence ───────────────────────────────────────────────────────────────────────────────────
|
|
30
32
|
//
|
|
31
|
-
// Every variable is READ before it is written and skipped when it already matches, so a second
|
|
32
|
-
// writes
|
|
33
|
-
//
|
|
34
|
-
//
|
|
33
|
+
// Every readable variable is READ before it is written and skipped when it already matches, so a second
|
|
34
|
+
// run writes only the derived secret, which it cannot read back; the subdomain is decided by reading
|
|
35
|
+
// `subdomainAccess` back, so a second run enables nothing; the database's backup period is read and set
|
|
36
|
+
// only when it differs; and the schema reconcile is an upsert. What a re-run does do is redeploy — that is
|
|
37
|
+
// what "deploy" means, and the platform keeps the previous version serving whenever the new one fails its
|
|
35
38
|
// readiness gate.
|
|
36
39
|
|
|
37
|
-
import {
|
|
40
|
+
import { IAM_PROXY_SECRET_VARIABLE, readEnvironmentName } from '@fabrika/auth-core'
|
|
38
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'
|
|
39
53
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
40
54
|
import { encodeProxyManifestJson, FABRIKA_PROXY_MANIFEST_JSON } from '@fabrika/proxy-core/manifest'
|
|
41
55
|
import { assertServiceEnvValueFits } from '../api'
|
|
@@ -44,12 +58,14 @@ import {
|
|
|
44
58
|
prepareZeropsDescriptor,
|
|
45
59
|
readableContent,
|
|
46
60
|
type Sleeper,
|
|
61
|
+
waitForProcess,
|
|
47
62
|
ZEROPS_ACTIVE,
|
|
48
63
|
ZEROPS_SERVICE_NOT_HTTP,
|
|
49
64
|
ZEROPS_TERMINAL,
|
|
50
65
|
type ZeropsApi,
|
|
51
66
|
ZeropsApiError,
|
|
52
67
|
type ZeropsNamespaceProxySource,
|
|
68
|
+
type ZeropsProcess,
|
|
53
69
|
type ZeropsService,
|
|
54
70
|
type ZeropsServiceEnv,
|
|
55
71
|
} from '../index'
|
|
@@ -58,7 +74,7 @@ import { assertCheckerPublished, type CheckerPublication } from './checker-relea
|
|
|
58
74
|
import type { PlatformDeployInput } from './deploy-options'
|
|
59
75
|
import { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
|
|
60
76
|
import { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
|
|
61
|
-
import { derivePlatformHosts,
|
|
77
|
+
import { derivePlatformHosts, type PlatformPlacement, ZEROPS_SUBDOMAIN_VARIABLE } from './hosts'
|
|
62
78
|
import { PRIVATE_IAM_UPSTREAM } from './install-environment'
|
|
63
79
|
import {
|
|
64
80
|
FABRIKA_ZEROPS_PROXY_SOURCE_JSON,
|
|
@@ -68,11 +84,9 @@ import {
|
|
|
68
84
|
validateInstallationSetups,
|
|
69
85
|
} from './installation-source'
|
|
70
86
|
import type { DeployLog } from './log'
|
|
71
|
-
import { type IamOAuthConfig, platformIamOAuthConfig } from './mcp'
|
|
72
87
|
import { archiveWithDataFile } from './platform-archive'
|
|
73
88
|
import type { PreparedPlatformSource } from './platform-source'
|
|
74
89
|
import { type PlatformUploadFetch, uploadPlatformArchive } from './platform-upload'
|
|
75
|
-
import { platformProxyAppFor, type PlatformProxyComposition, platformServiceTableDigest, resolvePlatformProxyManifest } from './proxy-manifest'
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
92
|
* The four services this command touches, in the order it RESOLVES and WRITES them.
|
|
@@ -112,6 +126,18 @@ export const PLATFORM_SEQUENTIAL_DEPLOY = ['proxy', 'control'] as const
|
|
|
112
126
|
/** The proxy's hostname. The only publicly routed service, and the one that carries the manifest. */
|
|
113
127
|
export const PLATFORM_PROXY_SERVICE = 'proxy'
|
|
114
128
|
|
|
129
|
+
/** IAM's and control's database, on both tiers. This command deploys nothing to it; it sets its backup schedule. */
|
|
130
|
+
export const PLATFORM_DATABASE_SERVICE = 'db'
|
|
131
|
+
|
|
132
|
+
/** Operations' own database. A light installation created before it existed keeps Operations in `db`. */
|
|
133
|
+
export const OPERATIONS_DATABASE_SERVICE = 'operationsdb'
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Hourly. Zerops has no point-in-time recovery: a single-node database loses everything since its last `pg_dump`, which
|
|
137
|
+
* by default is daily. UNVERIFIED: that `backupPeriod` takes this 5-field cron, as the GUI documentation describes.
|
|
138
|
+
*/
|
|
139
|
+
export const PLATFORM_DATABASE_BACKUP_PERIOD = '0 * * * *'
|
|
140
|
+
|
|
115
141
|
/** How often a triggered pipeline is polled. */
|
|
116
142
|
const POLL_INTERVAL_MS = 5_000
|
|
117
143
|
/** Zerops caps a build pipeline at one hour; poll past that so the timeout is ours, not a coincidence. */
|
|
@@ -208,43 +234,58 @@ const resolveProject = async (
|
|
|
208
234
|
/** The private source service of an earlier layout; Control now streams sources itself (ADR-0029). */
|
|
209
235
|
const LEFTOVER_SOURCE_SERVICE = 'source'
|
|
210
236
|
|
|
237
|
+
/** The services this command touches: the four it deploys, and the database whose backups it schedules. */
|
|
238
|
+
interface ResolvedInstallation {
|
|
239
|
+
readonly services: ReadonlyMap<PlatformDeployService, ResolvedService>
|
|
240
|
+
readonly databaseId: string
|
|
241
|
+
readonly hasOperationsDatabase: boolean
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
/**
|
|
212
245
|
* Find every platform service by hostname in one listing.
|
|
213
246
|
*
|
|
214
|
-
* One call rather than
|
|
247
|
+
* One call rather than five `findService` lookups, so a missing service is reported as the whole set
|
|
215
248
|
* that is missing — an installation that was never imported fails once, naming all of it, rather than
|
|
216
|
-
*
|
|
249
|
+
* five runs later. Build runtimes (`build<hostname>v<n>`) share the project and are ignored by the
|
|
217
250
|
* exact-name match.
|
|
218
251
|
*/
|
|
219
252
|
const resolveServices = async (
|
|
220
253
|
projectId: string,
|
|
221
254
|
{ api, log, signal }: PlatformDeployCollaborators,
|
|
222
|
-
): Promise<
|
|
255
|
+
): Promise<ResolvedInstallation> => {
|
|
223
256
|
const services = await api.listProjectServices({ projectId, signal })
|
|
224
257
|
if (services.some((service: ZeropsService) => service.name === LEFTOVER_SOURCE_SERVICE)) {
|
|
225
258
|
log.warn('a `source` service from an earlier layout is present and unused; it may still hold GitHub App keys; delete it or reinstall')
|
|
226
259
|
}
|
|
227
|
-
const
|
|
228
|
-
const missing: string[] = []
|
|
229
|
-
for (const hostname of PLATFORM_DEPLOY_ORDER) {
|
|
260
|
+
const idOf = (hostname: string): string | undefined => {
|
|
230
261
|
const matches = services.filter((service: ZeropsService) => service.name === hostname)
|
|
231
262
|
if (matches.length > 1) {
|
|
232
263
|
throw new Error(`Zerops project ${projectId} has ${matches.length} services named \`${hostname}\``)
|
|
233
264
|
}
|
|
234
265
|
const match = matches[0]
|
|
235
|
-
|
|
266
|
+
return match === undefined || match.id === '' ? undefined : match.id
|
|
267
|
+
}
|
|
268
|
+
const resolved = new Map<PlatformDeployService, ResolvedService>()
|
|
269
|
+
const missing: string[] = []
|
|
270
|
+
for (const hostname of PLATFORM_DEPLOY_ORDER) {
|
|
271
|
+
const id = idOf(hostname)
|
|
272
|
+
if (id === undefined) {
|
|
236
273
|
missing.push(hostname)
|
|
237
274
|
continue
|
|
238
275
|
}
|
|
239
|
-
resolved.set(hostname, { hostname, id
|
|
276
|
+
resolved.set(hostname, { hostname, id })
|
|
240
277
|
}
|
|
241
|
-
|
|
278
|
+
const databaseId = idOf(PLATFORM_DATABASE_SERVICE)
|
|
279
|
+
if (databaseId === undefined) {
|
|
280
|
+
missing.push(PLATFORM_DATABASE_SERVICE)
|
|
281
|
+
}
|
|
282
|
+
if (missing.length > 0 || databaseId === undefined) {
|
|
242
283
|
throw new Error(
|
|
243
284
|
`Zerops project ${projectId} has no ${missing.join(', ')} service — \`platform deploy\` updates an installation, `
|
|
244
285
|
+ 'it does not create one',
|
|
245
286
|
)
|
|
246
287
|
}
|
|
247
|
-
return resolved
|
|
288
|
+
return { services: resolved, databaseId, hasOperationsDatabase: idOf(OPERATIONS_DATABASE_SERVICE) !== undefined }
|
|
248
289
|
}
|
|
249
290
|
|
|
250
291
|
export const serviceOf = (services: ReadonlyMap<PlatformDeployService, ResolvedService>, hostname: PlatformDeployService): ResolvedService => {
|
|
@@ -290,78 +331,56 @@ const resolvePlacement = (input: PlatformDeployInput, proxyEnv: LiveServiceEnv):
|
|
|
290
331
|
return { hosts: derivePlatformHosts(subdomains, listeners), scheme: input.scheme, zeropsSubdomain: true }
|
|
291
332
|
}
|
|
292
333
|
|
|
293
|
-
|
|
334
|
+
/** The credentials this command writes. Sensitive, so they cannot be compared and are rewritten on every deploy. */
|
|
335
|
+
const SENSITIVE_DEPLOY_KEYS: ReadonlySet<string> = new Set([IAM_PROXY_SECRET_VARIABLE])
|
|
294
336
|
|
|
295
337
|
/**
|
|
296
|
-
* 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.
|
|
297
340
|
*
|
|
298
341
|
* The namespace source default comes from the verified snapshot. Values are compared but never logged.
|
|
299
|
-
* IAM's OAuth configuration is written only when MCP authoring is enabled; without it the variable is
|
|
300
|
-
* left as it is, and the manifest — whose `/mcp` row then admits no OAuth token — is what turns MCP off.
|
|
301
342
|
*/
|
|
302
343
|
const desiredServiceEnv = (
|
|
303
344
|
environment: string,
|
|
345
|
+
projectId: string,
|
|
304
346
|
placement: PlatformPlacement,
|
|
305
347
|
proxySource: ZeropsNamespaceProxySource,
|
|
306
348
|
oauthConfig: IamOAuthConfig | undefined,
|
|
307
349
|
controlTableDigest: string,
|
|
350
|
+
iamAdminKey: string,
|
|
308
351
|
): ReadonlyMap<PlatformDeployService, ReadonlyMap<string, string>> => {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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],
|
|
316
370
|
])
|
|
317
|
-
if (oauthConfig !== undefined) {
|
|
318
|
-
iam.set(IAM_OAUTH_CONFIG, JSON.stringify(oauthConfig))
|
|
319
|
-
}
|
|
320
371
|
return new Map<PlatformDeployService, ReadonlyMap<string, string>>([
|
|
321
|
-
['iam', iam],
|
|
322
|
-
[
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
['ENVIRONMENT', environment],
|
|
326
|
-
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
327
|
-
['FABRIKA_OPERATIONS_PUBLIC_HOST', placement.hosts.operations],
|
|
328
|
-
]),
|
|
329
|
-
],
|
|
330
|
-
[
|
|
331
|
-
'proxy',
|
|
332
|
-
new Map([
|
|
333
|
-
['ENVIRONMENT', environment],
|
|
334
|
-
// Compared byte-for-byte against a token's `iss`, so this is the PUBLIC issuer and not the
|
|
335
|
-
// private `iam:3000` the manifest dials — `readProxyEnv` canonicalizes it to an origin.
|
|
336
|
-
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
337
|
-
['FABRIKA_PROXY_SESSION_OWNER_JSON', JSON.stringify({ issuer: iamOrigin, upstream: PRIVATE_IAM_UPSTREAM })],
|
|
338
|
-
]),
|
|
339
|
-
],
|
|
340
|
-
[
|
|
341
|
-
'control',
|
|
342
|
-
new Map([
|
|
343
|
-
[FABRIKA_ZEROPS_PROXY_SOURCE_JSON, serializeNamespaceProxySource(proxySource)],
|
|
344
|
-
['ENVIRONMENT', environment],
|
|
345
|
-
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
346
|
-
// A bare host: `controlPublicOrigin` accepts either spelling.
|
|
347
|
-
['FABRIKA_CONTROL_DOMAIN', placement.hosts.control],
|
|
348
|
-
// The digest of the console table the proxy above publishes: Control picks its own table, MCP or not,
|
|
349
|
-
// by it, and refuses to start under another, so the two cannot disagree at runtime.
|
|
350
|
-
[FABRIKA_SERVICE_TABLE_DIGEST, controlTableDigest],
|
|
351
|
-
['OPERATIONS_ARTIFACT_ORIGIN', operationsOrigin],
|
|
352
|
-
// The public IAM origin an APPLICATION project's proxy reaches — there is no private
|
|
353
|
-
// network between projects (ADR-0006).
|
|
354
|
-
['FABRIKA_ZEROPS_PROXY_IAM_URL', iamOrigin],
|
|
355
|
-
]),
|
|
356
|
-
],
|
|
372
|
+
['iam', fabrika.iam],
|
|
373
|
+
['operations', fabrika.operations],
|
|
374
|
+
['proxy', fabrika.proxy],
|
|
375
|
+
['control', control],
|
|
357
376
|
])
|
|
358
377
|
}
|
|
359
378
|
|
|
360
379
|
/**
|
|
361
380
|
* Write only what differs. Returns the KEYS written — never a value, not even in a debug line.
|
|
362
381
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
382
|
+
* Everything but `SENSITIVE_DEPLOY_KEYS` is written readable. A record an older release wrote sensitive
|
|
383
|
+
* reads as different and is rewritten readable, which is what lets the next deploy compare it.
|
|
365
384
|
*/
|
|
366
385
|
const applyServiceEnv = async (
|
|
367
386
|
service: ResolvedService,
|
|
@@ -372,7 +391,8 @@ const applyServiceEnv = async (
|
|
|
372
391
|
): Promise<string[]> => {
|
|
373
392
|
const written: string[] = []
|
|
374
393
|
for (const [key, value] of desired) {
|
|
375
|
-
|
|
394
|
+
const sensitive = SENSITIVE_DEPLOY_KEYS.has(key)
|
|
395
|
+
if (!sensitive && readableContent(live.get(key)) === value) {
|
|
376
396
|
continue
|
|
377
397
|
}
|
|
378
398
|
written.push(key)
|
|
@@ -382,7 +402,7 @@ const applyServiceEnv = async (
|
|
|
382
402
|
continue
|
|
383
403
|
}
|
|
384
404
|
assertRunning(collaborators.signal)
|
|
385
|
-
await collaborators.api.putServiceEnv({ serviceId: service.id, key, value, sensitive
|
|
405
|
+
await collaborators.api.putServiceEnv({ serviceId: service.id, key, value, sensitive, signal: collaborators.signal })
|
|
386
406
|
}
|
|
387
407
|
return written
|
|
388
408
|
}
|
|
@@ -591,6 +611,111 @@ const ensureSubdomainAccess = async (
|
|
|
591
611
|
}
|
|
592
612
|
}
|
|
593
613
|
|
|
614
|
+
/** What `ensureBackupPeriod` found and did. */
|
|
615
|
+
export type BackupPeriodOutcome =
|
|
616
|
+
| { readonly kind: 'unchanged' }
|
|
617
|
+
| { readonly kind: 'kept'; readonly current: string }
|
|
618
|
+
| { readonly kind: 'would-change'; readonly from: string }
|
|
619
|
+
| { readonly kind: 'changed'; readonly from: string }
|
|
620
|
+
| { readonly kind: 'unconfirmed'; readonly readBack: string }
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Whether a period is one the operator did not choose: empty, or once a day (`<minute> <hour> * * *`), which is the
|
|
624
|
+
* shape of the platform's daily default. Anything else is kept. UNVERIFIED: how the platform spells its default.
|
|
625
|
+
*/
|
|
626
|
+
const isDailyBackupPeriod = (period: string): boolean => period.trim() === '' || /^\d+ \d+ \* \* \*$/.test(period.trim())
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Set a managed service's backup period, read-compare-write, replacing only a daily one.
|
|
630
|
+
*
|
|
631
|
+
* A different period is sent with the retention the service reads — its own, else the default — WHOLE, because a
|
|
632
|
+
* PUT on this API resets what it leaves out. The decision is the read-back, never the PUT returning.
|
|
633
|
+
*/
|
|
634
|
+
export const ensureBackupPeriod = async (input: {
|
|
635
|
+
readonly api: Pick<ZeropsApi, 'getBackupConfig' | 'configureBackup' | 'getProcess'>
|
|
636
|
+
readonly sleep: Sleeper
|
|
637
|
+
readonly signal: AbortSignal
|
|
638
|
+
readonly serviceId: string
|
|
639
|
+
readonly hostname: string
|
|
640
|
+
readonly period: string
|
|
641
|
+
readonly dryRun: boolean
|
|
642
|
+
}): Promise<BackupPeriodOutcome> => {
|
|
643
|
+
const { api, sleep, signal, serviceId, hostname, period } = input
|
|
644
|
+
const current = await api.getBackupConfig({ serviceId, signal })
|
|
645
|
+
if (current.backupPeriod === period) {
|
|
646
|
+
return { kind: 'unchanged' }
|
|
647
|
+
}
|
|
648
|
+
if (!isDailyBackupPeriod(current.backupPeriod)) {
|
|
649
|
+
return { kind: 'kept', current: current.backupPeriod }
|
|
650
|
+
}
|
|
651
|
+
if (input.dryRun) {
|
|
652
|
+
return { kind: 'would-change', from: current.backupPeriod }
|
|
653
|
+
}
|
|
654
|
+
assertRunning(signal)
|
|
655
|
+
let process: ZeropsProcess | null
|
|
656
|
+
try {
|
|
657
|
+
process = await api.configureBackup({
|
|
658
|
+
serviceId,
|
|
659
|
+
backupPeriod: period,
|
|
660
|
+
retentionPolicy: current.retentionPolicy ?? current.defaultRetentionPolicy,
|
|
661
|
+
signal,
|
|
662
|
+
})
|
|
663
|
+
} catch (error) {
|
|
664
|
+
if (error instanceof ZeropsApiError && error.status >= 400 && error.status < 500) {
|
|
665
|
+
throw new Error(`zerops: setting the backup schedule of ${hostname} was refused (${error.code === '' ? error.status : error.code})`)
|
|
666
|
+
}
|
|
667
|
+
throw error
|
|
668
|
+
}
|
|
669
|
+
if (process !== null) {
|
|
670
|
+
await waitForProcess({ api, processId: process.id, sleep, signal, label: `the backup schedule change of ${hostname}` })
|
|
671
|
+
}
|
|
672
|
+
const after = await api.getBackupConfig({ serviceId, signal })
|
|
673
|
+
if (after.backupPeriod !== period) {
|
|
674
|
+
return { kind: 'unconfirmed', readBack: after.backupPeriod }
|
|
675
|
+
}
|
|
676
|
+
return { kind: 'changed', from: current.backupPeriod }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** `ensureBackupPeriod` on the platform database, reported as one log line; nothing it meets fails the deploy. */
|
|
680
|
+
const scheduleDatabaseBackups = async (databaseId: string, dryRun: boolean, collaborators: PlatformDeployCollaborators): Promise<void> => {
|
|
681
|
+
const { log, signal } = collaborators
|
|
682
|
+
const db = PLATFORM_DATABASE_SERVICE
|
|
683
|
+
const schedule = `\`${PLATFORM_DATABASE_BACKUP_PERIOD}\``
|
|
684
|
+
let backup: BackupPeriodOutcome
|
|
685
|
+
try {
|
|
686
|
+
backup = await ensureBackupPeriod({
|
|
687
|
+
api: collaborators.api,
|
|
688
|
+
sleep: collaborators.sleep,
|
|
689
|
+
signal,
|
|
690
|
+
serviceId: databaseId,
|
|
691
|
+
hostname: db,
|
|
692
|
+
period: PLATFORM_DATABASE_BACKUP_PERIOD,
|
|
693
|
+
dryRun,
|
|
694
|
+
})
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (signal.aborted) throw error
|
|
697
|
+
log.warn(`${db}: the backup schedule was not set (${error instanceof Error ? error.message : 'unknown error'}); set it to ${schedule} by hand`)
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
switch (backup.kind) {
|
|
701
|
+
case 'unchanged':
|
|
702
|
+
log.info(`${db}: backups already run on ${schedule}`)
|
|
703
|
+
return
|
|
704
|
+
case 'kept':
|
|
705
|
+
log.info(`${db}: backups keep the schedule \`${backup.current}\` someone chose instead of the daily default`)
|
|
706
|
+
return
|
|
707
|
+
case 'would-change':
|
|
708
|
+
log.info(`${db}: backups would run on ${schedule}, were \`${backup.from}\``)
|
|
709
|
+
return
|
|
710
|
+
case 'changed':
|
|
711
|
+
log.info(`${db}: backups now run on ${schedule}, were \`${backup.from}\``)
|
|
712
|
+
return
|
|
713
|
+
case 'unconfirmed':
|
|
714
|
+
log.warn(`${db}: backups were set to ${schedule} but read back \`${backup.readBack}\`; check the schedule by hand`)
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
594
719
|
/**
|
|
595
720
|
* The installation's environment name, refused when a public origin contradicts it.
|
|
596
721
|
*
|
|
@@ -637,7 +762,7 @@ export const deployPreparedPlatform = async (
|
|
|
637
762
|
|
|
638
763
|
log.step('Resolve the installation')
|
|
639
764
|
const project = await resolveProject(input, collaborators)
|
|
640
|
-
const services = await resolveServices(project.id, collaborators)
|
|
765
|
+
const { services, databaseId, hasOperationsDatabase } = await resolveServices(project.id, collaborators)
|
|
641
766
|
log.info(`project ${project.name} (${project.id}) — ${PLATFORM_DEPLOY_ORDER.join(', ')}`)
|
|
642
767
|
|
|
643
768
|
const proxy = serviceOf(services, PLATFORM_PROXY_SERVICE)
|
|
@@ -670,15 +795,23 @@ export const deployPreparedPlatform = async (
|
|
|
670
795
|
|
|
671
796
|
log.step('Write the service environment')
|
|
672
797
|
const controlTableDigest = platformServiceTableDigest(platformManifest, platformProxyAppFor(PLATFORM_PROXY_MANIFEST_TEMPLATE, 'control').id)
|
|
673
|
-
const desired = desiredServiceEnv(
|
|
798
|
+
const desired = desiredServiceEnv(
|
|
799
|
+
environment,
|
|
800
|
+
project.id,
|
|
801
|
+
placement,
|
|
802
|
+
proxySource,
|
|
803
|
+
oauthConfig,
|
|
804
|
+
controlTableDigest,
|
|
805
|
+
input.iamAdminKey,
|
|
806
|
+
)
|
|
674
807
|
for (const hostname of PLATFORM_DEPLOY_ORDER) {
|
|
675
808
|
const service = serviceOf(services, hostname)
|
|
676
809
|
const live = hostname === PLATFORM_PROXY_SERVICE ? proxyEnv : await readServiceEnv(service.id, collaborators)
|
|
677
810
|
const keys = desired.get(hostname) ?? new Map<string, string>()
|
|
678
|
-
const liveOAuthConfig = live.get(
|
|
811
|
+
const liveOAuthConfig = live.get(IAM_OAUTH_CONFIG_VARIABLE)
|
|
679
812
|
const oauthStillSet = liveOAuthConfig !== undefined && (liveOAuthConfig.sensitive || liveOAuthConfig.content.trim() !== '')
|
|
680
813
|
if (hostname === 'iam' && oauthConfig === undefined && oauthStillSet) {
|
|
681
|
-
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`)
|
|
682
815
|
}
|
|
683
816
|
const written = await applyServiceEnv(service, keys, live, input, collaborators)
|
|
684
817
|
log.info(
|
|
@@ -746,5 +879,17 @@ export const deployPreparedPlatform = async (
|
|
|
746
879
|
log.info(`\`${IAM_APP_ID}\` reconciled`)
|
|
747
880
|
}
|
|
748
881
|
|
|
882
|
+
// Last, and a warning rather than a failure: the backup API's behaviour is not yet measured on a live account
|
|
883
|
+
// (docs/reference/zerops-platform.md), and every service is already deployed, published and registered.
|
|
884
|
+
log.step('Schedule the platform database backups')
|
|
885
|
+
if (hasOperationsDatabase) {
|
|
886
|
+
await scheduleDatabaseBackups(databaseId, input.dryRun, collaborators)
|
|
887
|
+
} else {
|
|
888
|
+
log.warn(
|
|
889
|
+
`${PLATFORM_DATABASE_SERVICE}: backups keep their schedule — Operations shares this database on an installation `
|
|
890
|
+
+ `created before \`${OPERATIONS_DATABASE_SERVICE}\`, and hourly dumps would copy its whole error history`,
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
749
894
|
log.step(input.dryRun ? 'Dry run complete — nothing was written' : 'Installation deployed')
|
|
750
895
|
}
|
|
@@ -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',
|
|
@@ -44,16 +44,41 @@ cores and 48 GB per container, and costs more while it does.
|
|
|
44
44
|
**1. Platform tier** (`platform install --tier`, step 1). Cannot be changed later: a project's core package
|
|
45
45
|
is upgrade-only, and the data services differ.
|
|
46
46
|
|
|
47
|
-
| Tier | What runs | Idle floor
|
|
48
|
-
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
-
| `light` (default) | Lightweight core.
|
|
50
|
-
| `standard` | Serious core. HA PostgreSQL (3 nodes, `oltp-production`, dedicated CPU) for IAM and control; a second HA PostgreSQL and bucket for Operations; two containers of IAM and the proxy. | about $100
|
|
47
|
+
| Tier | What runs | Idle floor | Fails when |
|
|
48
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------ |
|
|
49
|
+
| `light` (default) | Lightweight core. Two single-node PostgreSQL (`oltp-hobby`): `db` for IAM and control, `operationsdb` for Operations; one shared bucket; one container each. | about $7.50 | any one container or `db` is down: sign-in stops |
|
|
50
|
+
| `standard` | Serious core. HA PostgreSQL (3 nodes, `oltp-production`, dedicated CPU) for IAM and control; a second HA PostgreSQL and bucket for Operations; two containers of IAM and the proxy. | about $100 | survives the loss of one node of each |
|
|
51
51
|
|
|
52
52
|
Most of `standard`'s price is the identity database's dedicated floor (about $72). `light` is right for an
|
|
53
53
|
evaluation and a small fleet that tolerates an outage; `standard` for sign-in that must stay up.
|
|
54
54
|
|
|
55
|
+
**What a lost node costs.** Zerops has no point-in-time recovery. Its backups are logical dumps of a whole
|
|
56
|
+
database service, daily between 00:00 and 01:00 UTC unless the service sets another schedule, and a restore is
|
|
57
|
+
manual (download, then `pg_restore`). A single-node PostgreSQL whose node fails loses everything written since
|
|
58
|
+
its last backup and stays down until someone restores it. An HA PostgreSQL (three nodes) keeps both its data and
|
|
59
|
+
its service. Zerops documents that a deploy starts the new container before it stops the old one, so a deploy
|
|
60
|
+
should not take a one-container runtime down; neither that nor how fast Zerops replaces a container lost with its
|
|
61
|
+
node is measured here. `platform install` and every `platform deploy` set the platform database `db` (users,
|
|
62
|
+
grants, deploy records) to back up hourly, unless someone chose another schedule; a light installation created
|
|
63
|
+
before `operationsdb` existed keeps its schedule. Hourly backups may shorten how far back the restore points
|
|
64
|
+
reach: if Zerops tags each one `daily`, the retention's daily count counts hours. Check `db`'s retention in the GUI. Operations' own `operationsdb`
|
|
65
|
+
keeps the daily default: its error history does not need that schedule.
|
|
66
|
+
|
|
67
|
+
**Presets.** The choices below combine into three shapes. Prices are the idle floor for three applications in
|
|
68
|
+
one production namespace; an application with a database runs at a 0.5 GB floor, about $2.10 per container.
|
|
69
|
+
|
|
70
|
+
| Preset | Platform tier | Production namespace | Idle floor | A lost node costs |
|
|
71
|
+
| ---------- | ------------- | --------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------- |
|
|
72
|
+
| evaluation | `light` | `cheap`, single-node PostgreSQL, one container each | about $16 | the data written since that database's last backup, and an outage |
|
|
73
|
+
| data | `light` | `cheap` with `--postgres-type=postgresql:ha@18 --postgres-profile=oltp-staging`, one container each | about $26 | applications' data survives; platform data since its last backup |
|
|
74
|
+
| stable | `standard` | `cheap` HA as above, `--core-package=SERIOUS`, two containers of each application | $130+ | a short failover; the namespace proxy has one container |
|
|
75
|
+
|
|
76
|
+
A staging namespace (`cheap`, single-node) adds about $9 to any of them.
|
|
77
|
+
|
|
55
78
|
**2. Namespace preset** (`control namespaces create --preset`, step 5). A namespace is the Zerops project
|
|
56
|
-
applications run in. Each one adds its own proxy, about $1.08.
|
|
79
|
+
applications run in. Each one adds its own proxy, about $1.08. A `cheap` namespace's shared PostgreSQL is
|
|
80
|
+
single-node unless `--postgres-type=postgresql:ha@18` asks for HA (about $11 at `--postgres-profile=oltp-staging`; the two flags go together);
|
|
81
|
+
the type is fixed once the namespace exists, the profile is not.
|
|
57
82
|
|
|
58
83
|
| Preset | Where an application's database lives | Use with |
|
|
59
84
|
| --------------- | ----------------------------------------------------------------------- | ------------------------------------ |
|
|
@@ -62,8 +87,8 @@ applications run in. Each one adds its own proxy, about $1.08.
|
|
|
62
87
|
| `full` | The namespace belongs to ONE application (`--exclusive-app=<id>`) | an application that must be isolated |
|
|
63
88
|
|
|
64
89
|
**3. The application's database** (the application's `fabrika.config.ts`, before step 5). The example
|
|
65
|
-
declares `postgresql:single@18` at `oltp-hobby`, about $1.45, plus its runtime
|
|
66
|
-
container. `postgresql:ha@18` survives a node failure: about $11 at `oltp-staging` (shared CPU), about $72 at
|
|
90
|
+
declares `postgresql:single@18` at `oltp-hobby`, about $1.45, plus its runtime at about $2.10 per
|
|
91
|
+
container (a deploy raises the floor of an application with a database to 0.5 GB). `postgresql:ha@18` survives a node failure: about $11 at `oltp-staging` (shared CPU), about $72 at
|
|
67
92
|
`oltp-production` (dedicated). Single to HA later means a new service and a data migration.
|
|
68
93
|
|
|
69
94
|
**4. Namespace core package** (`--core-package=LIGHT|SERIOUS`, step 5). `LIGHT` is free and the default on
|
|
@@ -127,10 +152,9 @@ fabrika platform install --provider=zerops --create-project --client-id=<clientI
|
|
|
127
152
|
over the same project would destroy the vault key. Start over in a new empty project.
|
|
128
153
|
- The account must be in the default region, or set `FABRIKA_ZEROPS_API_URL` for every command.
|
|
129
154
|
- Mail is optional. Without `--email-provider` the installation sends no mail. With one, add
|
|
130
|
-
`--email-provider=<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
files. They have no flag. The install writes the set on `iam` and `control`, the credentials sensitive. They
|
|
155
|
+
`--email-provider=<provider> --email-from=<address>`, optionally `--email-from-name=<name>`, and export the
|
|
156
|
+
provider's variables from their own `0600` files. `fabrika platform install --provider=zerops --help` lists the
|
|
157
|
+
providers and their variables, each named after its provider (for Resend `FABRIKA_EMAIL_RESEND_API_KEY`). They have no flag. The install writes the set on `iam` and `control`, the credentials sensitive. They
|
|
134
158
|
are the human's own, so they are not copied into the secrets file.
|
|
135
159
|
|
|
136
160
|
**Check.** The log ends with the `Hand off` step: where the credentials are, `The console answers at
|
|
@@ -352,7 +376,10 @@ fabrika control namespaces update --namespace=<id> --proxy-source=current
|
|
|
352
376
|
```
|
|
353
377
|
|
|
354
378
|
It re-pins the namespace and queues the reconcile that rebuilds its proxy; poll `namespaces get` until it is
|
|
355
|
-
`ready`.
|
|
379
|
+
`ready`. Re-pin every namespace after each upgrade. Until then, Control refuses a deploy into it that uses a form
|
|
380
|
+
newer than 0.6 (a public procedure, a file output, a capabilities requirement, a forwarded webhook `Authorization`),
|
|
381
|
+
also when its proxy reads the form, and a proxy older than the replacement of the caller's `Authorization` keeps
|
|
382
|
+
forwarding a caller's `px_` key to the application (`docs/reference/deployment-namespaces.md`). A namespace pinned to a release before the proxy manifest moved into the build archive carries it in a
|
|
356
383
|
service variable, which Zerops caps at about 32 KB: when a deploy fails with `serviceEnvValueTooLarge`, this is
|
|
357
384
|
the fix, and it also accepts a namespace in `failed`.
|
|
358
385
|
|
|
@@ -371,12 +398,15 @@ none, and `install` cannot be re-run.
|
|
|
371
398
|
2. Restart `iam` and `control`. Each reads the transport at boot. IAM refuses to start on an invalid one.
|
|
372
399
|
Control starts, logs a line that names the invalid variables, and refuses every deploy that needs the transport
|
|
373
400
|
until you fix them and restart it.
|
|
374
|
-
3. Redeploy every environment whose contract uses `installation` mail
|
|
375
|
-
|
|
376
|
-
`provider_unauthorized`.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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.
|
|
380
410
|
|
|
381
411
|
## When something fails
|
|
382
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}`
|