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