@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.
Files changed (45) hide show
  1. package/package.json +8 -7
  2. package/src/api/client.ts +11 -14
  3. package/src/api/polling.ts +42 -0
  4. package/src/api/readers.ts +9 -2
  5. package/src/api/service-env-limit.ts +29 -0
  6. package/src/api/service-env-response.ts +34 -2
  7. package/src/api/types.ts +39 -9
  8. package/src/api.ts +10 -1
  9. package/src/control.ts +23 -8
  10. package/src/execution.ts +41 -3
  11. package/src/index.ts +5 -0
  12. package/src/installation/admin.ts +4 -2
  13. package/src/installation/checker-release.ts +44 -0
  14. package/src/installation/deploy.ts +66 -22
  15. package/src/installation/generated/platform-console-schema.ts +18 -2
  16. package/src/installation/generated/platform-proxy-manifest.ts +20 -0
  17. package/src/installation/guide.md +319 -0
  18. package/src/installation/index.ts +42 -16
  19. package/src/installation/init-options.ts +2 -2
  20. package/src/installation/init.ts +8 -2
  21. package/src/installation/install-collaborators.ts +4 -0
  22. package/src/installation/install-environment.ts +52 -30
  23. package/src/installation/install-options.ts +20 -19
  24. package/src/installation/install-project.ts +23 -15
  25. package/src/installation/install-provisioning.ts +12 -12
  26. package/src/installation/install-secrets.ts +89 -0
  27. package/src/installation/install-token.ts +37 -0
  28. package/src/installation/install.ts +34 -21
  29. package/src/installation/machine-key.ts +3 -2
  30. package/src/installation/manifest.ts +29 -12
  31. package/src/installation/platform-archive.ts +87 -0
  32. package/src/installation/platform-upload.ts +2 -2
  33. package/src/installation/secrets.ts +3 -4
  34. package/src/installation/topology/setups.ts +15 -27
  35. package/src/installation/topology/topology.ts +105 -104
  36. package/src/managed-env.ts +11 -0
  37. package/src/manifest.ts +25 -3
  38. package/src/namespace/codec.ts +6 -1
  39. package/src/namespace/errors.ts +3 -1
  40. package/src/namespace/provisioning.ts +18 -5
  41. package/src/namespace/proxy-attempt.ts +56 -14
  42. package/src/namespace/update.ts +50 -8
  43. package/src/provider.ts +57 -13
  44. package/src/proxy-manifest-carrier.ts +32 -0
  45. 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?.content), ...(target.createdBuckets ?? [])])
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 written = [...SERVICE_MANAGED_KEYS.filter((key) => managedValues[key] !== null), ...Object.keys(familyVariables)]
506
- const removed = SERVICE_MANAGED_KEYS.filter((key) => managedValues[key] === null)
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?.content !== value) {
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?.content !== provenance) {
552
+ if (typeof provenance === 'string' && readableContent(marker) !== provenance) {
534
553
  assertRunning(signal)
535
- await zerops.api.putServiceEnv({ serviceId, key: STORAGE_PROVENANCE_KEY, value: provenance, signal })
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?.content !== value) {
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
+ }
@@ -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
  }