@serve.zone/interfaces 32.34.0 → 32.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/changelog.md +71 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/data/clusternode.d.ts +16 -2
  4. package/dist_ts/data/clusternode.js +11 -1
  5. package/dist_ts/data/deploymentlegacyimport.d.ts +92 -0
  6. package/dist_ts/data/deploymentlegacyimport.js +237 -0
  7. package/dist_ts/data/deploymentoperation.d.ts +63 -38
  8. package/dist_ts/data/deploymentoperation.js +12 -2
  9. package/dist_ts/data/deploymentpreflight.d.ts +42 -10
  10. package/dist_ts/data/deploymentpreflight.js +47 -10
  11. package/dist_ts/data/immutableimage.d.ts +26 -2
  12. package/dist_ts/data/immutableimage.js +12 -2
  13. package/dist_ts/data/index.d.ts +5 -0
  14. package/dist_ts/data/index.js +6 -1
  15. package/dist_ts/data/networknode.d.ts +16 -6
  16. package/dist_ts/data/networknode.js +1 -1
  17. package/dist_ts/data/runtimeassignmentexec.d.ts +110 -0
  18. package/dist_ts/data/runtimeassignmentexec.js +164 -0
  19. package/dist_ts/data/runtimeassignmentlogs.d.ts +121 -0
  20. package/dist_ts/data/runtimeassignmentlogs.js +139 -0
  21. package/dist_ts/data/runtimeassignmentstats.d.ts +60 -0
  22. package/dist_ts/data/runtimeassignmentstats.js +93 -0
  23. package/dist_ts/data/runtimelocalvolumearchive.d.ts +103 -0
  24. package/dist_ts/data/runtimelocalvolumearchive.js +111 -0
  25. package/dist_ts/data/user.d.ts +6 -1
  26. package/dist_ts/data/user.js +6 -1
  27. package/dist_ts/requests/runtimesession.d.ts +81 -0
  28. package/dist_ts/requests/service.d.ts +51 -0
  29. package/package.json +2 -2
  30. package/readme.md +109 -1
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/data/clusternode.ts +17 -2
  33. package/ts/data/deploymentlegacyimport.ts +371 -0
  34. package/ts/data/deploymentoperation.ts +83 -39
  35. package/ts/data/deploymentpreflight.ts +86 -18
  36. package/ts/data/immutableimage.ts +42 -3
  37. package/ts/data/index.ts +5 -0
  38. package/ts/data/networknode.ts +12 -5
  39. package/ts/data/runtimeassignmentexec.ts +271 -0
  40. package/ts/data/runtimeassignmentlogs.ts +249 -0
  41. package/ts/data/runtimeassignmentstats.ts +149 -0
  42. package/ts/data/runtimelocalvolumearchive.ts +207 -0
  43. package/ts/data/user.ts +10 -0
  44. package/ts/requests/runtimesession.ts +102 -0
  45. package/ts/requests/service.ts +66 -0
@@ -0,0 +1,371 @@
1
+ import type {
2
+ TImmutableContainerPlatform,
3
+ TOciManifestMediaType,
4
+ TSha256Digest,
5
+ } from './immutableimage.js';
6
+ import { normalizeSha256Digest } from './immutableimage.js';
7
+ import { hasExactKeys, isPlainRecord } from '../private/dataproperties.js';
8
+
9
+ /**
10
+ * Legacy image import: a greenfield deployment whose image is the one a legacy `deployOnPush`
11
+ * service already runs, instead of one built from a source revision.
12
+ *
13
+ * A legacy service without deployable source still has an image in Cloudly's registry and, until
14
+ * its runtime is frozen, healthy replicas running it. An administrator captures that fact once as
15
+ * a {@link ILegacyServiceRuntimeAttestation} while the replicas still run. A greenfield successor
16
+ * reservation names the attestation in `legacyImport` instead of a `sourceRevision`, and Cloudly
17
+ * then imports the attested image into the successor's own repository and release tag itself:
18
+ * the bytes never leave the registry and the release records that it was imported, not built.
19
+ *
20
+ * Deployment roots are indexes. An attested root that already is an index is imported as it is. A
21
+ * root that is one platform manifest is imported under an index whose only descriptor is that
22
+ * manifest, in the manifest's own family — a Docker manifest list around a Docker manifest, an OCI
23
+ * index around an OCI manifest — so the platform manifest stays byte-identical and its digest is
24
+ * still the one the legacy runtime ran. The attestation states everything that descriptor needs.
25
+ */
26
+
27
+ /** The media types a legacy image's per-platform manifest can carry. */
28
+ export type TLegacyImagePlatformManifestMediaType =
29
+ | 'application/vnd.oci.image.manifest.v1+json'
30
+ | 'application/vnd.docker.distribution.manifest.v2+json';
31
+
32
+ /** The index media type of the family a platform manifest belongs to. */
33
+ export type TLegacyImageImportIndexMediaType =
34
+ | 'application/vnd.oci.image.index.v1+json'
35
+ | 'application/vnd.docker.distribution.manifest.list.v2+json';
36
+
37
+ /** One platform manifest of a legacy image, as Cloudly read it from its own registry. */
38
+ export interface ILegacyImagePlatformManifest {
39
+ platform: TImmutableContainerPlatform;
40
+ manifestDigest: TSha256Digest;
41
+ manifestMediaType: TLegacyImagePlatformManifestMediaType;
42
+ /** Exact byte length of the platform manifest, which an index descriptor states. */
43
+ manifestSize: number;
44
+ imageConfigDigest: TSha256Digest;
45
+ }
46
+
47
+ /** The registry image a legacy service ran, under the mutable tag it was deployed from. */
48
+ export interface ILegacyServiceImageSource {
49
+ registryHost: string;
50
+ repository: string;
51
+ /** The mutable tag the legacy service was deployed from, `latest` included. */
52
+ tag: string;
53
+ /** Digest of the root the tag named: a single platform manifest or an index. */
54
+ rootDigest: TSha256Digest;
55
+ rootMediaType: TOciManifestMediaType;
56
+ /**
57
+ * The runnable Linux platforms the root carries, sorted by platform. A single-manifest root
58
+ * carries exactly one, whose manifest is the root itself.
59
+ */
60
+ platforms: ILegacyImagePlatformManifest[];
61
+ }
62
+
63
+ /** One healthy replica of the legacy service, observed running the attested digest. */
64
+ export interface ILegacyServiceRuntimeTaskAttestation {
65
+ deploymentId: string;
66
+ nodeName: string;
67
+ observedDigest: TSha256Digest;
68
+ }
69
+
70
+ /**
71
+ * Durable evidence, captured by an administrator while the legacy runtime still ran, that every
72
+ * required replica of a legacy service was healthy and ran exactly the registry root it names.
73
+ *
74
+ * Cloudly writes one and never changes it. A capture only succeeds on complete evidence, so a
75
+ * stored attestation is by construction one whose runtime digest is its source root digest.
76
+ */
77
+ export interface ILegacyServiceRuntimeAttestation {
78
+ id: string;
79
+ data: {
80
+ legacyServiceId: string;
81
+ organizationId: string;
82
+ source: ILegacyServiceImageSource;
83
+ runtime: {
84
+ requiredReplicaCount: number;
85
+ healthyReplicaCount: number;
86
+ runtimeDigest: TSha256Digest;
87
+ /** Sorted by deploymentId, one per required replica. */
88
+ tasks: ILegacyServiceRuntimeTaskAttestation[];
89
+ };
90
+ capturedByUserId: string;
91
+ capturedAt: number;
92
+ };
93
+ }
94
+
95
+ /**
96
+ * What a legacy-import reservation names in place of a source revision: the legacy service, the
97
+ * attestation Cloudly holds for it, and the root digest the deployer asserts that attestation
98
+ * carries.
99
+ */
100
+ export interface ILegacyImageImportReference {
101
+ legacyServiceId: string;
102
+ attestationId: string;
103
+ sourceRootDigest: TSha256Digest;
104
+ }
105
+
106
+ const identifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
107
+ /** An OCI tag, the mutable `latest` included: the source tag is whatever the legacy service ran. */
108
+ const ociTagRegex = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
109
+ const registryHostRegex = /^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?(?::[0-9]{1,5})?$/;
110
+ const repositoryRegex = /^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*$/;
111
+ const maximumManifestSize = 4 * 1024 * 1024;
112
+ const maximumReplicaCount = 64;
113
+
114
+ const legacyImagePlatformManifestMediaTypes: readonly TLegacyImagePlatformManifestMediaType[] = [
115
+ 'application/vnd.oci.image.manifest.v1+json',
116
+ 'application/vnd.docker.distribution.manifest.v2+json',
117
+ ];
118
+ const legacyImageImportIndexMediaTypes: readonly TLegacyImageImportIndexMediaType[] = [
119
+ 'application/vnd.oci.image.index.v1+json',
120
+ 'application/vnd.docker.distribution.manifest.list.v2+json',
121
+ ];
122
+ const legacyImagePlatforms: readonly TImmutableContainerPlatform[] = ['linux/amd64', 'linux/arm64'];
123
+
124
+ const isIdentifier = (valueArg: unknown): valueArg is string => (
125
+ typeof valueArg === 'string' && identifierRegex.test(valueArg)
126
+ );
127
+
128
+ const isCanonicalDigest = (valueArg: unknown): valueArg is TSha256Digest => (
129
+ typeof valueArg === 'string' && normalizeSha256Digest(valueArg) === valueArg
130
+ );
131
+
132
+ const isPositiveSafeInteger = (valueArg: unknown, maximumArg: number): valueArg is number => (
133
+ typeof valueArg === 'number'
134
+ && Number.isSafeInteger(valueArg)
135
+ && valueArg >= 1
136
+ && valueArg <= maximumArg
137
+ );
138
+
139
+ /** True when a media type names a single platform manifest rather than an index. */
140
+ export const isLegacyImagePlatformManifestMediaType = (
141
+ valueArg: unknown,
142
+ ): valueArg is TLegacyImagePlatformManifestMediaType => (
143
+ legacyImagePlatformManifestMediaTypes.some((mediaTypeArg) => mediaTypeArg === valueArg)
144
+ );
145
+
146
+ /** True when a media type names an index of either family. */
147
+ export const isLegacyImageImportIndexMediaType = (
148
+ valueArg: unknown,
149
+ ): valueArg is TLegacyImageImportIndexMediaType => (
150
+ legacyImageImportIndexMediaTypes.some((mediaTypeArg) => mediaTypeArg === valueArg)
151
+ );
152
+
153
+ export const validateLegacyImageImportReference = (referenceArg: unknown): string[] => {
154
+ if (!isPlainRecord(referenceArg)) return ['legacyImport must be an object'];
155
+ const errors: string[] = [];
156
+ if (!hasExactKeys(referenceArg, ['legacyServiceId', 'attestationId', 'sourceRootDigest'])) {
157
+ errors.push('legacyImport must use its exact schema');
158
+ }
159
+ if (!isIdentifier(referenceArg.legacyServiceId)) {
160
+ errors.push('legacyImport.legacyServiceId must be a bounded canonical identifier');
161
+ }
162
+ if (!isIdentifier(referenceArg.attestationId)) {
163
+ errors.push('legacyImport.attestationId must be a bounded canonical identifier');
164
+ }
165
+ if (!isCanonicalDigest(referenceArg.sourceRootDigest)) {
166
+ errors.push('legacyImport.sourceRootDigest must be a canonical sha256 digest');
167
+ }
168
+ return errors;
169
+ };
170
+
171
+ const validatePlatformManifest = (
172
+ manifestArg: unknown,
173
+ indexArg: number,
174
+ ): string[] => {
175
+ const label = `source.platforms[${indexArg}]`;
176
+ if (!isPlainRecord(manifestArg)) return [`${label} must be an object`];
177
+ const errors: string[] = [];
178
+ if (!hasExactKeys(manifestArg, [
179
+ 'platform',
180
+ 'manifestDigest',
181
+ 'manifestMediaType',
182
+ 'manifestSize',
183
+ 'imageConfigDigest',
184
+ ])) {
185
+ errors.push(`${label} must use its exact schema`);
186
+ }
187
+ if (!legacyImagePlatforms.some((platformArg) => platformArg === manifestArg.platform)) {
188
+ errors.push(`${label}.platform must be linux/amd64 or linux/arm64`);
189
+ }
190
+ if (!isCanonicalDigest(manifestArg.manifestDigest)) {
191
+ errors.push(`${label}.manifestDigest must be a canonical sha256 digest`);
192
+ }
193
+ if (!isLegacyImagePlatformManifestMediaType(manifestArg.manifestMediaType)) {
194
+ errors.push(`${label}.manifestMediaType must name an OCI or Docker image manifest`);
195
+ }
196
+ if (!isPositiveSafeInteger(manifestArg.manifestSize, maximumManifestSize)) {
197
+ errors.push(`${label}.manifestSize must be a positive bounded byte length`);
198
+ }
199
+ if (!isCanonicalDigest(manifestArg.imageConfigDigest)) {
200
+ errors.push(`${label}.imageConfigDigest must be a canonical sha256 digest`);
201
+ }
202
+ return errors;
203
+ };
204
+
205
+ const validateImageSource = (sourceArg: unknown): string[] => {
206
+ if (!isPlainRecord(sourceArg)) return ['source must be an object'];
207
+ const errors: string[] = [];
208
+ if (!hasExactKeys(sourceArg, [
209
+ 'registryHost',
210
+ 'repository',
211
+ 'tag',
212
+ 'rootDigest',
213
+ 'rootMediaType',
214
+ 'platforms',
215
+ ])) {
216
+ errors.push('source must use its exact schema');
217
+ }
218
+ if (typeof sourceArg.registryHost !== 'string' || !registryHostRegex.test(sourceArg.registryHost)) {
219
+ errors.push('source.registryHost must be a canonical registry host');
220
+ }
221
+ if (typeof sourceArg.repository !== 'string'
222
+ || sourceArg.repository.length > 255
223
+ || !repositoryRegex.test(sourceArg.repository)) {
224
+ errors.push('source.repository must be a canonical OCI repository name');
225
+ }
226
+ if (typeof sourceArg.tag !== 'string' || !ociTagRegex.test(sourceArg.tag)) {
227
+ errors.push('source.tag must be an OCI tag');
228
+ }
229
+ if (!isCanonicalDigest(sourceArg.rootDigest)) {
230
+ errors.push('source.rootDigest must be a canonical sha256 digest');
231
+ }
232
+ const rootIsManifest = isLegacyImagePlatformManifestMediaType(sourceArg.rootMediaType);
233
+ const rootIsIndex = isLegacyImageImportIndexMediaType(sourceArg.rootMediaType);
234
+ if (!rootIsManifest && !rootIsIndex) {
235
+ errors.push('source.rootMediaType must name an OCI or Docker manifest or index');
236
+ }
237
+ if (!Array.isArray(sourceArg.platforms)
238
+ || sourceArg.platforms.length < 1
239
+ || sourceArg.platforms.length > legacyImagePlatforms.length) {
240
+ errors.push('source.platforms must carry one or two runnable Linux platforms');
241
+ return errors;
242
+ }
243
+ const platforms = sourceArg.platforms as unknown[];
244
+ platforms.forEach((manifestArg, indexArg) => {
245
+ errors.push(...validatePlatformManifest(manifestArg, indexArg));
246
+ });
247
+ const platformNames = platforms.map((manifestArg) => (
248
+ isPlainRecord(manifestArg) ? manifestArg.platform : undefined
249
+ ));
250
+ const sortedPlatformNames = legacyImagePlatforms.filter((platformArg) => (
251
+ platformNames.includes(platformArg)
252
+ ));
253
+ if (JSON.stringify(platformNames) !== JSON.stringify(sortedPlatformNames)) {
254
+ errors.push('source.platforms must be unique and sorted by platform');
255
+ }
256
+ if (rootIsManifest) {
257
+ const onlyPlatform = platforms[0];
258
+ if (platforms.length !== 1
259
+ || !isPlainRecord(onlyPlatform)
260
+ || onlyPlatform.manifestDigest !== sourceArg.rootDigest
261
+ || onlyPlatform.manifestMediaType !== sourceArg.rootMediaType) {
262
+ errors.push('a single-manifest source root must be its own one platform manifest');
263
+ }
264
+ }
265
+ if (rootIsIndex && platforms.some((manifestArg) => (
266
+ isPlainRecord(manifestArg) && manifestArg.manifestDigest === sourceArg.rootDigest
267
+ ))) {
268
+ errors.push('an index source root cannot also be one of its platform manifests');
269
+ }
270
+ return errors;
271
+ };
272
+
273
+ const validateRuntime = (runtimeArg: unknown, rootDigestArg: unknown): string[] => {
274
+ if (!isPlainRecord(runtimeArg)) return ['runtime must be an object'];
275
+ const errors: string[] = [];
276
+ if (!hasExactKeys(runtimeArg, [
277
+ 'requiredReplicaCount',
278
+ 'healthyReplicaCount',
279
+ 'runtimeDigest',
280
+ 'tasks',
281
+ ])) {
282
+ errors.push('runtime must use its exact schema');
283
+ }
284
+ if (!isPositiveSafeInteger(runtimeArg.requiredReplicaCount, maximumReplicaCount)) {
285
+ errors.push('runtime.requiredReplicaCount must be a positive bounded replica count');
286
+ }
287
+ if (runtimeArg.healthyReplicaCount !== runtimeArg.requiredReplicaCount) {
288
+ errors.push('runtime.healthyReplicaCount must equal the required replica count');
289
+ }
290
+ if (!isCanonicalDigest(runtimeArg.runtimeDigest)) {
291
+ errors.push('runtime.runtimeDigest must be a canonical sha256 digest');
292
+ } else if (runtimeArg.runtimeDigest !== rootDigestArg) {
293
+ errors.push('runtime.runtimeDigest must be the attested source root digest');
294
+ }
295
+ if (!Array.isArray(runtimeArg.tasks) || runtimeArg.tasks.length !== runtimeArg.requiredReplicaCount) {
296
+ errors.push('runtime.tasks must hold exactly one task per required replica');
297
+ return errors;
298
+ }
299
+ const tasks = runtimeArg.tasks as unknown[];
300
+ tasks.forEach((taskArg, indexArg) => {
301
+ const label = `runtime.tasks[${indexArg}]`;
302
+ if (!isPlainRecord(taskArg)) {
303
+ errors.push(`${label} must be an object`);
304
+ return;
305
+ }
306
+ if (!hasExactKeys(taskArg, ['deploymentId', 'nodeName', 'observedDigest'])) {
307
+ errors.push(`${label} must use its exact schema`);
308
+ }
309
+ if (!isIdentifier(taskArg.deploymentId)) {
310
+ errors.push(`${label}.deploymentId must be a bounded canonical identifier`);
311
+ }
312
+ if (typeof taskArg.nodeName !== 'string' || !taskArg.nodeName || taskArg.nodeName.length > 253) {
313
+ errors.push(`${label}.nodeName must be a bounded non-empty name`);
314
+ }
315
+ if (taskArg.observedDigest !== runtimeArg.runtimeDigest) {
316
+ errors.push(`${label}.observedDigest must be the attested runtime digest`);
317
+ }
318
+ });
319
+ const deploymentIds = tasks.map((taskArg) => (
320
+ isPlainRecord(taskArg) ? taskArg.deploymentId : undefined
321
+ ));
322
+ const sortedDeploymentIds = deploymentIds
323
+ .filter((idArg): idArg is string => typeof idArg === 'string')
324
+ .sort();
325
+ if (new Set(deploymentIds).size !== deploymentIds.length
326
+ || JSON.stringify(deploymentIds) !== JSON.stringify(sortedDeploymentIds)) {
327
+ errors.push('runtime.tasks must be unique and sorted by deploymentId');
328
+ }
329
+ return errors;
330
+ };
331
+
332
+ export const validateLegacyServiceRuntimeAttestation = (attestationArg: unknown): string[] => {
333
+ if (!isPlainRecord(attestationArg)) return ['attestation must be an object'];
334
+ const errors: string[] = [];
335
+ if (!hasExactKeys(attestationArg, ['id', 'data'])) {
336
+ errors.push('attestation must use its exact schema');
337
+ }
338
+ if (!isIdentifier(attestationArg.id)) {
339
+ errors.push('id must be a bounded canonical identifier');
340
+ }
341
+ const data = attestationArg.data;
342
+ if (!isPlainRecord(data)) return [...errors, 'data must be an object'];
343
+ if (!hasExactKeys(data, [
344
+ 'legacyServiceId',
345
+ 'organizationId',
346
+ 'source',
347
+ 'runtime',
348
+ 'capturedByUserId',
349
+ 'capturedAt',
350
+ ])) {
351
+ errors.push('data must use its exact schema');
352
+ }
353
+ if (!isIdentifier(data.legacyServiceId)) {
354
+ errors.push('legacyServiceId must be a bounded canonical identifier');
355
+ }
356
+ if (!isIdentifier(data.organizationId)) {
357
+ errors.push('organizationId must be a bounded canonical identifier');
358
+ }
359
+ if (!isIdentifier(data.capturedByUserId)) {
360
+ errors.push('capturedByUserId must be a bounded canonical identifier');
361
+ }
362
+ if (!isPositiveSafeInteger(data.capturedAt, Number.MAX_SAFE_INTEGER)) {
363
+ errors.push('capturedAt must be a positive timestamp');
364
+ }
365
+ errors.push(...validateImageSource(data.source));
366
+ errors.push(...validateRuntime(
367
+ data.runtime,
368
+ isPlainRecord(data.source) ? data.source.rootDigest : undefined,
369
+ ));
370
+ return errors;
371
+ };
@@ -2,6 +2,8 @@ import type { IDeploymentPreflightProposedConfiguration, IDeploymentPreflightRun
2
2
  import type { IGatewayRouteDnsResult } from './gateway.js';
3
3
  import type { IImageRolloutStatus, IImmutableImageDeploymentPlan, TSha256Digest } from './immutableimage.js';
4
4
  import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
5
+ import type { ILegacyImageImportReference } from './deploymentlegacyimport.js';
6
+ import { validateLegacyImageImportReference } from './deploymentlegacyimport.js';
5
7
  import type { IRegistryTarget } from './registry.js';
6
8
  import { hasExactKeys } from '../private/dataproperties.js';
7
9
  import { normalizeServiceAbsolutePath, validateServiceContainerArgs } from './service.js';
@@ -14,7 +16,6 @@ interface IDeploymentReservationInputBase {
14
16
  organizationId: string;
15
17
  serviceId: string;
16
18
  idempotencyKey: string;
17
- sourceRevision: string;
18
19
  version: string;
19
20
  intentDigest: TSha256Digest;
20
21
  proposedConfiguration: IDeploymentPreflightProposedConfiguration;
@@ -22,15 +23,38 @@ interface IDeploymentReservationInputBase {
22
23
  routes: IDeploymentRouteRequest[];
23
24
  }
24
25
 
26
+ /** A deployment whose image its deployer builds and pushes from one source revision. */
27
+ interface ISourceDeploymentOrigin {
28
+ sourceRevision: string;
29
+ legacyImport?: never;
30
+ }
31
+
32
+ /**
33
+ * A greenfield deployment whose image Cloudly imports from an attested legacy service instead:
34
+ * there is no source revision, so none is stated.
35
+ */
36
+ interface ILegacyImportDeploymentOrigin {
37
+ legacyImport: ILegacyImageImportReference;
38
+ sourceRevision?: never;
39
+ }
40
+
25
41
  export interface IExistingServiceDeploymentReservationInput
26
- extends IDeploymentReservationInputBase {
42
+ extends IDeploymentReservationInputBase, ISourceDeploymentOrigin {
27
43
  mode: 'existing-service';
28
44
  expectedRolloutGeneration: number;
29
45
  expectedCurrentRolloutId?: string;
30
46
  }
31
47
 
32
48
  export interface IGreenfieldServiceDeploymentReservationInput
33
- extends IDeploymentReservationInputBase {
49
+ extends IDeploymentReservationInputBase, ISourceDeploymentOrigin {
50
+ mode: 'greenfield';
51
+ expectedRolloutGeneration?: never;
52
+ expectedCurrentRolloutId?: never;
53
+ }
54
+
55
+ /** A greenfield successor slot whose image is a legacy service's attested image. */
56
+ export interface ILegacyImportServiceDeploymentReservationInput
57
+ extends IDeploymentReservationInputBase, ILegacyImportDeploymentOrigin {
34
58
  mode: 'greenfield';
35
59
  expectedRolloutGeneration?: never;
36
60
  expectedCurrentRolloutId?: never;
@@ -38,7 +62,8 @@ extends IDeploymentReservationInputBase {
38
62
 
39
63
  export type IDeploymentReservationInput =
40
64
  | IExistingServiceDeploymentReservationInput
41
- | IGreenfieldServiceDeploymentReservationInput;
65
+ | IGreenfieldServiceDeploymentReservationInput
66
+ | ILegacyImportServiceDeploymentReservationInput;
42
67
 
43
68
  export type TDeploymentOperationState =
44
69
  | 'reserving'
@@ -240,7 +265,15 @@ export const validateDeploymentReservationInput = (inputArg: unknown): string[]
240
265
  errors.push(`${field} must be a bounded canonical identifier`);
241
266
  }
242
267
  }
243
- if (typeof inputArg.sourceRevision !== 'string'
268
+ if (inputArg.legacyImport !== undefined) {
269
+ errors.push(...validateLegacyImageImportReference(inputArg.legacyImport));
270
+ if (inputArg.mode !== 'greenfield') {
271
+ errors.push('a legacy import reserves a greenfield successor');
272
+ }
273
+ if (inputArg.sourceRevision !== undefined) {
274
+ errors.push('a legacy import has no source revision');
275
+ }
276
+ } else if (typeof inputArg.sourceRevision !== 'string'
244
277
  || !fullGitRevisionRegex.test(inputArg.sourceRevision)) {
245
278
  errors.push('sourceRevision must be a full lowercase git revision');
246
279
  }
@@ -394,40 +427,51 @@ export interface IDeploymentOperationFailure {
394
427
  export interface IServiceDeploymentOperation {
395
428
  id: string;
396
429
  revision: number;
397
- data: {
398
- mode: TDeploymentOperationMode;
399
- organizationId: string;
400
- serviceId: string;
401
- actorUserId: string;
402
- idempotencyKey: string;
403
- requestDigest: TSha256Digest;
404
- sourceRevision: string;
405
- version: string;
406
- intentDigest: TSha256Digest;
407
- configurationDigest: TSha256Digest;
408
- proposedConfiguration: IDeploymentPreflightProposedConfiguration;
409
- namespace: string;
410
- registryTarget: IRegistryTarget;
411
- releaseTag: string;
412
- routes: IDeploymentRouteRequest[];
413
- claimKeys: string[];
414
- state: TDeploymentOperationState;
415
- releaseId?: string;
416
- registryRootDigest?: TSha256Digest;
417
- rolloutId?: string;
418
- rolloutGeneration?: number;
419
- routeGeneration?: number;
420
- routeVerification?: IDeploymentRouteVerificationEvidence[];
421
- /**
422
- * Most recent failure record. Set when the operation fails and retained
423
- * as audit history through 'cleaning' and 'cleaned'; cleared only by a
424
- * forward transition such as a successful retry. A populated failure does
425
- * therefore NOT imply state === 'failed'.
426
- */
427
- failure?: IDeploymentOperationFailure;
428
- createdAt: number;
429
- updatedAt: number;
430
- };
430
+ data: IServiceDeploymentOperationData;
431
+ }
432
+
433
+ /**
434
+ * An operation's durable state. It states where its image comes from exactly as its reservation
435
+ * did: a `sourceRevision` for an image its deployer builds, or a `legacyImport` (greenfield only)
436
+ * for a legacy service's attested image Cloudly imports.
437
+ */
438
+ export type IServiceDeploymentOperationData = IServiceDeploymentOperationStateData & (
439
+ | ISourceDeploymentOrigin
440
+ | ILegacyImportDeploymentOrigin
441
+ );
442
+
443
+ interface IServiceDeploymentOperationStateData {
444
+ mode: TDeploymentOperationMode;
445
+ organizationId: string;
446
+ serviceId: string;
447
+ actorUserId: string;
448
+ idempotencyKey: string;
449
+ requestDigest: TSha256Digest;
450
+ version: string;
451
+ intentDigest: TSha256Digest;
452
+ configurationDigest: TSha256Digest;
453
+ proposedConfiguration: IDeploymentPreflightProposedConfiguration;
454
+ namespace: string;
455
+ registryTarget: IRegistryTarget;
456
+ releaseTag: string;
457
+ routes: IDeploymentRouteRequest[];
458
+ claimKeys: string[];
459
+ state: TDeploymentOperationState;
460
+ releaseId?: string;
461
+ registryRootDigest?: TSha256Digest;
462
+ rolloutId?: string;
463
+ rolloutGeneration?: number;
464
+ routeGeneration?: number;
465
+ routeVerification?: IDeploymentRouteVerificationEvidence[];
466
+ /**
467
+ * Most recent failure record. Set when the operation fails and retained
468
+ * as audit history through 'cleaning' and 'cleaned'; cleared only by a
469
+ * forward transition such as a successful retry. A populated failure does
470
+ * therefore NOT imply state === 'failed'.
471
+ */
472
+ failure?: IDeploymentOperationFailure;
473
+ createdAt: number;
474
+ updatedAt: number;
431
475
  }
432
476
 
433
477
  export interface IServiceDeploymentStatus {