@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 +5 -5
- package/src/control.ts +13 -0
- package/src/index.ts +3 -1
- package/src/namespace.ts +47 -15
- package/src/provider.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/provider-zerops",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
36
|
-
"@fabrika/auth-core": "^0.0.
|
|
37
|
-
"@fabrika/provider-contract": "^0.0.
|
|
38
|
-
"@fabrika/proxy-contract": "^0.0.
|
|
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
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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
|
/**
|