@fabrika/provider-zerops 0.0.26 → 0.3.1
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 +18 -5
- package/src/api/client.ts +522 -0
- package/src/api/errors.ts +50 -0
- package/src/api/polling.ts +59 -0
- package/src/api/readers.ts +185 -0
- package/src/api/service-env-response.ts +34 -0
- package/src/api/types.ts +602 -0
- package/src/api.ts +29 -1378
- package/src/authoring.ts +7 -10
- package/src/cli-args.ts +0 -5
- package/src/cli.ts +3 -28
- package/src/collaborators.ts +2 -4
- package/src/compile.ts +75 -20
- package/src/control.ts +217 -447
- package/src/descriptor.ts +135 -0
- package/src/index.ts +24 -128
- package/src/installation/admin-options.ts +128 -0
- package/src/installation/admin.ts +123 -0
- package/src/installation/deploy-options.ts +155 -0
- package/src/installation/deploy.ts +668 -0
- package/src/installation/generated/platform-console-schema.ts +41 -0
- package/src/installation/generated/platform-proxy-manifest.ts +72 -0
- package/src/installation/hosts.ts +117 -0
- package/src/installation/index.ts +366 -0
- package/src/installation/init.ts +521 -0
- package/src/installation/install-collaborators.ts +28 -0
- package/src/installation/install-environment.ts +193 -0
- package/src/installation/install-options.ts +193 -0
- package/src/installation/install-project.ts +201 -0
- package/src/installation/install-provisioning.ts +185 -0
- package/src/installation/install.ts +286 -0
- package/src/installation/installation-source.ts +42 -0
- package/src/installation/log.ts +77 -0
- package/src/installation/machine-key.ts +65 -0
- package/src/installation/manifest.ts +95 -0
- package/src/installation/placement-options.ts +33 -0
- package/src/installation/platform-source.ts +106 -0
- package/src/installation/platform-upload.ts +34 -0
- package/src/installation/proxy-manifest.ts +113 -0
- package/src/installation/secrets.ts +79 -0
- package/src/installation/sidecar.ts +122 -0
- package/src/installation/source-options.ts +12 -0
- package/src/installation/templates/README.md +116 -0
- package/src/installation/templates/fabrika.ref +1 -0
- package/src/installation/templates/gitignore +3 -0
- package/src/installation/templates/platform.yml +144 -0
- package/src/installation/topology/artifacts.ts +141 -0
- package/src/installation/topology/index.ts +13 -0
- package/src/installation/topology/installation-schemas.ts +225 -0
- package/src/installation/topology/invariants.ts +86 -0
- package/src/installation/topology/setups.ts +442 -0
- package/src/installation/topology/topology.ts +501 -0
- package/src/installation/topology/validate.ts +26 -0
- package/src/installation/upgrade-options.ts +108 -0
- package/src/installation/upgrade-output.ts +31 -0
- package/src/installation/upgrade.ts +373 -0
- package/src/managed-env.ts +105 -0
- package/src/manifest.ts +208 -315
- package/src/namespace/claims.ts +77 -0
- package/src/namespace/codec.ts +179 -0
- package/src/namespace/constants.ts +30 -0
- package/src/namespace/errors.ts +60 -0
- package/src/namespace/manifest.ts +28 -0
- package/src/namespace/operator.ts +151 -0
- package/src/namespace/provisioning.ts +482 -0
- package/src/namespace/proxy-attempt.ts +220 -0
- package/src/namespace/proxy-source.ts +22 -0
- package/src/namespace/topology.ts +115 -0
- package/src/namespace/types.ts +114 -0
- package/src/namespace.ts +34 -1260
- package/src/plan.ts +21 -23
- package/src/provider.ts +399 -254
- package/src/service-references.ts +28 -0
- package/src/source/constants.ts +11 -0
- package/src/source/port.ts +49 -0
- package/src/source/types.ts +4 -0
- package/src/source/validation.ts +72 -0
- package/src/source.ts +2 -1220
- package/src/types.ts +8 -7
- package/src/version-attempt.ts +18 -0
- package/src/codec.ts +0 -108
- package/src/namespace-command.ts +0 -209
- package/src/source-connection.ts +0 -344
package/src/control.ts
CHANGED
|
@@ -1,84 +1,64 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ControlProvider,
|
|
3
3
|
JsonValue,
|
|
4
|
+
ProviderApp,
|
|
5
|
+
ProviderCancelOutcome,
|
|
4
6
|
ProviderCodec,
|
|
5
7
|
ProviderDeployInput,
|
|
6
8
|
ProviderDeploymentNamespace,
|
|
9
|
+
ProviderDeployOutcome,
|
|
7
10
|
ProviderEnvelope,
|
|
8
11
|
ProviderEnvironment,
|
|
9
|
-
ProviderReconcileInput,
|
|
10
12
|
ProviderReconcileOutcome,
|
|
11
13
|
ProviderRegistration,
|
|
12
14
|
ProviderRegistrationInput,
|
|
15
|
+
ProviderSourceBinding,
|
|
13
16
|
ProviderSourceResolution,
|
|
14
17
|
ProviderSourceResolutionInput,
|
|
15
|
-
ProviderTerminalOutcome,
|
|
16
|
-
RuntimeProvider,
|
|
17
|
-
RuntimeProviderRun,
|
|
18
18
|
SchemaReconciler,
|
|
19
19
|
} from '@fabrika/provider-contract'
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
ZEROPS_PROCESS_FINISHED,
|
|
24
|
-
ZEROPS_PROCESS_TERMINAL,
|
|
25
|
-
ZEROPS_TERMINAL,
|
|
26
|
-
type ZeropsApi,
|
|
27
|
-
ZeropsApiError,
|
|
28
|
-
type ZeropsAppVersion,
|
|
29
|
-
type ZeropsProcess,
|
|
30
|
-
} from './api'
|
|
20
|
+
import { decodeEnvelope, ProviderSecretError } from '@fabrika/provider-contract'
|
|
21
|
+
import { z } from 'zod'
|
|
22
|
+
import { createZeropsApi, waitForProcess, ZEROPS_ACTIVE, ZEROPS_TERMINAL, type ZeropsApi, type ZeropsAppVersion } from './api'
|
|
31
23
|
import { defaultSleep, defaultZeropsCollaborators, type Sleeper } from './collaborators'
|
|
24
|
+
import { assertAuthoredManagedInputs, assertAuthoredManagedName, parseManagedValues } from './managed-env'
|
|
32
25
|
import {
|
|
26
|
+
assertFabrikaManifestCoordinates,
|
|
33
27
|
type FabrikaManifest,
|
|
34
28
|
manifestServiceHostnames,
|
|
35
|
-
parseFabrikaManifest,
|
|
36
29
|
renderFabrikaImportYaml,
|
|
37
30
|
renderFabrikaProvisioningYaml,
|
|
38
31
|
verifyZeropsArtifactSourceDescriptor,
|
|
39
32
|
zeropsArtifactCodec,
|
|
40
33
|
} from './manifest'
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
import {
|
|
35
|
+
createZeropsNamespaceCapabilities,
|
|
36
|
+
type ZeropsNamespaceProxySource,
|
|
37
|
+
type ZeropsNamespaceTarget,
|
|
38
|
+
zeropsNamespaceTargetCodec,
|
|
39
|
+
} from './namespace'
|
|
40
|
+
import { createZeropsDeployer, ZEROPS_VERSION_DEADLINE_MS } from './provider'
|
|
41
|
+
import { allowedReferenceHosts, foreignReferenceHost, serviceReferenceHosts } from './service-references'
|
|
42
|
+
import { normalizeZeropsSourceRepository } from './source'
|
|
43
|
+
import type { ZeropsSourcePort, ZeropsSourcePrivateBinding } from './source/port'
|
|
44
44
|
import type { ZeropsRunState, ZeropsRuntimeTarget } from './types'
|
|
45
|
+
import { deleteVersion, isVersionNotFound } from './version-attempt'
|
|
45
46
|
|
|
46
47
|
/** Provider coordinates that are safe to persist. Credentials are composed only for a live run. */
|
|
47
48
|
export interface ZeropsStoredTarget {
|
|
48
49
|
serviceId: string
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
-
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
53
|
-
throw new Error('Zerops stored target must be an object')
|
|
54
|
-
}
|
|
55
|
-
return payload[key]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const requiredString = (payload: JsonValue, key: string): string => {
|
|
59
|
-
const value = property(payload, key)
|
|
60
|
-
if (typeof value !== 'string' || value === '') {
|
|
61
|
-
throw new Error(`Zerops stored target ${key} must be a non-empty string`)
|
|
62
|
-
}
|
|
63
|
-
return value
|
|
64
|
-
}
|
|
52
|
+
const storedTargetSchema = z.object({ serviceId: z.string().min(1) })
|
|
65
53
|
|
|
66
54
|
export const zeropsStoredTargetCodec: ProviderCodec<ZeropsStoredTarget> = {
|
|
67
55
|
version: 2,
|
|
68
56
|
encode: (target) => ({ serviceId: target.serviceId }),
|
|
69
|
-
decode: (payload) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const decodeEnvelope = <T>(kind: string, envelope: ProviderEnvelope, codec: ProviderCodec<T>): T => {
|
|
75
|
-
if (envelope.provider !== 'zerops') {
|
|
76
|
-
throw new Error(`${kind} belongs to provider "${envelope.provider}", expected "zerops"`)
|
|
77
|
-
}
|
|
78
|
-
if (envelope.version !== codec.version) {
|
|
79
|
-
throw new Error(`${kind} schema version ${envelope.version} is not supported by provider "zerops"`)
|
|
80
|
-
}
|
|
81
|
-
return codec.decode(envelope.payload)
|
|
57
|
+
decode: (payload) => {
|
|
58
|
+
const result = storedTargetSchema.safeParse(payload)
|
|
59
|
+
if (!result.success) throw new Error('Zerops stored target serviceId must be a non-empty string')
|
|
60
|
+
return result.data
|
|
61
|
+
},
|
|
82
62
|
}
|
|
83
63
|
|
|
84
64
|
const envelope = <T>(codec: ProviderCodec<T>, value: T): ProviderEnvelope => ({
|
|
@@ -87,61 +67,23 @@ const envelope = <T>(codec: ProviderCodec<T>, value: T): ProviderEnvelope => ({
|
|
|
87
67
|
payload: codec.encode(value),
|
|
88
68
|
})
|
|
89
69
|
|
|
90
|
-
export interface ZeropsBeforeDeployInput {
|
|
91
|
-
readonly appId: string
|
|
92
|
-
readonly env: string
|
|
93
|
-
readonly namespaceId: string
|
|
94
|
-
readonly target: ZeropsStoredTarget & {
|
|
95
|
-
readonly projectId: string
|
|
96
|
-
readonly proxyServiceId: string
|
|
97
|
-
/** The namespace proxy has no Git integration, so its pipeline must be told what to build. */
|
|
98
|
-
readonly proxyBuildFromGit: string
|
|
99
|
-
}
|
|
100
|
-
readonly artifact: FabrikaManifest
|
|
101
|
-
readonly api: ZeropsApi
|
|
102
|
-
readonly signal: AbortSignal
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export type ZeropsBeforeDeploy = (input: ZeropsBeforeDeployInput) => Promise<void>
|
|
106
|
-
|
|
107
|
-
export type ZeropsProviderExecutor = (
|
|
108
|
-
provider: RuntimeProvider,
|
|
109
|
-
run: RuntimeProviderRun,
|
|
110
|
-
) => Promise<ProviderTerminalOutcome>
|
|
111
|
-
|
|
112
70
|
export interface ZeropsControlProviderOptions {
|
|
113
71
|
readonly accessToken: string
|
|
114
72
|
readonly apiBaseUrl?: string
|
|
115
73
|
readonly propustkaUrl?: string
|
|
116
74
|
readonly adminKey?: string
|
|
117
75
|
readonly api?: ZeropsApi
|
|
118
|
-
readonly source:
|
|
76
|
+
readonly source: ZeropsSourcePort
|
|
119
77
|
readonly reconcileSchema?: SchemaReconciler
|
|
120
78
|
readonly sleep?: Sleeper
|
|
121
|
-
readonly sourceCancelSleep?: Sleeper
|
|
122
|
-
readonly execute: ZeropsProviderExecutor
|
|
123
|
-
readonly beforeDeploy?: ZeropsBeforeDeploy
|
|
124
79
|
readonly namespaces?: {
|
|
125
80
|
readonly clientId: string
|
|
126
|
-
readonly
|
|
81
|
+
readonly proxySource: ZeropsNamespaceProxySource
|
|
127
82
|
readonly iamUrl: string
|
|
128
83
|
readonly iamKey: string
|
|
129
84
|
}
|
|
130
85
|
}
|
|
131
86
|
|
|
132
|
-
export interface ZeropsBoundSourceResolutionInput extends ProviderSourceResolutionInput {
|
|
133
|
-
readonly sourceBinding: ZeropsSourceTransportBinding
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface ZeropsBoundDeployInput extends ProviderDeployInput {
|
|
137
|
-
readonly sourceBinding: ZeropsSourceTransportBinding
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface ZeropsControlProvider extends ControlProvider {
|
|
141
|
-
resolveSourceWithBinding(input: ZeropsBoundSourceResolutionInput): Promise<ProviderSourceResolution>
|
|
142
|
-
deployWithBinding(input: ZeropsBoundDeployInput): Promise<ProviderTerminalOutcome>
|
|
143
|
-
}
|
|
144
|
-
|
|
145
87
|
interface DecodedZeropsEnvironment {
|
|
146
88
|
readonly target: ZeropsStoredTarget
|
|
147
89
|
readonly namespace: ProviderDeploymentNamespace
|
|
@@ -149,7 +91,7 @@ interface DecodedZeropsEnvironment {
|
|
|
149
91
|
}
|
|
150
92
|
|
|
151
93
|
const decodeEnvironment = (environment: ProviderEnvironment): DecodedZeropsEnvironment => {
|
|
152
|
-
const target = decodeEnvelope('target', environment.target, zeropsStoredTargetCodec)
|
|
94
|
+
const target = decodeEnvelope('target', 'zerops', environment.target, zeropsStoredTargetCodec)
|
|
153
95
|
const placement = decodeNamespace(environment)
|
|
154
96
|
return {
|
|
155
97
|
target,
|
|
@@ -172,14 +114,14 @@ const decodeNamespace = (
|
|
|
172
114
|
}
|
|
173
115
|
return {
|
|
174
116
|
namespace,
|
|
175
|
-
namespaceTarget: decodeEnvelope('namespace target', namespace.target, zeropsNamespaceTargetCodec),
|
|
117
|
+
namespaceTarget: decodeEnvelope('namespace target', 'zerops', namespace.target, zeropsNamespaceTargetCodec),
|
|
176
118
|
}
|
|
177
119
|
}
|
|
178
120
|
|
|
179
121
|
const resolvedEnvironment = (
|
|
180
122
|
environment: ProviderEnvironment,
|
|
181
123
|
options: { requireReady: boolean },
|
|
182
|
-
): DecodedZeropsEnvironment & { projectId: string
|
|
124
|
+
): DecodedZeropsEnvironment & { projectId: string } => {
|
|
183
125
|
const decoded = decodeEnvironment(environment)
|
|
184
126
|
const projectId = decoded.namespaceTarget.projectId
|
|
185
127
|
if (projectId === undefined) {
|
|
@@ -188,34 +130,20 @@ const resolvedEnvironment = (
|
|
|
188
130
|
if (options.requireReady && decoded.namespaceTarget.ready !== true) {
|
|
189
131
|
throw new Error(`Zerops deployment namespace \`${decoded.namespace.id}\` is not ready`)
|
|
190
132
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw new Error(`Zerops deployment namespace \`${decoded.namespace.id}\` has no proxy build source`)
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
...decoded,
|
|
201
|
-
projectId,
|
|
202
|
-
...(proxyServiceId === undefined ? {} : { proxyServiceId }),
|
|
203
|
-
...(proxyBuildFromGit === undefined ? {} : { proxyBuildFromGit }),
|
|
133
|
+
return { ...decoded, projectId }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const assertEnvironmentOfApp = (app: ProviderApp, environment: ProviderEnvironment): void => {
|
|
137
|
+
if (app.id !== environment.appId) {
|
|
138
|
+
throw new Error(`Zerops environment belongs to app \`${environment.appId}\`, expected \`${app.id}\``)
|
|
204
139
|
}
|
|
205
140
|
}
|
|
206
141
|
|
|
207
142
|
const normalizeRegistration = (input: ProviderRegistrationInput): ProviderRegistration => {
|
|
208
|
-
|
|
209
|
-
throw new Error(`Zerops environment belongs to app \`${input.environment.appId}\`, expected \`${input.app.id}\``)
|
|
210
|
-
}
|
|
211
|
-
if ((input.app.source.githubConnectionId === undefined) !== (input.app.source.githubInstallationId === undefined)) {
|
|
212
|
-
throw new Error('Zerops private source requires both connection and installation coordinates')
|
|
213
|
-
}
|
|
143
|
+
assertEnvironmentOfApp(input.app, input.environment)
|
|
214
144
|
const decoded = decodeEnvironment(input.environment)
|
|
215
|
-
const artifact =
|
|
216
|
-
|
|
217
|
-
{ appId: input.app.id, env: input.environment.env },
|
|
218
|
-
)
|
|
145
|
+
const artifact = decodeEnvelope('artifact', 'zerops', input.environment.artifact, zeropsArtifactCodec)
|
|
146
|
+
assertFabrikaManifestCoordinates(artifact.app, { appId: input.app.id, env: input.environment.env })
|
|
219
147
|
return {
|
|
220
148
|
app: input.app,
|
|
221
149
|
environment: {
|
|
@@ -232,112 +160,89 @@ const normalizeRegistration = (input: ProviderRegistrationInput): ProviderRegist
|
|
|
232
160
|
|
|
233
161
|
const abortSignal = (): AbortSignal => new AbortController().signal
|
|
234
162
|
|
|
163
|
+
/** Every private Zerops source names its keyed connection (ADR-0029). */
|
|
164
|
+
const privateBinding = (binding: ProviderSourceBinding | undefined): ZeropsSourcePrivateBinding | undefined =>
|
|
165
|
+
binding === undefined ? undefined : { connectionId: binding.connectionId, installationId: binding.installationId }
|
|
166
|
+
|
|
235
167
|
const IMMUTABLE_GIT_OBJECT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/
|
|
236
|
-
const BUILD_TRIGGER_CONSISTENCY_MS = 10_000
|
|
237
|
-
const SOURCE_CANCEL_TIMEOUT_MS = 5000
|
|
238
168
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
if (typeof appVersionId !== 'string' || appVersionId === '' || appVersionId !== externalId) {
|
|
246
|
-
throw new Error('Zerops run state appVersionId must match the external id')
|
|
247
|
-
}
|
|
248
|
-
if (phase === 'version_created' || phase === 'source_uploaded' || phase === 'build_trigger_requested') {
|
|
249
|
-
if (Object.keys(value).some((key) => key !== 'appVersionId' && key !== 'phase')) {
|
|
250
|
-
throw new Error('Zerops run state contains an unknown field')
|
|
251
|
-
}
|
|
252
|
-
return { appVersionId, phase }
|
|
253
|
-
}
|
|
254
|
-
if (phase !== 'build_triggered') {
|
|
255
|
-
throw new Error('Zerops run state phase is invalid')
|
|
256
|
-
}
|
|
257
|
-
if (Object.keys(value).some((key) => key !== 'appVersionId' && key !== 'phase' && key !== 'processId')) {
|
|
258
|
-
throw new Error('Zerops run state contains an unknown field')
|
|
259
|
-
}
|
|
260
|
-
const processId = value['processId']
|
|
261
|
-
if (processId !== undefined && (typeof processId !== 'string' || processId === '')) {
|
|
262
|
-
throw new Error('Zerops run state processId must be a non-empty string when present')
|
|
263
|
-
}
|
|
264
|
-
return { appVersionId, phase, ...(processId === undefined ? {} : { processId }) }
|
|
169
|
+
const checkpointSchema = z.object({ appVersionId: z.string().min(1), triggered: z.boolean() }).strict()
|
|
170
|
+
|
|
171
|
+
/** The run checkpoint (ADR-0044), or null for anything else — an older shape included. */
|
|
172
|
+
export const parseZeropsCheckpoint = (value: unknown): ZeropsRunState | null => {
|
|
173
|
+
const result = checkpointSchema.safeParse(value)
|
|
174
|
+
return result.success ? result.data : null
|
|
265
175
|
}
|
|
266
176
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
177
|
+
/** The recorded checkpoint for exactly this run's external id, or null. */
|
|
178
|
+
const runCheckpoint = (input: { providerState?: JsonValue; externalId: string }): ZeropsRunState | null => {
|
|
179
|
+
const checkpoint = parseZeropsCheckpoint(input.providerState)
|
|
180
|
+
return checkpoint !== null && checkpoint.appVersionId === input.externalId ? checkpoint : null
|
|
181
|
+
}
|
|
271
182
|
|
|
272
|
-
const
|
|
273
|
-
appVersionId: string,
|
|
274
|
-
processId?: string,
|
|
275
|
-
): Extract<ZeropsRunState, { phase: 'build_triggered' }> => ({
|
|
276
|
-
appVersionId,
|
|
277
|
-
phase: 'build_triggered',
|
|
278
|
-
...(processId === undefined ? {} : { processId }),
|
|
279
|
-
})
|
|
183
|
+
const pastDeadline = (startedAtMs: number | undefined): boolean => startedAtMs !== undefined && Date.now() > startedAtMs + ZEROPS_VERSION_DEADLINE_MS
|
|
280
184
|
|
|
281
185
|
/** Build the complete Zerops lifecycle bundle without importing control-plane persistence. */
|
|
282
|
-
export const createZeropsControlProvider = (options: ZeropsControlProviderOptions):
|
|
186
|
+
export const createZeropsControlProvider = (options: ZeropsControlProviderOptions): ControlProvider => {
|
|
283
187
|
if (options.accessToken === '') {
|
|
284
188
|
throw new Error('Zerops access token must be a non-empty string')
|
|
285
189
|
}
|
|
286
190
|
const api = options.api ?? createZeropsApi({ token: options.accessToken, baseUrl: options.apiBaseUrl })
|
|
287
|
-
const
|
|
288
|
-
|
|
191
|
+
const getVersion = async (input: { appVersionId: string; signal: AbortSignal }): Promise<ZeropsAppVersion | null> => {
|
|
192
|
+
input.signal.throwIfAborted()
|
|
193
|
+
try {
|
|
194
|
+
return await api.getAppVersion(input)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
input.signal.throwIfAborted()
|
|
197
|
+
if (!isVersionNotFound(error)) throw error
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const deployRun = createZeropsDeployer((target) => {
|
|
289
202
|
const defaults = defaultZeropsCollaborators(target)
|
|
290
203
|
return {
|
|
291
204
|
api,
|
|
292
205
|
source: options.source,
|
|
293
|
-
...(options.sourceCancelSleep === undefined ? {} : { sourceCancelSleep: options.sourceCancelSleep }),
|
|
294
206
|
reconcileSchema: options.reconcileSchema ?? defaults.reconcileSchema,
|
|
295
207
|
sleep: options.sleep ?? defaultSleep,
|
|
296
208
|
}
|
|
297
|
-
}, {
|
|
298
|
-
bindingForRun: (runId) => sourceBindings.get(runId),
|
|
299
|
-
uploadV2: async (input) => {
|
|
300
|
-
const uploadV2 = options.source.uploadV2
|
|
301
|
-
if (uploadV2 === undefined) throw new Error('Zerops keyed source transport is unavailable')
|
|
302
|
-
return uploadV2.call(options.source, input)
|
|
303
|
-
},
|
|
304
209
|
})
|
|
305
210
|
const namespaceCapabilities = options.namespaces === undefined
|
|
306
211
|
? undefined
|
|
307
212
|
: createZeropsNamespaceCapabilities({
|
|
308
213
|
...options.namespaces,
|
|
214
|
+
source: options.source,
|
|
309
215
|
api,
|
|
310
216
|
...(options.sleep !== undefined ? { sleep: options.sleep } : {}),
|
|
311
217
|
})
|
|
312
218
|
|
|
313
|
-
const artifactFor = (environment: ProviderEnvironment): FabrikaManifest =>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
219
|
+
const artifactFor = (environment: ProviderEnvironment): FabrikaManifest => {
|
|
220
|
+
const artifact = decodeEnvelope('artifact', 'zerops', environment.artifact, zeropsArtifactCodec)
|
|
221
|
+
assertFabrikaManifestCoordinates(artifact.app, { appId: environment.appId, env: environment.env })
|
|
222
|
+
return artifact
|
|
223
|
+
}
|
|
318
224
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
sourceController.abort()
|
|
225
|
+
/**
|
|
226
|
+
* A variable written through the env API resolves `${host_KEY}` at container start like `run.envVariables`
|
|
227
|
+
* does, across services whatever `envIsolation` says — so an unchecked secret reads the proxy's key.
|
|
228
|
+
*/
|
|
229
|
+
const assertOwnReferencesOnly = (environment: ProviderEnvironment, name: string, value: string): void => {
|
|
230
|
+
// A secret naming no service at all must not start failing on an artifact that no longer parses.
|
|
231
|
+
if (serviceReferenceHosts(name).length === 0 && serviceReferenceHosts(value).length === 0) return
|
|
232
|
+
const allowed = allowedReferenceHosts(manifestServiceHostnames(artifactFor(environment)))
|
|
233
|
+
const foreign = foreignReferenceHost(name, allowed) ?? foreignReferenceHost(value, allowed)
|
|
234
|
+
if (foreign !== null) {
|
|
235
|
+
throw new ProviderSecretError(`zerops: secret \`${name}\` references service \`${foreign}\`, which this app does not own`, 'foreignReference')
|
|
236
|
+
}
|
|
332
237
|
}
|
|
333
238
|
|
|
334
239
|
const reconcileActiveSchema = async (
|
|
335
|
-
input:
|
|
336
|
-
placement: ReturnType<typeof resolvedEnvironment>,
|
|
240
|
+
input: { environment: ProviderEnvironment; returnOrigins?: readonly string[] },
|
|
337
241
|
signal: AbortSignal,
|
|
338
242
|
): Promise<void> => {
|
|
339
243
|
const artifact = artifactFor(input.environment)
|
|
340
244
|
if (artifact.app.schema === undefined || options.propustkaUrl === undefined) return
|
|
245
|
+
const placement = resolvedEnvironment(input.environment, { requireReady: false })
|
|
341
246
|
const runtimeTarget: ZeropsRuntimeTarget = {
|
|
342
247
|
projectId: placement.projectId,
|
|
343
248
|
serviceId: placement.target.serviceId,
|
|
@@ -347,6 +252,7 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
347
252
|
...(options.adminKey !== undefined ? { adminKey: options.adminKey } : {}),
|
|
348
253
|
}
|
|
349
254
|
const reconcileSchema = options.reconcileSchema ?? defaultZeropsCollaborators(runtimeTarget).reconcileSchema
|
|
255
|
+
signal.throwIfAborted()
|
|
350
256
|
await reconcileSchema({
|
|
351
257
|
url: options.propustkaUrl,
|
|
352
258
|
app: artifact.app.id,
|
|
@@ -357,88 +263,44 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
357
263
|
})
|
|
358
264
|
}
|
|
359
265
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
signal: AbortSignal,
|
|
365
|
-
): Promise<ProviderReconcileOutcome> => {
|
|
366
|
-
if (state.processId !== undefined) {
|
|
367
|
-
const process = await api.getProcess({ processId: state.processId, signal })
|
|
368
|
-
if (process.id !== state.processId) throw new Error('Zerops process response has different coordinates')
|
|
369
|
-
if (
|
|
370
|
-
process.status !== undefined
|
|
371
|
-
&& ZEROPS_PROCESS_TERMINAL.has(process.status)
|
|
372
|
-
&& process.status !== ZEROPS_PROCESS_FINISHED
|
|
373
|
-
) {
|
|
374
|
-
return { state: 'failed' }
|
|
375
|
-
}
|
|
266
|
+
const assertVersionCoordinates = (environment: ProviderEnvironment, appVersionId: string, version: ZeropsAppVersion): void => {
|
|
267
|
+
const serviceId = decodeEnvironment(environment).target.serviceId
|
|
268
|
+
if (version.id !== appVersionId || (version.serviceStackId !== undefined && version.serviceStackId !== serviceId)) {
|
|
269
|
+
throw new Error('Zerops app-version response has different coordinates')
|
|
376
270
|
}
|
|
377
|
-
const version = await api.getAppVersion({ appVersionId: state.appVersionId, signal })
|
|
378
|
-
if (version.id !== state.appVersionId) throw new Error('Zerops app-version response has different coordinates')
|
|
379
|
-
if (version.status === ZEROPS_ACTIVE) {
|
|
380
|
-
await reconcileActiveSchema(input, placement, signal)
|
|
381
|
-
return { state: 'succeeded' }
|
|
382
|
-
}
|
|
383
|
-
if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) return { state: 'failed' }
|
|
384
|
-
return { state: 'running' }
|
|
385
271
|
}
|
|
386
272
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const connectionId = input.app.source.githubConnectionId
|
|
392
|
-
const installationId = input.app.source.githubInstallationId
|
|
393
|
-
if (binding === undefined) {
|
|
394
|
-
if (connectionId !== undefined || installationId !== undefined) {
|
|
395
|
-
throw new Error('Zerops private source requires an explicit transport binding')
|
|
396
|
-
}
|
|
397
|
-
return
|
|
398
|
-
}
|
|
399
|
-
if (connectionId !== binding.connectionId || installationId !== binding.installationId) {
|
|
400
|
-
throw new Error('Zerops source transport binding has different application coordinates')
|
|
401
|
-
}
|
|
273
|
+
/** Best-effort: a refused or slow cancel-build must not decide the outcome. */
|
|
274
|
+
const cancelBuildQuietly = async (appVersionId: string, signal: AbortSignal): Promise<void> => {
|
|
275
|
+
signal.throwIfAborted()
|
|
276
|
+
await api.cancelBuild({ appVersionId, signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) }).catch(() => {})
|
|
402
277
|
}
|
|
403
278
|
|
|
404
|
-
const resolveSource = async (
|
|
405
|
-
input
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const registration = normalizeRegistration({ app: input.app, environment: input.environment })
|
|
410
|
-
const artifact = decodeEnvelope('artifact', registration.environment.artifact, zeropsArtifactCodec)
|
|
279
|
+
const resolveSource = async (input: ProviderSourceResolutionInput): Promise<ProviderSourceResolution> => {
|
|
280
|
+
const binding = privateBinding(input.sourceBinding)
|
|
281
|
+
assertEnvironmentOfApp(input.app, input.environment)
|
|
282
|
+
decodeEnvironment(input.environment)
|
|
283
|
+
const artifact = artifactFor(input.environment)
|
|
411
284
|
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
requestedRef: registration.app.source.ref,
|
|
285
|
+
const result = await options.source.resolve({
|
|
286
|
+
repository: normalizeZeropsSourceRepository(input.app.source.repoUrl),
|
|
287
|
+
requestedRef: input.app.source.ref,
|
|
416
288
|
...(input.expectedCommitSha === undefined ? {} : { expectedCommitSha: input.expectedCommitSha }),
|
|
289
|
+
...(binding === undefined ? {} : { privateBinding: binding }),
|
|
417
290
|
descriptorSha256: artifact.target.sourceDescriptor.sha256,
|
|
418
291
|
signal: input.signal,
|
|
419
|
-
}
|
|
420
|
-
let result: ZeropsSourceResolveResult
|
|
421
|
-
if (binding === undefined) {
|
|
422
|
-
result = await options.source.resolve(base)
|
|
423
|
-
} else {
|
|
424
|
-
const resolveV2 = options.source.resolveV2
|
|
425
|
-
if (resolveV2 === undefined) throw new Error('Zerops keyed source transport is unavailable')
|
|
426
|
-
result = await resolveV2.call(options.source, {
|
|
427
|
-
...base,
|
|
428
|
-
privateBinding: { connectionId: binding.connectionId, installationId: binding.installationId },
|
|
429
|
-
})
|
|
430
|
-
}
|
|
292
|
+
})
|
|
431
293
|
if (
|
|
432
|
-
result.
|
|
433
|
-
|| result.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
|
|
294
|
+
result.descriptorSha256 !== artifact.target.sourceDescriptor.sha256
|
|
434
295
|
|| (input.expectedCommitSha !== undefined && result.commitSha !== input.expectedCommitSha)
|
|
435
296
|
) throw new Error('Zerops source resolution returned different coordinates')
|
|
436
297
|
return { commitSha: result.commitSha }
|
|
437
298
|
}
|
|
438
299
|
|
|
439
|
-
const controlProvider:
|
|
300
|
+
const controlProvider: ControlProvider = {
|
|
440
301
|
id: 'zerops',
|
|
441
302
|
normalizeRegistration,
|
|
303
|
+
source: { binding: 'connection' },
|
|
442
304
|
/**
|
|
443
305
|
* A Zerops artifact IS the compiled manifest, so the declaration is right here and core can refuse
|
|
444
306
|
* a variable this app would never read. An artifact that will not decode answers `undefined`
|
|
@@ -447,26 +309,20 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
447
309
|
*/
|
|
448
310
|
declaredVariables: ({ artifact }) => {
|
|
449
311
|
try {
|
|
450
|
-
return
|
|
312
|
+
return decodeEnvelope('artifact', 'zerops', artifact, zeropsArtifactCodec).app.pipeline.vars
|
|
451
313
|
} catch {
|
|
452
314
|
return undefined
|
|
453
315
|
}
|
|
454
316
|
},
|
|
455
|
-
resolveSource
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
const proxyBuildFromGit = placement.proxyBuildFromGit
|
|
466
|
-
if (proxyBuildFromGit === undefined) {
|
|
467
|
-
throw new Error(`Zerops deployment namespace \`${placement.namespace.id}\` has no proxy build source`)
|
|
468
|
-
}
|
|
469
|
-
const artifact = decodeEnvelope('artifact', registration.environment.artifact, zeropsArtifactCodec)
|
|
317
|
+
resolveSource,
|
|
318
|
+
deploy: async (input: ProviderDeployInput): Promise<ProviderDeployOutcome> => {
|
|
319
|
+
input.signal.throwIfAborted()
|
|
320
|
+
assertAuthoredManagedInputs({ variableNames: Object.keys(input.vars), secretNames: Object.keys(input.secrets) })
|
|
321
|
+
const managedEnvironment = parseManagedValues(input.managedEnvironment)
|
|
322
|
+
const binding = privateBinding(input.sourceBinding)
|
|
323
|
+
assertEnvironmentOfApp(input.app, input.environment)
|
|
324
|
+
const placement = resolvedEnvironment(input.environment, { requireReady: true })
|
|
325
|
+
const artifact = artifactFor(input.environment)
|
|
470
326
|
await verifyZeropsArtifactSourceDescriptor(artifact.target.sourceDescriptor)
|
|
471
327
|
let source: ZeropsRuntimeTarget['source']
|
|
472
328
|
if (!input.dryRun) {
|
|
@@ -477,54 +333,7 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
477
333
|
runId: input.runId,
|
|
478
334
|
repository: normalizeZeropsSourceRepository(input.app.source.repoUrl),
|
|
479
335
|
commitSha: input.app.source.ref,
|
|
480
|
-
...(
|
|
481
|
-
? {}
|
|
482
|
-
: { githubInstallationId: input.app.source.githubInstallationId }),
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (!input.dryRun) {
|
|
486
|
-
await options.beforeDeploy?.({
|
|
487
|
-
appId: input.app.id,
|
|
488
|
-
env: input.environment.env,
|
|
489
|
-
namespaceId: placement.namespace.id,
|
|
490
|
-
target: {
|
|
491
|
-
serviceId: placement.target.serviceId,
|
|
492
|
-
projectId: placement.projectId,
|
|
493
|
-
proxyServiceId,
|
|
494
|
-
proxyBuildFromGit,
|
|
495
|
-
},
|
|
496
|
-
artifact,
|
|
497
|
-
api,
|
|
498
|
-
signal: input.signal,
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
const managedEnvironment = input.managedEnvironment
|
|
502
|
-
const managedEntries = Object.entries(managedEnvironment).sort(([left], [right]) => left.localeCompare(right))
|
|
503
|
-
const managedNames = managedEntries.map(([name]) => name)
|
|
504
|
-
if (input.dryRun) {
|
|
505
|
-
if (managedNames.length > 0) {
|
|
506
|
-
input.events.log(
|
|
507
|
-
` [dry-run] would reconcile managed environment on service ${placement.target.serviceId}: ${managedNames.join(', ')}`,
|
|
508
|
-
)
|
|
509
|
-
}
|
|
510
|
-
} else {
|
|
511
|
-
const removedNames = new Set(managedEntries.filter(([, value]) => value === null).map(([name]) => name))
|
|
512
|
-
const existing = removedNames.size === 0
|
|
513
|
-
? []
|
|
514
|
-
: await api.listServiceEnv({ serviceId: placement.target.serviceId, signal: input.signal })
|
|
515
|
-
for (const [name, value] of managedEntries) {
|
|
516
|
-
if (value === null) {
|
|
517
|
-
for (const item of existing) {
|
|
518
|
-
if (item.key === name) await api.deleteServiceEnv({ envId: item.id, signal: input.signal })
|
|
519
|
-
}
|
|
520
|
-
} else {
|
|
521
|
-
await api.putServiceEnv({
|
|
522
|
-
serviceId: placement.target.serviceId,
|
|
523
|
-
key: name,
|
|
524
|
-
value,
|
|
525
|
-
signal: input.signal,
|
|
526
|
-
})
|
|
527
|
-
}
|
|
336
|
+
...(binding === undefined ? {} : { privateBinding: binding }),
|
|
528
337
|
}
|
|
529
338
|
}
|
|
530
339
|
const runtimeTarget: ZeropsRuntimeTarget = {
|
|
@@ -536,148 +345,100 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
536
345
|
...(options.propustkaUrl !== undefined ? { propustkaUrl: options.propustkaUrl } : {}),
|
|
537
346
|
...(options.adminKey !== undefined ? { adminKey: options.adminKey } : {}),
|
|
538
347
|
}
|
|
539
|
-
|
|
348
|
+
const result = await deployRun({
|
|
540
349
|
appId: input.app.id,
|
|
541
350
|
env: input.environment.env,
|
|
542
|
-
...(input.environment.domain !== undefined ? { domain: input.environment.domain } : {}),
|
|
543
351
|
...(input.returnOrigins === undefined ? {} : { returnOrigins: input.returnOrigins }),
|
|
544
|
-
cwd: input.app.source.workerDir ?? '.',
|
|
545
352
|
secrets: input.secrets,
|
|
546
353
|
vars: input.vars,
|
|
547
|
-
managedEnvironment
|
|
354
|
+
managedEnvironment,
|
|
355
|
+
target: runtimeTarget,
|
|
356
|
+
artifact,
|
|
548
357
|
dryRun: input.dryRun,
|
|
549
358
|
signal: input.signal,
|
|
550
359
|
events: input.events,
|
|
551
|
-
target: runtimeProvider.encodeTarget(runtimeTarget),
|
|
552
|
-
artifact: registration.environment.artifact,
|
|
553
360
|
})
|
|
361
|
+
input.signal.throwIfAborted()
|
|
362
|
+
return { state: result.status }
|
|
554
363
|
},
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
cancel: async (input) => {
|
|
567
|
-
resolvedEnvironment(input.environment, { requireReady: false })
|
|
568
|
-
const state = parseRunState(input.providerState, input.externalId)
|
|
569
|
-
await cancelSourceBestEffort(input.runId, state.appVersionId)
|
|
570
|
-
if (state.phase === 'build_triggered') {
|
|
571
|
-
await api.cancelBuild({ appVersionId: state.appVersionId, signal: abortSignal() })
|
|
572
|
-
return
|
|
573
|
-
}
|
|
574
|
-
if (state.phase === 'build_trigger_requested') {
|
|
575
|
-
await (options.sleep ?? defaultSleep)(BUILD_TRIGGER_CONSISTENCY_MS, abortSignal())
|
|
576
|
-
const version = await api.getAppVersion({ appVersionId: state.appVersionId, signal: abortSignal() })
|
|
577
|
-
if (version.id !== state.appVersionId) throw new Error('Zerops app-version response has different coordinates')
|
|
578
|
-
if (version.status === undefined) {
|
|
579
|
-
throw new Error('Zerops app-version status is not observable yet')
|
|
364
|
+
cancel: async (input): Promise<ProviderCancelOutcome> => {
|
|
365
|
+
const signal = input.signal
|
|
366
|
+
signal.throwIfAborted()
|
|
367
|
+
const checkpoint = runCheckpoint(input)
|
|
368
|
+
if (checkpoint === null) return { state: 'settled', outcome: 'failed' }
|
|
369
|
+
if (!checkpoint.triggered) {
|
|
370
|
+
try {
|
|
371
|
+
await deleteVersion(api, checkpoint.appVersionId, signal)
|
|
372
|
+
} catch {
|
|
373
|
+
signal.throwIfAborted()
|
|
374
|
+
return { state: 'pending' }
|
|
580
375
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
376
|
+
return { state: 'settled', outcome: 'failed' }
|
|
377
|
+
}
|
|
378
|
+
const version = await getVersion({ appVersionId: checkpoint.appVersionId, signal })
|
|
379
|
+
if (version === null) return { state: 'settled', outcome: 'failed' }
|
|
380
|
+
assertVersionCoordinates(input.environment, checkpoint.appVersionId, version)
|
|
381
|
+
// Cancellation never implies rollback: a version that activated first makes the run a success,
|
|
382
|
+
// which finishes the same IAM touchpoint a deploy does.
|
|
383
|
+
if (version.status === ZEROPS_ACTIVE) {
|
|
384
|
+
try {
|
|
385
|
+
await reconcileActiveSchema(input, signal)
|
|
386
|
+
} catch {
|
|
387
|
+
signal.throwIfAborted()
|
|
388
|
+
return { state: 'pending' }
|
|
584
389
|
}
|
|
390
|
+
return { state: 'settled', outcome: 'succeeded' }
|
|
585
391
|
}
|
|
586
|
-
|
|
392
|
+
if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) return { state: 'settled', outcome: 'failed' }
|
|
393
|
+
await cancelBuildQuietly(checkpoint.appVersionId, signal)
|
|
394
|
+
// A build request that never landed leaves the version UPLOADING for good; the deadline settles it.
|
|
395
|
+
return pastDeadline(input.startedAtMs) ? { state: 'settled', outcome: 'failed' } : { state: 'pending' }
|
|
587
396
|
},
|
|
588
397
|
reconcile: async (input): Promise<ProviderReconcileOutcome> => {
|
|
589
|
-
const signal =
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
398
|
+
const signal = input.signal
|
|
399
|
+
signal.throwIfAborted()
|
|
400
|
+
const log = async (line: string): Promise<void> => {
|
|
401
|
+
await input.log?.(line).catch(() => {})
|
|
402
|
+
}
|
|
403
|
+
const checkpoint = runCheckpoint(input)
|
|
404
|
+
if (checkpoint === null) {
|
|
405
|
+
await log('unrecognised checkpoint; failing without platform calls')
|
|
595
406
|
return { state: 'failed' }
|
|
596
407
|
}
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
} catch {
|
|
603
|
-
await cancelSourceBestEffort(input.runId, state.appVersionId)
|
|
604
|
-
await api.deleteAppVersion({ appVersionId: state.appVersionId, signal: abortSignal() })
|
|
605
|
-
return { state: 'failed' }
|
|
606
|
-
}
|
|
607
|
-
try {
|
|
608
|
-
await input.checkpoint(runState(state.appVersionId, 'build_trigger_requested'))
|
|
609
|
-
} catch {
|
|
610
|
-
return { state: 'running' }
|
|
611
|
-
}
|
|
612
|
-
let process: ZeropsProcess
|
|
613
|
-
try {
|
|
614
|
-
process = await api.buildAndDeployAppVersion({
|
|
615
|
-
appVersionId: state.appVersionId,
|
|
616
|
-
zeropsYaml: artifact.target.sourceDescriptor.contents,
|
|
617
|
-
...(artifact.target.zeropsSetup === undefined ? {} : { zeropsYamlSetup: artifact.target.zeropsSetup }),
|
|
618
|
-
signal,
|
|
619
|
-
})
|
|
620
|
-
} catch (error) {
|
|
621
|
-
if (error instanceof ZeropsApiError && error.status >= 400 && error.status < 500) {
|
|
622
|
-
await cancelSourceBestEffort(input.runId, state.appVersionId)
|
|
623
|
-
await api.deleteAppVersion({ appVersionId: state.appVersionId, signal: abortSignal() })
|
|
624
|
-
return { state: 'failed' }
|
|
625
|
-
}
|
|
626
|
-
let observed: ZeropsAppVersion
|
|
627
|
-
try {
|
|
628
|
-
await (options.sleep ?? defaultSleep)(BUILD_TRIGGER_CONSISTENCY_MS, signal)
|
|
629
|
-
observed = await api.getAppVersion({ appVersionId: state.appVersionId, signal })
|
|
630
|
-
} catch {
|
|
631
|
-
return { state: 'running' }
|
|
632
|
-
}
|
|
633
|
-
if (observed.id !== state.appVersionId || observed.status === undefined) return { state: 'running' }
|
|
634
|
-
if (observed.status === 'UPLOADING') {
|
|
635
|
-
await cancelSourceBestEffort(input.runId, state.appVersionId)
|
|
636
|
-
await api.deleteAppVersion({ appVersionId: state.appVersionId, signal: abortSignal() })
|
|
637
|
-
return { state: 'failed' }
|
|
638
|
-
}
|
|
639
|
-
const triggered = buildTriggeredState(state.appVersionId)
|
|
640
|
-
try {
|
|
641
|
-
await input.checkpoint(triggered)
|
|
642
|
-
} catch {
|
|
643
|
-
return { state: 'running' }
|
|
644
|
-
}
|
|
645
|
-
return reconcileTriggered(input, triggered, placement, signal)
|
|
646
|
-
}
|
|
647
|
-
const triggered = buildTriggeredState(state.appVersionId, process.id)
|
|
648
|
-
try {
|
|
649
|
-
await input.checkpoint(triggered)
|
|
650
|
-
} catch {
|
|
651
|
-
return { state: 'running' }
|
|
652
|
-
}
|
|
653
|
-
return reconcileTriggered(input, triggered, placement, signal)
|
|
408
|
+
if (!checkpoint.triggered) {
|
|
409
|
+
// A throw keeps the run running; the next maintenance pass retries the deletion.
|
|
410
|
+
await deleteVersion(api, checkpoint.appVersionId, signal)
|
|
411
|
+
await log(`deleted app-version ${checkpoint.appVersionId}; its build was never requested`)
|
|
412
|
+
return { state: 'failed' }
|
|
654
413
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
try {
|
|
671
|
-
await input.checkpoint(triggered)
|
|
672
|
-
} catch {
|
|
673
|
-
return { state: 'running' }
|
|
674
|
-
}
|
|
675
|
-
return reconcileTriggered(input, triggered, placement, signal)
|
|
414
|
+
let version: ZeropsAppVersion | null
|
|
415
|
+
try {
|
|
416
|
+
version = await getVersion({ appVersionId: checkpoint.appVersionId, signal })
|
|
417
|
+
} catch {
|
|
418
|
+
signal.throwIfAborted()
|
|
419
|
+
if (!pastDeadline(input.startedAtMs)) return { state: 'running' }
|
|
420
|
+
await cancelBuildQuietly(checkpoint.appVersionId, signal)
|
|
421
|
+
await log(`abandoned app-version ${checkpoint.appVersionId}: its status stayed unobservable past the run deadline`)
|
|
422
|
+
return { state: 'failed' }
|
|
423
|
+
}
|
|
424
|
+
if (version === null) return { state: 'failed' }
|
|
425
|
+
assertVersionCoordinates(input.environment, checkpoint.appVersionId, version)
|
|
426
|
+
if (version.status === ZEROPS_ACTIVE) {
|
|
427
|
+
await reconcileActiveSchema(input, signal)
|
|
428
|
+
return { state: 'succeeded' }
|
|
676
429
|
}
|
|
677
|
-
|
|
430
|
+
if (version.status !== undefined && ZEROPS_TERMINAL.has(version.status)) return { state: 'failed' }
|
|
431
|
+
if (pastDeadline(input.startedAtMs)) {
|
|
432
|
+
await cancelBuildQuietly(checkpoint.appVersionId, signal)
|
|
433
|
+
await log(`abandoned non-terminal app-version ${checkpoint.appVersionId} (${version.status ?? 'unknown'})`)
|
|
434
|
+
return { state: 'failed' }
|
|
435
|
+
}
|
|
436
|
+
return { state: 'running' }
|
|
678
437
|
},
|
|
679
438
|
secrets: {
|
|
680
439
|
put: async (input) => {
|
|
440
|
+
assertAuthoredManagedName(input.name)
|
|
441
|
+
assertOwnReferencesOnly(input.environment, input.name, input.value)
|
|
681
442
|
const target = decodeEnvironment(input.environment).target
|
|
682
443
|
await api.putServiceEnv({
|
|
683
444
|
serviceId: target.serviceId,
|
|
@@ -688,6 +449,7 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
688
449
|
return { valueRef: `zerops:${target.serviceId}/${encodeURIComponent(input.name)}` }
|
|
689
450
|
},
|
|
690
451
|
delete: async (input) => {
|
|
452
|
+
assertAuthoredManagedName(input.name)
|
|
691
453
|
const target = decodeEnvironment(input.environment).target
|
|
692
454
|
const variables = await api.listServiceEnv({ serviceId: target.serviceId, signal: abortSignal() })
|
|
693
455
|
const found = variables.find((variable) => variable.key === input.name)
|
|
@@ -707,13 +469,11 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
707
469
|
throw new Error(`Zerops deployment namespace \`${namespace.id}\` is not ready`)
|
|
708
470
|
}
|
|
709
471
|
const projectId = namespaceTarget.projectId
|
|
710
|
-
const manifest =
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
},
|
|
716
|
-
)
|
|
472
|
+
const manifest = decodeEnvelope('artifact', 'zerops', input.registration.environment.artifact, zeropsArtifactCodec)
|
|
473
|
+
assertFabrikaManifestCoordinates(manifest.app, {
|
|
474
|
+
appId: input.registration.app.id,
|
|
475
|
+
env: input.registration.environment.env,
|
|
476
|
+
})
|
|
717
477
|
const hostnames = manifestServiceHostnames(manifest)
|
|
718
478
|
const observed = await Promise.all(hostnames.map(async (hostname) => ({
|
|
719
479
|
hostname,
|
|
@@ -723,20 +483,30 @@ export const createZeropsControlProvider = (options: ZeropsControlProviderOption
|
|
|
723
483
|
if (present.length !== 0 && present.length !== observed.length) {
|
|
724
484
|
throw new Error(`Zerops registration found only ${present.length} of ${observed.length} declared services`)
|
|
725
485
|
}
|
|
726
|
-
|
|
486
|
+
const existingDeploy = observed.find((entry) => entry.hostname === manifest.target.deployService)?.service
|
|
487
|
+
if (present.length > 0 && (existingDeploy === null || existingDeploy === undefined)) {
|
|
488
|
+
throw new Error('Zerops registration has no deploy service')
|
|
489
|
+
}
|
|
490
|
+
input.signal.throwIfAborted()
|
|
491
|
+
// The first import refuses name collisions atomically, including services created after the lookup.
|
|
727
492
|
const imported = await api.importServices({
|
|
728
493
|
projectId,
|
|
729
494
|
yaml: present.length === 0 ? renderFabrikaProvisioningYaml(manifest) : renderFabrikaImportYaml(manifest),
|
|
730
495
|
signal: input.signal,
|
|
731
496
|
})
|
|
497
|
+
for (const service of imported.services) {
|
|
498
|
+
for (const process of service.processes) {
|
|
499
|
+
await waitForProcess({
|
|
500
|
+
api,
|
|
501
|
+
processId: process.id,
|
|
502
|
+
sleep: options.sleep ?? defaultSleep,
|
|
503
|
+
signal: input.signal,
|
|
504
|
+
label: `the ${service.name} import`,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
}
|
|
732
508
|
const reported = imported.services.find((service) => service.name === manifest.target.deployService)
|
|
733
|
-
const
|
|
734
|
-
const discovered = reported ?? observedDeployService
|
|
735
|
-
?? await api.findService({
|
|
736
|
-
projectId,
|
|
737
|
-
hostname: manifest.target.deployService,
|
|
738
|
-
signal: input.signal,
|
|
739
|
-
})
|
|
509
|
+
const discovered = present.length === 0 ? reported : existingDeploy
|
|
740
510
|
if (discovered === null || discovered === undefined || discovered.id === '') {
|
|
741
511
|
throw new Error(`Zerops import did not create deploy service \`${manifest.target.deployService}\``)
|
|
742
512
|
}
|