@fabrika/provider-zerops 0.0.14 → 0.0.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/provider-zerops",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,9 +32,9 @@
32
32
  "gen:schema": "bun scripts/generate-zerops-types.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@fabrika/auth": "^0.0.14",
36
- "@fabrika/auth-core": "^0.0.14",
37
- "@fabrika/provider-contract": "^0.0.14",
38
- "@fabrika/proxy-contract": "^0.0.14"
35
+ "@fabrika/auth": "^0.0.16",
36
+ "@fabrika/auth-core": "^0.0.16",
37
+ "@fabrika/provider-contract": "^0.0.16",
38
+ "@fabrika/proxy-contract": "^0.0.16"
39
39
  }
40
40
  }
package/src/control.ts CHANGED
@@ -440,6 +440,19 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
440
440
  const controlProvider: ZeropsControlProvider = {
441
441
  id: 'zerops',
442
442
  normalizeRegistration,
443
+ /**
444
+ * A Zerops artifact IS the compiled manifest, so the declaration is right here and core can refuse
445
+ * a variable this app would never read. An artifact that will not decode answers `undefined`
446
+ * rather than an empty set: a broken envelope is a registration problem, and reporting it as an
447
+ * undeclared variable would point the operator at the wrong thing.
448
+ */
449
+ declaredVariables: ({ artifact }) => {
450
+ try {
451
+ return parseFabrikaManifest(decodeEnvelope('artifact', artifact, zeropsArtifactCodec)).app.pipeline.vars
452
+ } catch {
453
+ return undefined
454
+ }
455
+ },
443
456
  resolveSource: (input) => resolveSource(input),
444
457
  resolveSourceWithBinding: (input) => resolveSource(input, input.sourceBinding),
445
458
  deploy: async (input: ProviderDeployInput): Promise<ProviderTerminalOutcome> => {
package/src/index.ts CHANGED
@@ -69,8 +69,9 @@ export {
69
69
  compileZeropsNamespaceTopology,
70
70
  createZeropsNamespaceCapabilities,
71
71
  createZeropsNamespaceOperator,
72
+ ensureNamespaceProxyIam,
73
+ ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE,
72
74
  ZEROPS_NAMESPACE_IAM_KEY_VARIABLE,
73
- ZEROPS_NAMESPACE_IAM_URL_VARIABLE,
74
75
  ZEROPS_NAMESPACE_POSTGRES_HOSTNAME,
75
76
  ZEROPS_NAMESPACE_PROXY_HOSTNAME,
76
77
  ZEROPS_SHARED_POSTGRES_CONNECTION_STRING,
@@ -78,6 +79,7 @@ export {
78
79
  zeropsNamespaceTargetCodec,
79
80
  } from './namespace'
80
81
  export type {
82
+ NamespaceProxyIamInput,
81
83
  ZeropsNamespaceOperatorOptions,
82
84
  ZeropsNamespaceOptions,
83
85
  ZeropsNamespacePlanOptions,
package/src/namespace.ts CHANGED
@@ -31,7 +31,7 @@ import type { ZeropsServiceSpec, ZeropsSourceTarget } from './types'
31
31
 
32
32
  export const ZEROPS_NAMESPACE_PROXY_HOSTNAME = 'proxy'
33
33
  export const ZEROPS_NAMESPACE_POSTGRES_HOSTNAME = 'postgres'
34
- export const ZEROPS_NAMESPACE_IAM_URL_VARIABLE = 'FABRIKA_IAM_URL'
34
+ export const ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE = 'FABRIKA_IAM_ISSUER'
35
35
  export const ZEROPS_NAMESPACE_IAM_KEY_VARIABLE = 'FABRIKA_IAM_KEY'
36
36
  /**
37
37
  * How an app reaches the namespace-owned PostgreSQL service — port, database and TLS mode NAMED, not
@@ -737,6 +737,47 @@ const resolveServices = async (
737
737
  return postgres === null ? { proxy } : { proxy, postgres }
738
738
  }
739
739
 
740
+ export interface NamespaceProxyIamInput {
741
+ readonly api: Pick<ZeropsApi, 'listServiceEnv' | 'putServiceEnv'>
742
+ readonly proxyServiceId: string
743
+ /** The public IAM origin the proxy compares a token's `iss` against. */
744
+ readonly iamUrl: string
745
+ readonly iamKey: string
746
+ readonly signal: AbortSignal
747
+ }
748
+
749
+ /**
750
+ * Converge the two variables the namespace proxy's own CODE reads, and call this from everywhere that
751
+ * code is replaced — not only where the namespace is created.
752
+ *
753
+ * `syncZeropsProxy` rebuilds the proxy from git on every app deploy, so a proxy can take new code
754
+ * whose configuration nobody rewrote. That is not hypothetical: the `FABRIKA_IAM_URL` →
755
+ * `FABRIKA_IAM_ISSUER` rename ([ADR-0035](../../../docs/decisions/0035-the-platform-owns-the-application-iam-issuer.md))
756
+ * left a live namespace proxy crash-looping on `FABRIKA_IAM_ISSUER is required`, because provisioning
757
+ * had written the old name months earlier and the deploy path only ever wrote the manifest. Writing
758
+ * these BEFORE the pipeline is triggered is what makes a rename a flag day rather than an outage
759
+ * ([ADR-0024](../../../docs/decisions/0024-retire-the-legacy-environment-name-fallback.md)).
760
+ */
761
+ export const ensureNamespaceProxyIam = async (input: NamespaceProxyIamInput): Promise<void> => {
762
+ const variables = await input.api.listServiceEnv({ serviceId: input.proxyServiceId, signal: input.signal })
763
+ const issuer = variables.find((variable) => variable.key === ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE)
764
+ if (issuer?.content !== input.iamUrl) {
765
+ await input.api.putServiceEnv({
766
+ serviceId: input.proxyServiceId,
767
+ key: ZEROPS_NAMESPACE_IAM_ISSUER_VARIABLE,
768
+ value: input.iamUrl,
769
+ signal: input.signal,
770
+ })
771
+ }
772
+ // Secret read-back is not trustworthy, so overwrite it idempotently without inspecting its value.
773
+ await input.api.putServiceEnv({
774
+ serviceId: input.proxyServiceId,
775
+ key: ZEROPS_NAMESPACE_IAM_KEY_VARIABLE,
776
+ value: input.iamKey,
777
+ signal: input.signal,
778
+ })
779
+ }
780
+
740
781
  const ensureProxyConfiguration = async (
741
782
  target: NormalizedZeropsNamespaceTarget,
742
783
  options: ZeropsNamespaceOptions,
@@ -759,20 +800,11 @@ const ensureProxyConfiguration = async (
759
800
  })
760
801
  deployRequired = true
761
802
  }
762
- const iamUrl = byKey.get(ZEROPS_NAMESPACE_IAM_URL_VARIABLE)
763
- if (iamUrl?.content !== options.iamUrl) {
764
- await options.api.putServiceEnv({
765
- serviceId: proxyServiceId,
766
- key: ZEROPS_NAMESPACE_IAM_URL_VARIABLE,
767
- value: options.iamUrl,
768
- signal,
769
- })
770
- }
771
- // Secret read-back is not trustworthy, so overwrite it idempotently without inspecting its value.
772
- await options.api.putServiceEnv({
773
- serviceId: proxyServiceId,
774
- key: ZEROPS_NAMESPACE_IAM_KEY_VARIABLE,
775
- value: options.iamKey,
803
+ await ensureNamespaceProxyIam({
804
+ api: options.api,
805
+ proxyServiceId,
806
+ iamUrl: options.iamUrl,
807
+ iamKey: options.iamKey,
776
808
  signal,
777
809
  })
778
810
  return deployRequired
package/src/provider.ts CHANGED
@@ -152,9 +152,21 @@ const cancelSourceBestEffort = async (env: StepEnv, source: ZeropsRuntimeSource,
152
152
  sourceController.abort()
153
153
  }
154
154
 
155
+ /**
156
+ * Drop a version that was created and never handed to the platform.
157
+ *
158
+ * BEST EFFORT ON PURPOSE. Every caller runs this while already failing, so a cleanup that throws would
159
+ * replace the reason the deploy failed with the reason the cleanup did — which is what happened live,
160
+ * where a real `trigger-deploy` failure surfaced as `delete app-version failed (400)` and the cause was
161
+ * gone. The leftover version is visible on the platform; the lost diagnosis was not.
162
+ */
155
163
  const cleanupPreTriggerVersion = async (env: StepEnv, source: ZeropsRuntimeSource, appVersionId: string): Promise<void> => {
156
164
  await cancelSourceBestEffort(env, source, appVersionId)
157
- await env.zerops.api.deleteAppVersion({ appVersionId, signal: AbortSignal.timeout(5000) })
165
+ try {
166
+ await env.zerops.api.deleteAppVersion({ appVersionId, signal: AbortSignal.timeout(5000) })
167
+ } catch (error) {
168
+ env.log(` [warn] app-version ${appVersionId} was left behind: ${error instanceof Error ? error.message : 'unknown error'}`)
169
+ }
158
170
  }
159
171
 
160
172
  /**