@fabrika/provider-zerops 0.4.1 → 0.4.3
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 +8 -7
- package/src/api/client.ts +11 -14
- package/src/api/polling.ts +42 -0
- package/src/api/readers.ts +9 -2
- package/src/api/service-env-limit.ts +29 -0
- package/src/api/service-env-response.ts +34 -2
- package/src/api/types.ts +39 -9
- package/src/api.ts +10 -1
- package/src/control.ts +23 -8
- package/src/execution.ts +41 -3
- package/src/index.ts +5 -0
- package/src/installation/admin.ts +4 -2
- package/src/installation/checker-release.ts +44 -0
- package/src/installation/deploy.ts +66 -22
- package/src/installation/generated/platform-console-schema.ts +18 -2
- package/src/installation/generated/platform-proxy-manifest.ts +20 -0
- package/src/installation/guide.md +319 -0
- package/src/installation/index.ts +42 -16
- package/src/installation/init-options.ts +2 -2
- package/src/installation/init.ts +8 -2
- package/src/installation/install-collaborators.ts +4 -0
- package/src/installation/install-environment.ts +52 -30
- package/src/installation/install-options.ts +20 -19
- package/src/installation/install-project.ts +23 -15
- package/src/installation/install-provisioning.ts +12 -12
- package/src/installation/install-secrets.ts +89 -0
- package/src/installation/install-token.ts +37 -0
- package/src/installation/install.ts +34 -21
- package/src/installation/machine-key.ts +3 -2
- package/src/installation/manifest.ts +29 -12
- package/src/installation/platform-archive.ts +87 -0
- package/src/installation/platform-upload.ts +2 -2
- package/src/installation/secrets.ts +3 -4
- package/src/installation/topology/setups.ts +15 -27
- package/src/installation/topology/topology.ts +105 -104
- package/src/managed-env.ts +11 -0
- package/src/manifest.ts +25 -3
- package/src/namespace/codec.ts +6 -1
- package/src/namespace/errors.ts +3 -1
- package/src/namespace/provisioning.ts +18 -5
- package/src/namespace/proxy-attempt.ts +56 -14
- package/src/namespace/update.ts +50 -8
- package/src/provider.ts +57 -13
- package/src/proxy-manifest-carrier.ts +32 -0
- package/src/source/port.ts +9 -1
|
@@ -35,10 +35,14 @@
|
|
|
35
35
|
// readiness gate.
|
|
36
36
|
|
|
37
37
|
import { FABRIKA_SERVICE_TABLE_DIGEST, readEnvironmentName } from '@fabrika/auth-core'
|
|
38
|
+
import { IAM_APP_ID, IAM_APP_SCHEMA } from '@fabrika/iam-contract'
|
|
38
39
|
import type { SchemaReconciler } from '@fabrika/provider-contract'
|
|
39
40
|
import { encodeProxyManifestJson, FABRIKA_PROXY_MANIFEST_JSON } from '@fabrika/proxy-core/manifest'
|
|
41
|
+
import { assertServiceEnvValueFits } from '../api'
|
|
40
42
|
import {
|
|
43
|
+
enableSubdomainAccessWhenHttp,
|
|
41
44
|
prepareZeropsDescriptor,
|
|
45
|
+
readableContent,
|
|
42
46
|
type Sleeper,
|
|
43
47
|
ZEROPS_ACTIVE,
|
|
44
48
|
ZEROPS_SERVICE_NOT_HTTP,
|
|
@@ -47,7 +51,10 @@ import {
|
|
|
47
51
|
ZeropsApiError,
|
|
48
52
|
type ZeropsNamespaceProxySource,
|
|
49
53
|
type ZeropsService,
|
|
54
|
+
type ZeropsServiceEnv,
|
|
50
55
|
} from '../index'
|
|
56
|
+
import { ZEROPS_PROXY_MANIFEST_PATH } from '../proxy-manifest-carrier'
|
|
57
|
+
import { assertCheckerPublished, type CheckerPublication } from './checker-release'
|
|
51
58
|
import type { PlatformDeployInput } from './deploy-options'
|
|
52
59
|
import { PLATFORM_CONSOLE_APP_SCHEMA } from './generated/platform-console-schema'
|
|
53
60
|
import { PLATFORM_PROXY_MANIFEST_TEMPLATE } from './generated/platform-proxy-manifest'
|
|
@@ -63,6 +70,7 @@ import {
|
|
|
63
70
|
import type { DeployLog } from './log'
|
|
64
71
|
import { mergePlatformProxyManifest, readLiveProxyManifest } from './manifest'
|
|
65
72
|
import { type IamOAuthConfig, platformIamOAuthConfig } from './mcp'
|
|
73
|
+
import { archiveWithDataFile } from './platform-archive'
|
|
66
74
|
import type { PreparedPlatformSource } from './platform-source'
|
|
67
75
|
import { type PlatformUploadFetch, uploadPlatformArchive } from './platform-upload'
|
|
68
76
|
import { platformProxyAppFor, type PlatformProxyComposition, platformServiceTableDigest, resolvePlatformProxyManifest } from './proxy-manifest'
|
|
@@ -139,6 +147,7 @@ export interface PlatformServiceDeployCollaborators {
|
|
|
139
147
|
/** Everything `deployPlatform` reaches the outside world through. */
|
|
140
148
|
export interface PlatformDeployCollaborators extends PlatformServiceDeployCollaborators {
|
|
141
149
|
readonly prepareSource: PlatformSourcePreparer
|
|
150
|
+
readonly checkerPublished: CheckerPublication
|
|
142
151
|
readonly reconcileSchema: SchemaReconciler
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -146,6 +155,16 @@ export interface PlatformDeployCollaborators extends PlatformServiceDeployCollab
|
|
|
146
155
|
export interface PlatformServiceDeployInput {
|
|
147
156
|
readonly source: PreparedPlatformSource
|
|
148
157
|
readonly dryRun: boolean
|
|
158
|
+
/** The proxy's manifest, which its archive carries as the one data file (ADR-0045); the proxy is refused without it. */
|
|
159
|
+
readonly proxyManifest?: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The archive one service builds from: the snapshot, and for the proxy the snapshot plus its manifest. */
|
|
163
|
+
const serviceArchive = async (service: ResolvedService, input: PlatformServiceDeployInput): Promise<Blob> => {
|
|
164
|
+
const snapshot = Bun.file(input.source.archivePath)
|
|
165
|
+
if (service.hostname !== PLATFORM_PROXY_SERVICE) return snapshot
|
|
166
|
+
if (input.proxyManifest === undefined) throw new Error('the proxy is built only with the manifest its archive carries')
|
|
167
|
+
return new Blob([archiveWithDataFile(await snapshot.bytes(), { path: ZEROPS_PROXY_MANIFEST_PATH, contents: input.proxyManifest })])
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
/** One resolved platform service. */
|
|
@@ -237,13 +256,16 @@ export const serviceOf = (services: ReadonlyMap<PlatformDeployService, ResolvedS
|
|
|
237
256
|
return service
|
|
238
257
|
}
|
|
239
258
|
|
|
240
|
-
/**
|
|
259
|
+
/** One service's environment by key. A sensitive record carries no value. Values are never logged. */
|
|
260
|
+
export type LiveServiceEnv = ReadonlyMap<string, ZeropsServiceEnv>
|
|
261
|
+
|
|
262
|
+
/** Read one service's environment by key. */
|
|
241
263
|
export const readServiceEnv = async (
|
|
242
264
|
serviceId: string,
|
|
243
265
|
{ api, signal }: Pick<PlatformServiceDeployCollaborators, 'api' | 'signal'>,
|
|
244
|
-
): Promise<
|
|
266
|
+
): Promise<LiveServiceEnv> => {
|
|
245
267
|
const variables = await api.listServiceEnv({ serviceId, signal })
|
|
246
|
-
return new Map(variables.map((variable) => [variable.key, variable
|
|
268
|
+
return new Map(variables.map((variable) => [variable.key, variable]))
|
|
247
269
|
}
|
|
248
270
|
|
|
249
271
|
/**
|
|
@@ -254,11 +276,11 @@ export const readServiceEnv = async (
|
|
|
254
276
|
* `custom-domain` installation, where the domains are bound to the project balancer out of band and
|
|
255
277
|
* publishing a subdomain would add a public address nobody asked for.
|
|
256
278
|
*/
|
|
257
|
-
const resolvePlacement = (input: PlatformDeployInput, proxyEnv:
|
|
279
|
+
const resolvePlacement = (input: PlatformDeployInput, proxyEnv: LiveServiceEnv): PlatformPlacement => {
|
|
258
280
|
if (input.hosts !== undefined) {
|
|
259
281
|
return { hosts: input.hosts, scheme: input.scheme, zeropsSubdomain: false }
|
|
260
282
|
}
|
|
261
|
-
const subdomains = proxyEnv.get(ZEROPS_SUBDOMAIN_VARIABLE)
|
|
283
|
+
const subdomains = readableContent(proxyEnv.get(ZEROPS_SUBDOMAIN_VARIABLE))
|
|
262
284
|
if (subdomains === undefined || subdomains.trim() === '') {
|
|
263
285
|
throw new Error(
|
|
264
286
|
'the proxy service publishes no Zerops subdomain — deploy it once so it has HTTP ports, or name every host '
|
|
@@ -281,7 +303,6 @@ const IAM_OAUTH_CONFIG = 'FABRIKA_IAM_OAUTH_CONFIG'
|
|
|
281
303
|
const desiredServiceEnv = (
|
|
282
304
|
environment: string,
|
|
283
305
|
placement: PlatformPlacement,
|
|
284
|
-
manifestJson: string,
|
|
285
306
|
proxySource: ZeropsNamespaceProxySource,
|
|
286
307
|
oauthConfig: IamOAuthConfig | undefined,
|
|
287
308
|
controlTableDigest: string,
|
|
@@ -314,7 +335,6 @@ const desiredServiceEnv = (
|
|
|
314
335
|
// Compared byte-for-byte against a token's `iss`, so this is the PUBLIC issuer and not the
|
|
315
336
|
// private `iam:3000` the manifest dials — `readProxyEnv` canonicalizes it to an origin.
|
|
316
337
|
['FABRIKA_IAM_ISSUER', iamOrigin],
|
|
317
|
-
[FABRIKA_PROXY_MANIFEST_JSON, manifestJson],
|
|
318
338
|
['FABRIKA_PROXY_SESSION_OWNER_JSON', JSON.stringify({ issuer: iamOrigin, upstream: PRIVATE_IAM_UPSTREAM })],
|
|
319
339
|
]),
|
|
320
340
|
],
|
|
@@ -338,25 +358,32 @@ const desiredServiceEnv = (
|
|
|
338
358
|
])
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* Write only what differs. Returns the KEYS written — never a value, not even in a debug line.
|
|
363
|
+
*
|
|
364
|
+
* Nothing here is a credential, so everything is written readable. A record an older release wrote
|
|
365
|
+
* sensitive reads as different and is rewritten readable, which is what lets the next deploy compare it.
|
|
366
|
+
*/
|
|
342
367
|
const applyServiceEnv = async (
|
|
343
368
|
service: ResolvedService,
|
|
344
369
|
desired: ReadonlyMap<string, string>,
|
|
345
|
-
live:
|
|
370
|
+
live: LiveServiceEnv,
|
|
346
371
|
input: PlatformDeployInput,
|
|
347
372
|
collaborators: PlatformDeployCollaborators,
|
|
348
373
|
): Promise<string[]> => {
|
|
349
374
|
const written: string[] = []
|
|
350
375
|
for (const [key, value] of desired) {
|
|
351
|
-
if (live.get(key) === value) {
|
|
376
|
+
if (readableContent(live.get(key)) === value) {
|
|
352
377
|
continue
|
|
353
378
|
}
|
|
354
379
|
written.push(key)
|
|
380
|
+
// The client refuses it too; checking here lets a dry run report what the real write would refuse.
|
|
381
|
+
assertServiceEnvValueFits(key, value)
|
|
355
382
|
if (input.dryRun) {
|
|
356
383
|
continue
|
|
357
384
|
}
|
|
358
385
|
assertRunning(collaborators.signal)
|
|
359
|
-
await collaborators.api.putServiceEnv({ serviceId: service.id, key, value, signal: collaborators.signal })
|
|
386
|
+
await collaborators.api.putServiceEnv({ serviceId: service.id, key, value, sensitive: false, signal: collaborators.signal })
|
|
360
387
|
}
|
|
361
388
|
return written
|
|
362
389
|
}
|
|
@@ -434,11 +461,12 @@ export const deployPlatformService = async (
|
|
|
434
461
|
return
|
|
435
462
|
}
|
|
436
463
|
prepareZeropsDescriptor(input.source.descriptor.contents, service.hostname)
|
|
464
|
+
const archive = await serviceArchive(service, input)
|
|
437
465
|
const reserved = await api.createAppVersion({ serviceId: service.id, name: input.source.commitSha, signal })
|
|
438
466
|
const appVersionId = reserved.id
|
|
439
467
|
log.info(`${service.hostname}: app-version ${appVersionId}, commit ${input.source.commitSha}, setup ${service.hostname}`)
|
|
440
468
|
try {
|
|
441
|
-
await uploadPlatformArchive(reserved.uploadUrl,
|
|
469
|
+
await uploadPlatformArchive(reserved.uploadUrl, archive, signal, collaborators.uploadFetch)
|
|
442
470
|
assertRunning(signal)
|
|
443
471
|
} catch {
|
|
444
472
|
await cleanupReservation(service, appVersionId, collaborators)
|
|
@@ -502,7 +530,8 @@ const deployConcurrently = async (
|
|
|
502
530
|
* The decision is the READ-BACK and never the call returning: on an already-published service the
|
|
503
531
|
* platform answers 2xx and then fails the process it hands back, and a freshly-enabled one can still
|
|
504
532
|
* read `false` on the next read. Both live-verified. Runs on every deploy, so a subdomain someone
|
|
505
|
-
* turned off in the GUI comes back, and throws rather than leaving the installation unreachable.
|
|
533
|
+
* turned off in the GUI comes back, and throws rather than leaving the installation unreachable. The
|
|
534
|
+
* HTTP ports lag a fresh ACTIVE version, so `serviceStackIsNotHttp` is retried for a bounded window first.
|
|
506
535
|
*/
|
|
507
536
|
const ensureSubdomainAccess = async (
|
|
508
537
|
proxy: ResolvedService,
|
|
@@ -519,11 +548,11 @@ const ensureSubdomainAccess = async (
|
|
|
519
548
|
return
|
|
520
549
|
}
|
|
521
550
|
try {
|
|
522
|
-
await
|
|
551
|
+
await enableSubdomainAccessWhenHttp({ api, serviceId: proxy.id, sleep, signal })
|
|
523
552
|
} catch (error) {
|
|
524
553
|
if (error instanceof ZeropsApiError && error.code === ZEROPS_SERVICE_NOT_HTTP) {
|
|
525
554
|
throw new Error(
|
|
526
|
-
`the proxy cannot be published on a zerops.app subdomain: it exposes no deployed HTTP port (${ZEROPS_SERVICE_NOT_HTTP})`,
|
|
555
|
+
`the proxy cannot be published on a zerops.app subdomain: it still exposes no deployed HTTP port (${ZEROPS_SERVICE_NOT_HTTP}) minutes after its deploy`,
|
|
527
556
|
)
|
|
528
557
|
}
|
|
529
558
|
throw error
|
|
@@ -569,6 +598,7 @@ export const deployPlatform = async (
|
|
|
569
598
|
const source = await collaborators.prepareSource(input.sourceDir, collaborators.signal)
|
|
570
599
|
try {
|
|
571
600
|
validateInstallationSetups(source, PLATFORM_DEPLOY_ORDER)
|
|
601
|
+
await assertCheckerPublished(collaborators.checkerPublished, collaborators.signal)
|
|
572
602
|
await deployPreparedPlatform(input, collaborators, source)
|
|
573
603
|
} finally {
|
|
574
604
|
await source.dispose()
|
|
@@ -617,6 +647,7 @@ export const deployPreparedPlatform = async (
|
|
|
617
647
|
? 'MCP authoring: off'
|
|
618
648
|
: `MCP authoring: on, ${oauthConfig.clients.length} client(s), resource(s) ${oauthConfig.resources.map((entry) => entry.resource).join(', ')}`,
|
|
619
649
|
)
|
|
650
|
+
// The variable is the application entries the platform proxy carries, held by the operator; nothing here writes it.
|
|
620
651
|
const merged = mergePlatformProxyManifest(platformManifest, readLiveProxyManifest(proxyEnv.get(FABRIKA_PROXY_MANIFEST_JSON)))
|
|
621
652
|
log.info(`${merged.manifest.apps.length} app(s): ${merged.manifest.apps.map((app) => app.id).join(', ')}`)
|
|
622
653
|
if (merged.carried.length > 0) {
|
|
@@ -631,12 +662,14 @@ export const deployPreparedPlatform = async (
|
|
|
631
662
|
|
|
632
663
|
log.step('Write the service environment')
|
|
633
664
|
const controlTableDigest = platformServiceTableDigest(platformManifest, platformProxyAppFor(PLATFORM_PROXY_MANIFEST_TEMPLATE, 'control').id)
|
|
634
|
-
const desired = desiredServiceEnv(environment, placement,
|
|
665
|
+
const desired = desiredServiceEnv(environment, placement, proxySource, oauthConfig, controlTableDigest)
|
|
635
666
|
for (const hostname of PLATFORM_DEPLOY_ORDER) {
|
|
636
667
|
const service = serviceOf(services, hostname)
|
|
637
668
|
const live = hostname === PLATFORM_PROXY_SERVICE ? proxyEnv : await readServiceEnv(service.id, collaborators)
|
|
638
669
|
const keys = desired.get(hostname) ?? new Map<string, string>()
|
|
639
|
-
|
|
670
|
+
const liveOAuthConfig = live.get(IAM_OAUTH_CONFIG)
|
|
671
|
+
const oauthStillSet = liveOAuthConfig !== undefined && (liveOAuthConfig.sensitive || liveOAuthConfig.content.trim() !== '')
|
|
672
|
+
if (hostname === 'iam' && oauthConfig === undefined && oauthStillSet) {
|
|
640
673
|
log.warn(`iam: MCP authoring is off but ${IAM_OAUTH_CONFIG} is still set; IAM keeps issuing MCP tokens until it is removed`)
|
|
641
674
|
}
|
|
642
675
|
const written = await applyServiceEnv(service, keys, live, input, collaborators)
|
|
@@ -647,11 +680,12 @@ export const deployPreparedPlatform = async (
|
|
|
647
680
|
)
|
|
648
681
|
}
|
|
649
682
|
|
|
650
|
-
// EVERY deploy is after EVERY write
|
|
651
|
-
//
|
|
652
|
-
//
|
|
683
|
+
// EVERY deploy is after EVERY write, and the manifest rides in the proxy's own archive. That is what makes
|
|
684
|
+
// the sequence fail closed: a proxy build that does not go ACTIVE leaves the previous manifest in front of
|
|
685
|
+
// the previous code, because control builds only after it — and `ENVIRONMENT` is in place before any
|
|
686
|
+
// service that refuses `local` restarts.
|
|
653
687
|
log.step(`Deploy ${PLATFORM_CONCURRENT_DEPLOY.join(' + ')} together, then ${PLATFORM_SEQUENTIAL_DEPLOY.join(' → ')}`)
|
|
654
|
-
const buildInput = { source, dryRun: input.dryRun }
|
|
688
|
+
const buildInput = { source, dryRun: input.dryRun, proxyManifest: encodeProxyManifestJson(merged.manifest) }
|
|
655
689
|
await deployConcurrently(services, buildInput, collaborators)
|
|
656
690
|
for (const hostname of PLATFORM_SEQUENTIAL_DEPLOY) {
|
|
657
691
|
await deployPlatformService(serviceOf(services, hostname), buildInput, collaborators)
|
|
@@ -669,11 +703,12 @@ export const deployPreparedPlatform = async (
|
|
|
669
703
|
log.info('custom-domain installation: the operator binds each domain to the project balancer; no subdomain is published')
|
|
670
704
|
}
|
|
671
705
|
|
|
672
|
-
log.step('Register the console with IAM')
|
|
706
|
+
log.step('Register the console and IAM with IAM')
|
|
673
707
|
const consoleApp = platformProxyAppFor(PLATFORM_PROXY_MANIFEST_TEMPLATE, 'control')
|
|
674
708
|
const consoleOrigin = platformOrigin(placement.scheme, placement.hosts.control)
|
|
675
709
|
if (input.dryRun) {
|
|
676
710
|
log.info(`would reconcile \`${consoleApp.id}\` and register its return origin ${consoleOrigin}`)
|
|
711
|
+
log.info(`would reconcile \`${IAM_APP_ID}\`'s own vocabulary`)
|
|
677
712
|
} else {
|
|
678
713
|
// The schema PUT is what REGISTERS the app, and `apps.setReturnOrigins` 404s for an app IAM has
|
|
679
714
|
// never heard of — so both travel in this one call, in that order (ADR-0022/ADR-0023).
|
|
@@ -686,6 +721,15 @@ export const deployPreparedPlatform = async (
|
|
|
686
721
|
signal,
|
|
687
722
|
})
|
|
688
723
|
log.info(`\`${consoleApp.id}\` reconciled; return origin ${consoleOrigin}`)
|
|
724
|
+
// IAM's own vocabulary, which `user-admin` is granted from (ADR-0058). It has no return origin.
|
|
725
|
+
await collaborators.reconcileSchema({
|
|
726
|
+
url: platformOrigin(placement.scheme, placement.hosts.iam),
|
|
727
|
+
app: IAM_APP_ID,
|
|
728
|
+
schema: IAM_APP_SCHEMA,
|
|
729
|
+
adminKey: input.iamAdminKey,
|
|
730
|
+
signal,
|
|
731
|
+
})
|
|
732
|
+
log.info(`\`${IAM_APP_ID}\` reconciled`)
|
|
689
733
|
}
|
|
690
734
|
|
|
691
735
|
log.step(input.dryRun ? 'Dry run complete — nothing was written' : 'Installation deployed')
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { AppSchema } from '@fabrika/auth-core'
|
|
12
12
|
|
|
13
13
|
export const PLATFORM_CONSOLE_APP_SCHEMA: AppSchema = {
|
|
14
|
+
displayName: 'Console',
|
|
14
15
|
scopes: [
|
|
15
16
|
{ type: 'app', label: 'App' },
|
|
16
17
|
{ type: 'environment', label: 'Environment' },
|
|
@@ -31,14 +32,29 @@ export const PLATFORM_CONSOLE_APP_SCHEMA: AppSchema = {
|
|
|
31
32
|
{ action: 'operations.manage', description: 'Manage Operations sources, alerts, and retention' },
|
|
32
33
|
],
|
|
33
34
|
roles: {
|
|
35
|
+
observer: {
|
|
36
|
+
name: 'Observer',
|
|
37
|
+
description: 'Read Operations errors and activity.',
|
|
38
|
+
permissions: ['operations.read'],
|
|
39
|
+
},
|
|
40
|
+
triager: {
|
|
41
|
+
name: 'Triager',
|
|
42
|
+
description: 'Read and triage Operations errors.',
|
|
43
|
+
permissions: ['operations.read', 'operations.triage'],
|
|
44
|
+
},
|
|
45
|
+
deployer: {
|
|
46
|
+
name: 'Deployer',
|
|
47
|
+
description: 'Trigger, read and cancel deploys (no registry, namespace or secret management).',
|
|
48
|
+
permissions: ['deploy.*'],
|
|
49
|
+
},
|
|
34
50
|
operator: {
|
|
35
51
|
name: 'Operator',
|
|
36
|
-
description: '
|
|
52
|
+
description: 'Deploy, and read, triage and manage Operations (no registry or secret management).',
|
|
37
53
|
permissions: ['deploy.*', 'operations.*'],
|
|
38
54
|
},
|
|
39
55
|
admin: {
|
|
40
56
|
name: 'Admin',
|
|
41
|
-
description: '
|
|
57
|
+
description: 'Every console action in every scope.',
|
|
42
58
|
permissions: ['*'],
|
|
43
59
|
},
|
|
44
60
|
},
|
|
@@ -278,6 +278,16 @@ export const PLATFORM_PROXY_MANIFEST_TEMPLATE: PlatformProxyManifestTemplate = {
|
|
|
278
278
|
admission: null,
|
|
279
279
|
oauth: null,
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
id: 'github-setup',
|
|
283
|
+
kind: 'service',
|
|
284
|
+
method: 'GET',
|
|
285
|
+
path: '/api/source/github/setup/:connectionId',
|
|
286
|
+
callers: ['human'],
|
|
287
|
+
requirement: { kind: 'authenticated' },
|
|
288
|
+
admission: null,
|
|
289
|
+
oauth: null,
|
|
290
|
+
},
|
|
281
291
|
{
|
|
282
292
|
id: 'rpc',
|
|
283
293
|
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: 'github-setup',
|
|
468
|
+
kind: 'service',
|
|
469
|
+
method: 'GET',
|
|
470
|
+
path: '/api/source/github/setup/:connectionId',
|
|
471
|
+
callers: ['human'],
|
|
472
|
+
requirement: { kind: 'authenticated' },
|
|
473
|
+
admission: null,
|
|
474
|
+
oauth: null,
|
|
475
|
+
},
|
|
456
476
|
{
|
|
457
477
|
id: 'rpc',
|
|
458
478
|
kind: 'service',
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Installing fabrika on Zerops — the operator's guide
|
|
2
|
+
|
|
3
|
+
From an empty Zerops account to a serving application. This guide ships with the CLI, so it matches the
|
|
4
|
+
release you run. It gives the order and the traps. Each command's flags are in its own help, and that text
|
|
5
|
+
is authoritative:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
fabrika platform --provider=zerops --help
|
|
9
|
+
fabrika control --help
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run the steps in order and check each one before the next. Every command refuses a missing value with a
|
|
13
|
+
message that names the flag or the variable to set. Read the message: it names the fix.
|
|
14
|
+
|
|
15
|
+
## Before you start
|
|
16
|
+
|
|
17
|
+
**Ask the human for these first.** You cannot produce them, and each one blocks a later step:
|
|
18
|
+
|
|
19
|
+
| What | Needed at |
|
|
20
|
+
| ------------------------------------------------------------------------------------- | ---------- |
|
|
21
|
+
| A Zerops PERSONAL access token of a client OWNER, generated in the Zerops GUI | step 1 |
|
|
22
|
+
| A verified Zerops account (credit topped up once); an unverified one owns ONE project | step 5 |
|
|
23
|
+
| A `gh` login allowed to create a repository in the organization that owns it | step 3 |
|
|
24
|
+
| The administrator's e-mail address, and the human to open the enrollment URL | step 2 |
|
|
25
|
+
| For a PRIVATE application repository: the GitHub App connected in the console | step 5 |
|
|
26
|
+
| The fabrika release tag to install, for example `v0.4.3` (a branch is refused) | steps 1, 3 |
|
|
27
|
+
| The four decisions below | steps 1, 5 |
|
|
28
|
+
|
|
29
|
+
Every namespace is a Zerops project of its own, so the first one fails with `clientProjectCountLimitExceededTopUp`
|
|
30
|
+
on an unverified account (observed 2026-10-15).
|
|
31
|
+
|
|
32
|
+
Also needed on the machine: [Bun](https://bun.sh) ≥ 1.3, `git`, `gh`, `curl` and `jq`.
|
|
33
|
+
|
|
34
|
+
## Decisions: ask the human, with the prices
|
|
35
|
+
|
|
36
|
+
Each choice trades price against stability, and two of them cannot be undone. Show the human this section
|
|
37
|
+
and let them choose; do not pick for them. Prices are Zerops' list prices as of 2026-10-15
|
|
38
|
+
([zerops.io/pricing](https://zerops.io/pricing)): a shared core $0.60, a dedicated core $6.00, RAM $3.00 per
|
|
39
|
+
GB, disk $0.10 per GB and object storage $0.01 per GB, each per 30 days and billed per minute. A Lightweight
|
|
40
|
+
project core is free (15 build hours, 100 GB egress); a Serious one is $10 (150 build hours, 3 TB egress, a
|
|
41
|
+
highly available balancer). The figures below are the idle FLOOR. Every service scales up under load, to 8
|
|
42
|
+
cores and 48 GB per container, and costs more while it does.
|
|
43
|
+
|
|
44
|
+
**1. Platform tier** (`platform install --tier`, step 1). Cannot be changed later: a project's core package
|
|
45
|
+
is upgrade-only, and the data services differ.
|
|
46
|
+
|
|
47
|
+
| Tier | What runs | Idle floor | Fails when |
|
|
48
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------- |
|
|
49
|
+
| `light` (default) | Lightweight core. One single-node PostgreSQL (`oltp-hobby`) and one bucket shared by IAM, control and Operations; one container each. | about $6 | any one container or the database 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
|
+
|
|
52
|
+
Most of `standard`'s price is the identity database's dedicated floor (about $72). `light` is right for an
|
|
53
|
+
evaluation and a small fleet that tolerates an outage; `standard` for sign-in that must stay up.
|
|
54
|
+
|
|
55
|
+
**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.
|
|
57
|
+
|
|
58
|
+
| Preset | Where an application's database lives | Use with |
|
|
59
|
+
| --------------- | ----------------------------------------------------------------------- | ------------------------------------ |
|
|
60
|
+
| `cheap` | One shared single-node PostgreSQL in the namespace, about $1.45 for all | `fabrika.cheap.config.ts` |
|
|
61
|
+
| `mid` (default) | Each application imports its own database into the namespace | `fabrika.config.ts` |
|
|
62
|
+
| `full` | The namespace belongs to ONE application (`--exclusive-app=<id>`) | an application that must be isolated |
|
|
63
|
+
|
|
64
|
+
**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 from about $1.08 per
|
|
66
|
+
container. `postgresql:ha@18` survives a node failure: about $11 at `oltp-staging` (shared CPU), about $72 at
|
|
67
|
+
`oltp-production` (dedicated). Single to HA later means a new service and a data migration.
|
|
68
|
+
|
|
69
|
+
**4. Namespace core package** (`--core-package=LIGHT|SERIOUS`, step 5). `LIGHT` is free and the default on
|
|
70
|
+
every environment. `SERIOUS` adds $10 per namespace and a highly available balancer in front of its proxy,
|
|
71
|
+
and it can never be downgraded again.
|
|
72
|
+
|
|
73
|
+
## Secrets: rules for an agent
|
|
74
|
+
|
|
75
|
+
- Every credential is read from the ENVIRONMENT ONLY. No command takes one as a flag.
|
|
76
|
+
- Never print a credential, never put one in a command line you echo, never commit one.
|
|
77
|
+
- `platform install` writes every credential it creates to `--secrets-file`, mode `0600`, before it places
|
|
78
|
+
the first one. That file is the ONLY readable copy. Zerops stores them as sensitive variables and answers
|
|
79
|
+
`REDACTED` when they are read back, so never try to read a secret from the Zerops API, and never ask the
|
|
80
|
+
human for sudo mode to do it. Back the file up where the human keeps credentials.
|
|
81
|
+
- Load the file in a subshell, so its integration token never replaces the personal one in your shell:
|
|
82
|
+
`( set -a && . <secrets file> && set +a && fabrika … )`.
|
|
83
|
+
- Keep any other credential in its own `0600` file (`umask 077` first) and pass it as `NAME="$(cat <file>)"`.
|
|
84
|
+
Check a file with `wc -c`, never with `cat`.
|
|
85
|
+
|
|
86
|
+
## Step 0. Get the CLI at the release tag
|
|
87
|
+
|
|
88
|
+
`platform install` and `platform deploy` upload the source of the checkout the CLI runs from, and refuse
|
|
89
|
+
anything else (`--source-dir`). So the bring-up runs from a clean clone at the tag, not from a CLI installed
|
|
90
|
+
with `bun add --global`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git clone --depth 1 --branch <tag> https://github.com/contember/fabrika-platform.git fabrika-platform
|
|
94
|
+
cd fabrika-platform && bun install --frozen-lockfile
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
In this guide, `fabrika` means `bun <clone>/packages/cli/src/index.ts`. Do not edit the clone: a dirty
|
|
98
|
+
checkout is refused, and a local commit is not on GitHub, so namespaces cannot fetch it.
|
|
99
|
+
|
|
100
|
+
Install a released tag, not a branch. Every application build fetches `@fabrika/check` at the version of the
|
|
101
|
+
checkout, so `install` and `deploy` refuse a checkout whose version npm does not have yet: `this checkout is
|
|
102
|
+
version X, which is not published yet`. A `main` between a release's version bump and its publish is such a
|
|
103
|
+
checkout.
|
|
104
|
+
|
|
105
|
+
## Step 1. Install the platform
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
export FABRIKA_ZEROPS_ACCESS_TOKEN="$(cat <personal token file>)"
|
|
109
|
+
curl -s -H "Authorization: Bearer $FABRIKA_ZEROPS_ACCESS_TOKEN" \
|
|
110
|
+
https://api.app-prg1.zerops.io/api/rest/public/user/info | jq '{email, clients: [.clientUserList[] | {clientId, name: .client.accountName, role: .roleCode}]}'
|
|
111
|
+
fabrika platform install --provider=zerops --create-project --client-id=<clientId> --env=<name> --tier=<light|standard> \
|
|
112
|
+
--secrets-file=<path outside the clone> --source-dir=. --yes > install.log 2>&1
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- Check the token kind first. The `email` of a personal token is the person's address, and its `role` on the
|
|
116
|
+
client is `OWNER`. An `email` of `token-<id>@zerops.io` is an INTEGRATION token. The install refuses one before
|
|
117
|
+
it creates anything, because an integration token may not mint the token the control plane needs. Ask the
|
|
118
|
+
human for a personal token instead.
|
|
119
|
+
- Run it from the clone root. It runs for many minutes: it imports the tier's services, builds the proxy
|
|
120
|
+
once to learn the public hosts, then deploys everything.
|
|
121
|
+
- `--yes` is required when stdin is not a terminal.
|
|
122
|
+
- `--secrets-file` names a NEW file outside the clone, for example `~/fabrika-<env>.secrets.env`. An existing
|
|
123
|
+
file is refused. The log prints no credential.
|
|
124
|
+
- The project id is printed BEFORE the long wait. If the run is interrupted, re-run with
|
|
125
|
+
`--project-id=<id>` instead of `--create-project`.
|
|
126
|
+
- A project that already holds generated secrets is refused. Do not try to force it: a second bring-up
|
|
127
|
+
over the same project would destroy the vault key. Start over in a new empty project.
|
|
128
|
+
- The account must be in the default region, or set `FABRIKA_ZEROPS_API_URL` for every command.
|
|
129
|
+
|
|
130
|
+
**Check.** The log ends with the `Hand off` step: where the credentials are, `The console answers at
|
|
131
|
+
https://<console host>`, and an `ADMIT YOURSELF` box that holds the step 2 command with `--iam-host` filled
|
|
132
|
+
in. The secrets file exists with mode `0600`. The three hosts are printed earlier, right after pass 1, as
|
|
133
|
+
`✓ iam <host>`, `✓ console <host>` and `✓ operations <host>`. Write them down.
|
|
134
|
+
|
|
135
|
+
## Step 2. Admit the first administrator
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
( set -a && . <secrets file> && set +a && \
|
|
139
|
+
fabrika platform admin --provider=zerops --email=<address> --iam-host=<IAM host from step 1> )
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
It prints one enrollment URL and the time it expires. Nothing is e-mailed: the installation has no mail
|
|
143
|
+
service, so do not tell the human to wait for a message. The URL is a credential: give it to the human only,
|
|
144
|
+
and do not log it. The human opens it, sets a password and signs in at the console origin.
|
|
145
|
+
|
|
146
|
+
The URL is valid for ONE HOUR. Hand it over at once. A re-run creates nothing new: while the enrollment is
|
|
147
|
+
unused it only reports that one is outstanding, and it never prints the URL again. To get a new URL, after
|
|
148
|
+
expiry or after a lost one, run the same command with `--reissue`. It replaces the old enrollment, so the old
|
|
149
|
+
URL stops working.
|
|
150
|
+
|
|
151
|
+
## Step 3. Create the sidecar repository
|
|
152
|
+
|
|
153
|
+
The sidecar is a GitHub repository the operator owns. Its workflow runs `platform deploy` at the pinned tag.
|
|
154
|
+
|
|
155
|
+
Steps 4 and 5 do not depend on this step: the installation from step 1 already runs. The sidecar is what
|
|
156
|
+
upgrades it later (step 6). Run this step before or after step 5. Only `platform upgrade` needs it.
|
|
157
|
+
|
|
158
|
+
`init` takes the integration token step 1 minted, never the personal one. The secrets file holds it as
|
|
159
|
+
`FABRIKA_ZEROPS_ACCESS_TOKEN`, beside the provisioning key. Run `init` from a directory that is NOT inside
|
|
160
|
+
any git repository. The fabrika clone is a repository, and a home directory under dotfiles version control is
|
|
161
|
+
one too. Otherwise pass `--checkout=<dir>` outside one:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
( set -a && . <secrets file> && set +a && \
|
|
165
|
+
fabrika platform init --provider=zerops <installation> --repo=<owner>/<name> --tag=<tag> \
|
|
166
|
+
--project-id=<project id> --env=<name from step 1> --yes < /dev/null )
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- Without `--repo` the repository is `contember/fabrika-zerops-<installation>`. Pass `--repo` for any
|
|
170
|
+
other organization.
|
|
171
|
+
- Keep the local checkout path. `platform upgrade` needs it later (`--sidecar=<dir>`).
|
|
172
|
+
|
|
173
|
+
**Check.** The `platform` workflow run in the sidecar repository ends green:
|
|
174
|
+
`gh run list --repo <owner>/<name>`.
|
|
175
|
+
|
|
176
|
+
## Step 4. Mint a machine key for `fabrika control`
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
( umask 077 && set -a && . <secrets file> && set +a && FABRIKA_IAM_RPC_URL=https://<IAM host> \
|
|
180
|
+
fabrika control key issue --label=<name> --permissions=app.manage,namespace.manage,deploy.trigger,deploy.read \
|
|
181
|
+
--expires-in=2592000 > control.key )
|
|
182
|
+
wc -c control.key && ls -l control.key
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The secrets file supplies `FABRIKA_IAM_RPC_KEY` and `FABRIKA_IAM_PROVISIONING_KEY`.
|
|
186
|
+
|
|
187
|
+
- stdout carries the key and nothing else; the credential id goes to stderr. Keep the redirect inside the
|
|
188
|
+
`umask 077` subshell: `control.key` is then created with mode `0600` (`-rw-------`). Outside it, the file
|
|
189
|
+
is readable by other users.
|
|
190
|
+
- `FABRIKA_IAM_RPC_URL` is IAM's PUBLIC `https://` origin. The private `http://iam:3000` answers only inside
|
|
191
|
+
the Zerops project.
|
|
192
|
+
- A key expires after at most 30 days. Mint a new one when it does.
|
|
193
|
+
|
|
194
|
+
For every `fabrika control` command from here on:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
export FABRIKA_CONTROL_URL=https://<console host>
|
|
198
|
+
FABRIKA_CONTROL_KEY="$(cat control.key)" fabrika control apps list
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Check.** On a fresh installation `apps list` prints `no applications registered yet` (on stderr) and
|
|
202
|
+
exits 0. An authentication error means the key or the console origin is wrong.
|
|
203
|
+
|
|
204
|
+
## Step 5. Deploy the first application
|
|
205
|
+
|
|
206
|
+
The example is [`contember/fabrika-example-zerops`](https://github.com/contember/fabrika-example-zerops).
|
|
207
|
+
Its `fabrika.config.ts` places the app with its own database (`mid` preset), and `fabrika.cheap.config.ts`
|
|
208
|
+
places it on the namespace's shared PostgreSQL (`cheap` preset). Use the config that matches the preset.
|
|
209
|
+
|
|
210
|
+
**Repository access decides how you register** (step 5.2). Check it first:
|
|
211
|
+
`curl -s -o /dev/null -w '%{http_code}\n' https://api.github.com/repos/<owner>/<name>`.
|
|
212
|
+
|
|
213
|
+
- `200`: the repository is public. Register it with `--installation-id=none`. Control reads it without a token.
|
|
214
|
+
- `404`: GitHub hides it from an anonymous caller, so it is private (or the name is wrong). `register` refuses
|
|
215
|
+
it with `--installation-id=none`. Ask the human to connect the GitHub App in the console first (Settings →
|
|
216
|
+
Source, a browser flow), then register without `--installation-id`.
|
|
217
|
+
|
|
218
|
+
**Use the installation's release in the application.** The application's `@fabrika/*` packages, the CLI
|
|
219
|
+
among them, must be the version of the tag you installed: for `v0.4.3`, `^0.4.3`. Another version can build
|
|
220
|
+
a manifest this installation's control refuses or reads differently. From the fabrika clone of step 0, run
|
|
221
|
+
`bun scripts/release.ts example-pin <application checkout> --tag=<tag>`. It rewrites every `@fabrika/*` range
|
|
222
|
+
and commits nothing. Commit and push the change in the application repository, then run `bun install` there.
|
|
223
|
+
Check with `bun pm ls | grep @fabrika/cli`.
|
|
224
|
+
|
|
225
|
+
1. **Create a namespace** with the preset and core package the human chose (see Decisions). Without a
|
|
226
|
+
custom domain bound to its proxy, pass `--public-access=zerops-subdomain`; the default is
|
|
227
|
+
`custom-domain`, which serves no host until you bind one.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
fabrika control namespaces plan --namespace=<id> --env=<name> --preset=<cheap|mid|full> --public-access=zerops-subdomain
|
|
231
|
+
fabrika control namespaces create --namespace=<id> --env=<name> --preset=<cheap|mid|full> --public-access=zerops-subdomain
|
|
232
|
+
fabrika control namespaces get --namespace=<id>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`create` returns while the state is `pending`. Provisioning takes several minutes. Poll `get` every
|
|
236
|
+
30 seconds until it is `ready`.
|
|
237
|
+
|
|
238
|
+
- If the state is `failed`, `get` prints the cause and the command that resumes it on stderr. Run
|
|
239
|
+
`fabrika control namespaces reconcile --namespace=<id>` and poll `get` again. Provisioning resumes from
|
|
240
|
+
where it stopped. Do not create a second namespace.
|
|
241
|
+
- `get` lists one host per line as `<host> <port> free` or `<host> <port> taken by <app>/<env>`. A
|
|
242
|
+
`zerops-subdomain` namespace has six hosts, one per public port of its proxy: 8080 and 8082–8086. No
|
|
243
|
+
port is special or reserved; 8081 is the proxy's health port and is never listed. Use ANY `free` host,
|
|
244
|
+
for example the first one. Each host serves exactly one application environment, so six is the limit
|
|
245
|
+
per namespace.
|
|
246
|
+
|
|
247
|
+
2. **Build and register**, from the application's repository root. The repository must be pushed: control
|
|
248
|
+
deploys the commit on GitHub, not your working tree.
|
|
249
|
+
|
|
250
|
+
`--app=<id>` is the application id the manifest carries: `jq -r .app.id fabrika.manifest.json` after the
|
|
251
|
+
build. The application's `fabrika.config.ts` sets it (`defineApp({ id })`); the example takes it from its
|
|
252
|
+
contract's vocabulary, and it is `notes`.
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
bun install
|
|
256
|
+
bunx fabrika app build --env=<name> # cheap: add --config=fabrika.cheap.config.ts
|
|
257
|
+
FABRIKA_CONTROL_KEY="$(cat control.key)" bunx fabrika control register --provider=zerops --app=<id> \
|
|
258
|
+
--repo=https://github.com/<owner>/<name> --env=<name> --manifest=fabrika.manifest.json \
|
|
259
|
+
--namespace=<namespace id> --domain=<free host> --installation-id=none
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `--installation-id=none` works only for a repository GitHub serves without a token. `register` reads it
|
|
263
|
+
once and refuses one that is not publicly readable, so the failure shows here and not at deploy. A
|
|
264
|
+
private repository needs the GitHub App connected in the console first (Settings → Source, a browser
|
|
265
|
+
flow for the human); then omit the flag.
|
|
266
|
+
- `register` CREATES and refuses a second registration. A changed manifest goes through
|
|
267
|
+
`control apps environments put` with the same placement flags.
|
|
268
|
+
|
|
269
|
+
3. **Deploy and follow it.**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
FABRIKA_CONTROL_KEY="$(cat control.key)" bunx fabrika control deploy --app=<id> --env=<name>
|
|
273
|
+
FABRIKA_CONTROL_KEY="$(cat control.key)" bunx fabrika control runs list --app=<id>
|
|
274
|
+
FABRIKA_CONTROL_KEY="$(cat control.key)" bunx fabrika control runs log --run=<run id>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
A failed run's log ends with `deploy failed: <reason>`. For example, `source resolve failed
|
|
278
|
+
(ref_not_found)` means control could not read the repository or the ref.
|
|
279
|
+
|
|
280
|
+
The deploy fails when the repository at the deployed commit does not match the registered manifest.
|
|
281
|
+
Commit and push, rebuild the manifest, `apps environments put` it, and deploy again.
|
|
282
|
+
|
|
283
|
+
On an environment named `prod`, a change that widens access waits for a person to approve it in the
|
|
284
|
+
console before it deploys.
|
|
285
|
+
|
|
286
|
+
**Check.** The run ends `succeeded`. `https://<host>/healthz` answers 200, and `/` redirects to the IAM
|
|
287
|
+
sign-in. A green run proves only that Zerops accepted the build: have the human sign in and use the app.
|
|
288
|
+
|
|
289
|
+
## Step 6. Upgrade later
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
fabrika platform upgrade --provider=zerops --to=<newer tag> --sidecar=<sidecar checkout>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
It commits the tag to the sidecar's `fabrika.ref`, pushes, and follows the workflow run it triggers. It
|
|
296
|
+
needs only the `gh` login. Existing namespaces keep their proxy version. To move one to the proxy of the
|
|
297
|
+
new release, after the run succeeded:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
fabrika control namespaces update --namespace=<id> --proxy-source=current
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
It re-pins the namespace and queues the reconcile that rebuilds its proxy; poll `namespaces get` until it is
|
|
304
|
+
`ready`. A namespace pinned to a release before the proxy manifest moved into the build archive carries it in a
|
|
305
|
+
service variable, which Zerops caps at about 32 KB: when a deploy fails with `serviceEnvValueTooLarge`, this is
|
|
306
|
+
the fix, and it also accepts a namespace in `failed`.
|
|
307
|
+
|
|
308
|
+
## When something fails
|
|
309
|
+
|
|
310
|
+
- Every refusal names its flag or variable. Fix that one value and re-run. `init`, `admin`, `deploy` and
|
|
311
|
+
`upgrade` are safe to re-run. `install` is not: see step 1.
|
|
312
|
+
- A failed install pass 2 is finished with `platform deploy`, with the credentials from the secrets file. The
|
|
313
|
+
install prints the command.
|
|
314
|
+
- Lost the secrets file? Nothing in fabrika can recover it, and Zerops will not hand the values back. Start a
|
|
315
|
+
new installation in a new project.
|
|
316
|
+
- A Zerops build or deploy failure: read the service log in the Zerops GUI or with
|
|
317
|
+
`zcli service log -S <service id> --show-build-logs`.
|
|
318
|
+
- Do not work around a refusal by editing the clone or the generated sidecar. Report what failed, with the
|
|
319
|
+
command and its message, to the people who maintain fabrika.
|