@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
package/src/provider.ts
CHANGED
|
@@ -8,19 +8,22 @@
|
|
|
8
8
|
// runs. Recovery never resumes a deploy (ADR-0044): a version reserved but never triggered is deleted, and
|
|
9
9
|
// one whose build may have been requested is settled only by polling it.
|
|
10
10
|
|
|
11
|
-
import { appEnvironmentId } from '@fabrika/auth-core'
|
|
11
|
+
import { appEnvironmentId, FABRIKA_SOURCE_MAP_UPLOAD_KEY } from '@fabrika/auth-core'
|
|
12
|
+
import { type OperationsSourceMapUploadState, readSourceMapUploadReport } from '@fabrika/operations-contract/releases'
|
|
12
13
|
import {
|
|
13
14
|
type DeployStepResult,
|
|
14
15
|
followLeaseLoss,
|
|
15
16
|
isLeaseLoss,
|
|
16
17
|
type JsonValue,
|
|
17
18
|
type ProviderApprovedAccess,
|
|
19
|
+
type ProviderDeployInput,
|
|
18
20
|
ProviderInvocationAbort,
|
|
19
21
|
type ProviderManagedEnvironment,
|
|
20
22
|
type ProviderRunEvents,
|
|
21
23
|
runSteps,
|
|
22
24
|
} from '@fabrika/provider-contract'
|
|
23
25
|
import {
|
|
26
|
+
readableContent,
|
|
24
27
|
waitForProcess,
|
|
25
28
|
ZEROPS_ACTIVE,
|
|
26
29
|
ZEROPS_PROCESS_FINISHED,
|
|
@@ -47,9 +50,11 @@ import {
|
|
|
47
50
|
assertAuthoredManagedDocument,
|
|
48
51
|
assertAuthoredManagedInputs,
|
|
49
52
|
isFrameworkFamilyVariable,
|
|
53
|
+
isSensitiveFamilyVariable,
|
|
50
54
|
type ManagedValues,
|
|
51
55
|
overlayRelease,
|
|
52
56
|
parseManagedEnvironment,
|
|
57
|
+
SENSITIVE_MANAGED_KEYS,
|
|
53
58
|
SERVICE_MANAGED_KEYS,
|
|
54
59
|
} from './managed-env'
|
|
55
60
|
import {
|
|
@@ -81,6 +86,8 @@ export interface ZeropsDeployRun {
|
|
|
81
86
|
readonly artifact: FabrikaManifest
|
|
82
87
|
/** Control's approved access for the environment (ADR-0050); the lock-down and the IAM schema come from it. */
|
|
83
88
|
readonly access?: ProviderApprovedAccess
|
|
89
|
+
/** The run-scoped source-map destination Control minted; the build uploads to it. */
|
|
90
|
+
readonly artifactUpload?: ProviderDeployInput['artifactUpload']
|
|
84
91
|
readonly dryRun: boolean
|
|
85
92
|
readonly signal: AbortSignal
|
|
86
93
|
readonly events: ProviderRunEvents
|
|
@@ -91,6 +98,8 @@ export interface ZeropsDeployResult {
|
|
|
91
98
|
readonly env: string
|
|
92
99
|
readonly status: 'succeeded' | 'failed'
|
|
93
100
|
readonly steps: readonly DeployStepResult[]
|
|
101
|
+
/** Whether the build's source maps reached Operations; absent when the run had no upload to make. */
|
|
102
|
+
readonly artifactState?: OperationsSourceMapUploadState
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
/** How often `await-version` asks Zerops for the version's status. */
|
|
@@ -271,6 +280,8 @@ interface Attempt {
|
|
|
271
280
|
uploadUrl?: string
|
|
272
281
|
processId?: string
|
|
273
282
|
triggered: boolean
|
|
283
|
+
/** What the build's source-map upload reported in its log, once a relayed line said so. */
|
|
284
|
+
sourceMaps?: OperationsSourceMapUploadState
|
|
274
285
|
}
|
|
275
286
|
|
|
276
287
|
/** Everything one step's executor needs, bundled so the per-kind handlers stay small. */
|
|
@@ -353,6 +364,8 @@ const relayLog = async (
|
|
|
353
364
|
}
|
|
354
365
|
seen.add(key)
|
|
355
366
|
env.log(` │ ${line.message}`)
|
|
367
|
+
const reported = readSourceMapUploadReport(line.message)
|
|
368
|
+
if (reported !== null) env.attempt.sourceMaps = reported
|
|
356
369
|
}
|
|
357
370
|
} catch (error) {
|
|
358
371
|
env.log(` [warn] build-log relay unavailable: ${error instanceof Error ? error.message : 'unknown error'}`)
|
|
@@ -452,8 +465,8 @@ const verifyExposure = async (env: StepEnv): Promise<void> => {
|
|
|
452
465
|
const created = env.attempt.created ?? new Map<string, string>()
|
|
453
466
|
const buckets = artifact.target.importDocument.services.filter((service) => isObjectStorageType(service.type)).map((service) => service.hostname)
|
|
454
467
|
const records = buckets.length === 0 ? [] : await zerops.api.listServiceEnv({ serviceId: target.serviceId, signal })
|
|
455
|
-
const marker = records.find((record) => record.key === STORAGE_PROVENANCE_KEY)
|
|
456
|
-
const known = new Set([...parseStorageProvenance(marker
|
|
468
|
+
const marker = readableContent(records.find((record) => record.key === STORAGE_PROVENANCE_KEY))
|
|
469
|
+
const known = new Set([...parseStorageProvenance(marker), ...(target.createdBuckets ?? [])])
|
|
457
470
|
const findings: ExposureFinding[] = []
|
|
458
471
|
const bucketIds: string[] = []
|
|
459
472
|
for (const hostname of manifestServiceHostnames(artifact)) {
|
|
@@ -502,8 +515,13 @@ const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
|
502
515
|
const { zerops, log, signal, managedValues, familyVariables } = env
|
|
503
516
|
const serviceId = env.run.target.serviceId
|
|
504
517
|
if (env.dryRun) {
|
|
505
|
-
const
|
|
506
|
-
const
|
|
518
|
+
const upload = env.run.artifactUpload !== undefined
|
|
519
|
+
const written = [
|
|
520
|
+
...SERVICE_MANAGED_KEYS.filter((key) => managedValues[key] !== null),
|
|
521
|
+
...Object.keys(familyVariables),
|
|
522
|
+
...(upload ? [FABRIKA_SOURCE_MAP_UPLOAD_KEY] : []),
|
|
523
|
+
]
|
|
524
|
+
const removed = [...SERVICE_MANAGED_KEYS.filter((key) => managedValues[key] === null), ...(upload ? [] : [FABRIKA_SOURCE_MAP_UPLOAD_KEY])]
|
|
507
525
|
log(` [dry-run] would write service variables on ${serviceId}: ${written.join(', ') || '(none)'}`)
|
|
508
526
|
log(` [dry-run] would remove service variables on ${serviceId} if present: ${removed.join(', ') || '(none)'}`)
|
|
509
527
|
if (Object.keys(familyVariables).length > 0) {
|
|
@@ -517,9 +535,10 @@ const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
|
517
535
|
for (const key of SERVICE_MANAGED_KEYS) {
|
|
518
536
|
const value = managedValues[key]
|
|
519
537
|
const existing = records.find((record) => record.key === key)
|
|
538
|
+
const sensitive = SENSITIVE_MANAGED_KEYS.has(key)
|
|
520
539
|
assertRunning(signal)
|
|
521
|
-
if (value !== null && existing
|
|
522
|
-
await zerops.api.putServiceEnv({ serviceId, key, value, signal })
|
|
540
|
+
if (value !== null && (sensitive || readableContent(existing) !== value)) {
|
|
541
|
+
await zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal })
|
|
523
542
|
log(` ${key}: updated`)
|
|
524
543
|
} else if (value === null && existing !== undefined) {
|
|
525
544
|
await zerops.api.deleteServiceEnv({ envId: existing.id, signal })
|
|
@@ -530,9 +549,10 @@ const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
|
530
549
|
}
|
|
531
550
|
const provenance = env.attempt.storageProvenance
|
|
532
551
|
const marker = records.find((record) => record.key === STORAGE_PROVENANCE_KEY)
|
|
533
|
-
if (typeof provenance === 'string' && marker
|
|
552
|
+
if (typeof provenance === 'string' && readableContent(marker) !== provenance) {
|
|
534
553
|
assertRunning(signal)
|
|
535
|
-
|
|
554
|
+
// Not a credential, and the exposure read-back above reads it: it must stay readable.
|
|
555
|
+
await zerops.api.putServiceEnv({ serviceId, key: STORAGE_PROVENANCE_KEY, value: provenance, sensitive: false, signal })
|
|
536
556
|
log(` ${STORAGE_PROVENANCE_KEY}: updated`)
|
|
537
557
|
} else if (provenance === null && marker !== undefined) {
|
|
538
558
|
assertRunning(signal)
|
|
@@ -542,9 +562,10 @@ const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
|
542
562
|
// A workload key is written like a managed key; one whose workload is gone is removed, so no stale key stays readable.
|
|
543
563
|
for (const [key, value] of Object.entries(familyVariables)) {
|
|
544
564
|
const existing = records.find((record) => record.key === key)
|
|
565
|
+
const sensitive = isSensitiveFamilyVariable(key)
|
|
545
566
|
assertRunning(signal)
|
|
546
|
-
if (existing
|
|
547
|
-
await zerops.api.putServiceEnv({ serviceId, key, value, signal })
|
|
567
|
+
if (sensitive || readableContent(existing) !== value) {
|
|
568
|
+
await zerops.api.putServiceEnv({ serviceId, key, value, sensitive, signal })
|
|
548
569
|
log(` ${key}: updated`)
|
|
549
570
|
} else {
|
|
550
571
|
log(` ${key}: unchanged`)
|
|
@@ -557,6 +578,18 @@ const writeServiceEnv = async (env: StepEnv): Promise<void> => {
|
|
|
557
578
|
log(` ${record.key}: removed`)
|
|
558
579
|
}
|
|
559
580
|
}
|
|
581
|
+
// The build lifts the upload's bearer from here (`${RUNTIME_…}`), so the descriptor never holds it.
|
|
582
|
+
const bearer = env.run.artifactUpload?.bearer ?? null
|
|
583
|
+
const uploadKey = records.find((record) => record.key === FABRIKA_SOURCE_MAP_UPLOAD_KEY)
|
|
584
|
+
if (bearer !== null) {
|
|
585
|
+
assertRunning(signal)
|
|
586
|
+
await zerops.api.putServiceEnv({ serviceId, key: FABRIKA_SOURCE_MAP_UPLOAD_KEY, value: bearer, sensitive: true, signal })
|
|
587
|
+
log(` ${FABRIKA_SOURCE_MAP_UPLOAD_KEY}: updated`)
|
|
588
|
+
} else if (uploadKey !== undefined) {
|
|
589
|
+
assertRunning(signal)
|
|
590
|
+
await zerops.api.deleteServiceEnv({ envId: uploadKey.id, signal })
|
|
591
|
+
log(` ${FABRIKA_SOURCE_MAP_UPLOAD_KEY}: removed`)
|
|
592
|
+
}
|
|
560
593
|
}
|
|
561
594
|
|
|
562
595
|
/** Run one step's effect. Resolves on success, throws on failure (caught + recorded by `runSteps`). */
|
|
@@ -655,7 +688,7 @@ const runStep = async (spec: ZeropsStep, env: StepEnv): Promise<void> => {
|
|
|
655
688
|
}
|
|
656
689
|
|
|
657
690
|
case 'trigger-build': {
|
|
658
|
-
const locked = lockedExecutionFor(artifact, run, dryRun)
|
|
691
|
+
const locked = lockedExecutionFor(artifact, run, dryRun, run.artifactUpload)
|
|
659
692
|
if (dryRun) {
|
|
660
693
|
log(
|
|
661
694
|
locked === null
|
|
@@ -673,6 +706,7 @@ const runStep = async (spec: ZeropsStep, env: StepEnv): Promise<void> => {
|
|
|
673
706
|
: renderLockedExecutionDescriptor(contents, artifact.target.zeropsSetup, release, locked)
|
|
674
707
|
if (locked !== null) {
|
|
675
708
|
log(` generated run section; build checks @fabrika/check@${FABRIKA_CHECK_VERSION}, typescript@${PLATFORM_TYPESCRIPT_VERSION}`)
|
|
709
|
+
if (locked.sourceMaps !== null) log(` the build uploads the source maps of ${locked.sourceMaps.assetRoots.length} asset root(s)`)
|
|
676
710
|
}
|
|
677
711
|
assertRunning(signal)
|
|
678
712
|
// Recorded BEFORE the request: from here on the version is settled only by polling it (ADR-0044).
|
|
@@ -804,5 +838,15 @@ async (run: ZeropsDeployRun): Promise<ZeropsDeployResult> => {
|
|
|
804
838
|
run.events.log(` [warn] app-version ${appVersionId} could not be deleted; it was never built`)
|
|
805
839
|
}
|
|
806
840
|
}
|
|
807
|
-
return { appId: run.appId, env: run.env, ...result }
|
|
841
|
+
return { appId: run.appId, env: run.env, ...result, ...artifactStateOf(run, result.status, env.attempt) }
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** A run that uploads reports `complete` only when its build log said so; a missing or unread report is `incomplete`. */
|
|
845
|
+
const artifactStateOf = (
|
|
846
|
+
run: ZeropsDeployRun,
|
|
847
|
+
status: ZeropsDeployResult['status'],
|
|
848
|
+
attempt: Attempt,
|
|
849
|
+
): Pick<ZeropsDeployResult, 'artifactState'> => {
|
|
850
|
+
if (run.artifactUpload === undefined || run.dryRun || status !== 'succeeded') return {}
|
|
851
|
+
return { artifactState: attempt.sourceMaps === 'complete' ? 'complete' : 'incomplete' }
|
|
808
852
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FABRIKA_PROXY_MANIFEST_JSON } from '@fabrika/proxy-core/manifest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { appliedZeropsSetups, parseZeropsDescriptor, selectZeropsDescriptorSetup } from './descriptor'
|
|
4
|
+
|
|
5
|
+
/** The path of the proxy manifest in the build archive: the archive's one data file (ADR-0045), read by the proxy build. */
|
|
6
|
+
export const ZEROPS_PROXY_MANIFEST_PATH = 'proxy.manifest.json'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* How a proxy build receives its manifest.
|
|
10
|
+
*
|
|
11
|
+
* - `environment`: the build lifts `FABRIKA_PROXY_MANIFEST_JSON` from the service and writes it to the file, as every
|
|
12
|
+
* release before the archive carrier did. A namespace pinned to such a commit keeps its descriptor (ADR-0045), so it
|
|
13
|
+
* gets the variable, bound by the Zerops value limit.
|
|
14
|
+
* - `archive`: the build reads the file the uploaded archive carries.
|
|
15
|
+
*/
|
|
16
|
+
export type ZeropsProxyManifestCarrier = 'environment' | 'archive'
|
|
17
|
+
|
|
18
|
+
const buildSectionSchema = z.object({ build: z.object({ envVariables: z.record(z.string()).optional() }).passthrough() }).passthrough()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The carrier a descriptor's proxy setup reads. Decided by the build section Zerops applies (the selected setup's, else
|
|
22
|
+
* the nearest one it `extends`): a lifted `FABRIKA_PROXY_MANIFEST_JSON` would overwrite an archived file, so it wins.
|
|
23
|
+
*/
|
|
24
|
+
export const zeropsProxyManifestCarrier = (descriptorContents: string, setup = 'proxy'): ZeropsProxyManifestCarrier => {
|
|
25
|
+
const parsed = parseZeropsDescriptor(descriptorContents)
|
|
26
|
+
for (const entry of appliedZeropsSetups(parsed, selectZeropsDescriptorSetup(parsed, setup))) {
|
|
27
|
+
const read = buildSectionSchema.safeParse(entry)
|
|
28
|
+
if (!read.success) continue
|
|
29
|
+
return read.data.build.envVariables?.[FABRIKA_PROXY_MANIFEST_JSON] === undefined ? 'archive' : 'environment'
|
|
30
|
+
}
|
|
31
|
+
return 'archive'
|
|
32
|
+
}
|
package/src/source/port.ts
CHANGED
|
@@ -20,10 +20,16 @@ export interface ZeropsSourceResolution {
|
|
|
20
20
|
readonly descriptorSha256: string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** ADR-0045: the one generated data file a platform archive carries beside the exact commit, at a path the commit lacks. */
|
|
24
|
+
export interface ZeropsSourceDataFile {
|
|
25
|
+
readonly path: string
|
|
26
|
+
readonly contents: string
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
/** ADR-0045: platform archives are anonymous, tree-prechecked, keep relative symlinks. */
|
|
24
30
|
export type ZeropsSourceArchive =
|
|
25
31
|
| { readonly kind: 'application'; readonly privateBinding?: ZeropsSourcePrivateBinding }
|
|
26
|
-
| { readonly kind: 'platform' }
|
|
32
|
+
| { readonly kind: 'platform'; readonly dataFile?: ZeropsSourceDataFile }
|
|
27
33
|
|
|
28
34
|
export interface ZeropsSourceUploadInput {
|
|
29
35
|
readonly repository: ZeropsSourceRepository
|
|
@@ -38,6 +44,8 @@ export interface ZeropsSourceUploadInput {
|
|
|
38
44
|
export interface ZeropsSourceUploadResult {
|
|
39
45
|
readonly commitSha: string
|
|
40
46
|
readonly descriptorSha256: string
|
|
47
|
+
/** The sha256 of the data file the archive carried; absent without one. */
|
|
48
|
+
readonly dataFileSha256?: string
|
|
41
49
|
readonly entryCount: number
|
|
42
50
|
readonly expandedBytes: number
|
|
43
51
|
}
|