@serve.zone/interfaces 32.67.0 → 32.69.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 (38) hide show
  1. package/changelog.md +20 -0
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/data/clusternode.d.ts +15 -2
  4. package/dist_ts/data/clusternode.js +1 -1
  5. package/dist_ts/data/deploymentoperation.d.ts +17 -3
  6. package/dist_ts/data/deploymentoperation.js +4 -1
  7. package/dist_ts/data/deploymentreleaseredeploy.d.ts +88 -0
  8. package/dist_ts/data/deploymentreleaseredeploy.js +120 -0
  9. package/dist_ts/data/immutableimage.d.ts +17 -3
  10. package/dist_ts/data/immutableimage.js +18 -1
  11. package/dist_ts/data/index.d.ts +3 -0
  12. package/dist_ts/data/index.js +4 -1
  13. package/dist_ts/data/nodeaction.d.ts +439 -0
  14. package/dist_ts/data/nodeaction.js +434 -0
  15. package/dist_ts/data/sparkfleettarget.d.ts +80 -0
  16. package/dist_ts/data/sparkfleettarget.js +71 -0
  17. package/dist_ts/data/sparknodereport.d.ts +19 -0
  18. package/dist_ts/data/sparknodereport.js +21 -4
  19. package/dist_ts/data/user.d.ts +7 -1
  20. package/dist_ts/data/user.js +6 -1
  21. package/dist_ts/requests/deployment.d.ts +5 -0
  22. package/dist_ts/requests/node.d.ts +100 -0
  23. package/dist_ts/requests/service.d.ts +42 -0
  24. package/package.json +2 -2
  25. package/readme.md +56 -0
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/data/clusternode.ts +17 -2
  28. package/ts/data/deploymentoperation.ts +21 -2
  29. package/ts/data/deploymentreleaseredeploy.ts +182 -0
  30. package/ts/data/immutableimage.ts +36 -3
  31. package/ts/data/index.ts +3 -0
  32. package/ts/data/nodeaction.ts +755 -0
  33. package/ts/data/sparkfleettarget.ts +138 -0
  34. package/ts/data/sparknodereport.ts +42 -3
  35. package/ts/data/user.ts +12 -1
  36. package/ts/requests/deployment.ts +5 -0
  37. package/ts/requests/node.ts +118 -0
  38. package/ts/requests/service.ts +55 -0
@@ -0,0 +1,182 @@
1
+ import { readDataProperties } from '../private/dataproperties.js';
2
+
3
+ /**
4
+ * Release redeploy: an existing service deploys one of its own releases again, from Cloudly's
5
+ * registry, without a build or a push.
6
+ *
7
+ * An administrator (or a deployment machine user granted `deployment:redeploy-release` on the
8
+ * service) starts it with `startServiceReleaseDeployment`, naming the release and the service's
9
+ * current rollout fence. Cloudly then runs the whole deployment itself: it creates an
10
+ * existing-service operation whose origin is `releaseRedeploy`, tags the release's root under the
11
+ * operation's own tag in the service's repository (the tag is the operation id, which starts with
12
+ * {@link releaseRedeployContract.tagPrefix} and so can never be a version string), records
13
+ * `cloudly-release-redeploy` evidence naming the source release, promotes the release and drives
14
+ * the operation through rollout, route promotion and public verification to `succeeded` or
15
+ * `failed`. The operation's status is the only statement that the deploy happened.
16
+ *
17
+ * What the redeploy declares is the declaration in force now: the routes, public domains and
18
+ * proposed configuration of the operation that owns the service's current rollout plan, never the
19
+ * declaration the source release was first deployed with.
20
+ *
21
+ * Only a release that was accepted (promoted successfully before), is still retained by the
22
+ * registry's retention policy and is still whole (every manifest and blob present) can be
23
+ * redeployed. A release accepted before the current one is rolled back to (plan mode `rollback`);
24
+ * the current release, or one accepted after it, is promoted again (plan mode `promotion`).
25
+ */
26
+
27
+ /** Where a release redeploy's image comes from: one release of the service itself. */
28
+ export interface IReleaseRedeployReference {
29
+ /** The release whose registry root the redeploy deploys again. */
30
+ sourceReleaseId: string;
31
+ }
32
+
33
+ export const releaseRedeployContract = Object.freeze({
34
+ /**
35
+ * Every release redeploy's operation id, and therefore its release tag, starts with this. A
36
+ * semantic version never does, so a redeploy tag never collides with a version tag.
37
+ */
38
+ tagPrefix: 'redeploy-',
39
+ /** Operations one history read answers when it names no limit. */
40
+ historyDefaultLimit: 20,
41
+ /** Operations one history read answers at most. */
42
+ historyMaximumLimit: 50,
43
+ });
44
+
45
+ /**
46
+ * The named refusals `startServiceReleaseDeployment` answers in its error's `code`, beside the
47
+ * shared deployment codes it reuses (`DEPLOYMENT_CAPABILITY_FORBIDDEN`, `DEPLOYMENT_IDENTITY_INVALID`,
48
+ * `DEPLOY_ON_PUSH_MUST_BE_DISABLED`, `DEPLOYMENT_ROLLOUT_FENCE_CONFLICT`,
49
+ * `DEPLOYMENT_IDEMPOTENCY_CONFLICT`, `DEPLOYMENT_PREFLIGHT_UNAVAILABLE`).
50
+ *
51
+ * - `DEPLOYMENT_REDEPLOY_INVALID`: the request is malformed.
52
+ * - `DEPLOYMENT_SERVICE_NOT_FOUND`: no such service.
53
+ * - `DEPLOYMENT_REDEPLOY_NO_CURRENT_PLAN`: the service has no current rollout plan to redeploy over.
54
+ * - `DEPLOYMENT_REDEPLOY_DECLARATION_UNAVAILABLE`: the plan is not owned by a deployment operation
55
+ * whose declaration Cloudly can state again (an adoption, or an operation that is gone).
56
+ * - `DEPLOYMENT_OPERATION_IN_FLIGHT`: another deployment operation of the service is not finished.
57
+ * - `DEPLOYMENT_RELEASE_NOT_FOUND`: no such release of this service.
58
+ * - `DEPLOYMENT_RELEASE_NOT_ACCEPTED`: the release was never promoted successfully.
59
+ * - `DEPLOYMENT_RELEASE_NOT_RETAINED`: retention no longer keeps the release.
60
+ * - `DEPLOYMENT_RELEASE_INCOMPLETE`: a manifest or blob of the release is missing from the registry.
61
+ * - `DEPLOYMENT_TARGET_SWARM_PHASE`: the service's rollout targets a cluster still in its Swarm
62
+ * phase, which a Cloudly-run deployment does not drive.
63
+ * - `DEPLOYMENT_TARGET_UNAVAILABLE`: the service's rollout target cannot be resolved.
64
+ */
65
+ export const serviceReleaseDeploymentRefusalCodes = [
66
+ 'DEPLOYMENT_REDEPLOY_INVALID',
67
+ 'DEPLOYMENT_SERVICE_NOT_FOUND',
68
+ 'DEPLOYMENT_REDEPLOY_NO_CURRENT_PLAN',
69
+ 'DEPLOYMENT_REDEPLOY_DECLARATION_UNAVAILABLE',
70
+ 'DEPLOYMENT_OPERATION_IN_FLIGHT',
71
+ 'DEPLOYMENT_RELEASE_NOT_FOUND',
72
+ 'DEPLOYMENT_RELEASE_NOT_ACCEPTED',
73
+ 'DEPLOYMENT_RELEASE_NOT_RETAINED',
74
+ 'DEPLOYMENT_RELEASE_INCOMPLETE',
75
+ 'DEPLOYMENT_TARGET_SWARM_PHASE',
76
+ 'DEPLOYMENT_TARGET_UNAVAILABLE',
77
+ ] as const;
78
+
79
+ export type TServiceReleaseDeploymentRefusalCode = typeof serviceReleaseDeploymentRefusalCodes[number];
80
+
81
+ /** What `startServiceReleaseDeployment` states besides the caller's identity. */
82
+ export interface IServiceReleaseDeploymentStartInput {
83
+ serviceId: string;
84
+ releaseId: string;
85
+ /** Binds one start; the same key with the same request answers the operation it started. */
86
+ idempotencyKey: string;
87
+ /** The service's current rollout generation: at least 1, since a redeploy needs a current plan. */
88
+ expectedRolloutGeneration: number;
89
+ /** The service's current rollout id. */
90
+ expectedCurrentRolloutId: string;
91
+ }
92
+
93
+ /** What `getServiceDeploymentOperations` states besides the caller's identity. */
94
+ export interface IServiceDeploymentOperationHistoryQuery {
95
+ serviceId: string;
96
+ /** 1 to `releaseRedeployContract.historyMaximumLimit`; `historyDefaultLimit` when omitted. */
97
+ limit?: number;
98
+ /** An operation id the previous page answered as `nextBefore`: the page starts after it. */
99
+ before?: string;
100
+ }
101
+
102
+ const redeployIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
103
+
104
+ const isRedeployIdentifier = (valueArg: unknown): valueArg is string => (
105
+ typeof valueArg === 'string' && redeployIdentifierRegex.test(valueArg)
106
+ );
107
+
108
+ const startInputKeys = [
109
+ 'serviceId',
110
+ 'releaseId',
111
+ 'idempotencyKey',
112
+ 'expectedRolloutGeneration',
113
+ 'expectedCurrentRolloutId',
114
+ ] as const;
115
+
116
+ /** Every error of a start input; empty when it is exactly one valid input. */
117
+ export const validateServiceReleaseDeploymentStartInput = (inputArg: unknown): string[] => {
118
+ const input = readDataProperties(inputArg);
119
+ if (input === null) return ['release deployment start input must be a plain object'];
120
+ const errors: string[] = [];
121
+ const keys = Object.keys(input);
122
+ if (keys.length !== startInputKeys.length
123
+ || !startInputKeys.every((keyArg) => keys.includes(keyArg))) {
124
+ errors.push(`release deployment start input must carry exactly ${startInputKeys.join(', ')}`);
125
+ }
126
+ for (const field of ['serviceId', 'releaseId', 'idempotencyKey', 'expectedCurrentRolloutId'] as const) {
127
+ if (!isRedeployIdentifier(input[field])) {
128
+ errors.push(`${field} must be a bounded canonical identifier`);
129
+ }
130
+ }
131
+ if (!Number.isSafeInteger(input.expectedRolloutGeneration)
132
+ || (input.expectedRolloutGeneration as number) < 1) {
133
+ errors.push('expectedRolloutGeneration must be the current generation, at least 1');
134
+ }
135
+ return errors;
136
+ };
137
+
138
+ /** Every error of a release redeploy reference; empty when it is exactly one valid reference. */
139
+ export const validateReleaseRedeployReference = (referenceArg: unknown): string[] => {
140
+ const reference = readDataProperties(referenceArg);
141
+ if (reference === null) return ['releaseRedeploy must be a plain object'];
142
+ const keys = Object.keys(reference);
143
+ const errors: string[] = [];
144
+ if (keys.length !== 1 || keys[0] !== 'sourceReleaseId') {
145
+ errors.push('releaseRedeploy must carry exactly sourceReleaseId');
146
+ }
147
+ if (!isRedeployIdentifier(reference.sourceReleaseId)) {
148
+ errors.push('releaseRedeploy.sourceReleaseId must be a bounded canonical identifier');
149
+ }
150
+ return errors;
151
+ };
152
+
153
+ /** Every error of a history query; empty when it is exactly one valid query. */
154
+ export const validateServiceDeploymentOperationHistoryQuery = (queryArg: unknown): string[] => {
155
+ const query = readDataProperties(queryArg);
156
+ if (query === null) return ['deployment operation history query must be a plain object'];
157
+ const errors: string[] = [];
158
+ if (Object.keys(query).some((keyArg) => !['serviceId', 'limit', 'before'].includes(keyArg))) {
159
+ errors.push('deployment operation history query carries only serviceId, limit and before');
160
+ }
161
+ if (!isRedeployIdentifier(query.serviceId)) {
162
+ errors.push('serviceId must be a bounded canonical identifier');
163
+ }
164
+ if (query.limit !== undefined
165
+ && (!Number.isSafeInteger(query.limit)
166
+ || (query.limit as number) < 1
167
+ || (query.limit as number) > releaseRedeployContract.historyMaximumLimit)) {
168
+ errors.push(`limit must be an integer from 1 to ${releaseRedeployContract.historyMaximumLimit}`);
169
+ }
170
+ if (query.before !== undefined && !isRedeployIdentifier(query.before)) {
171
+ errors.push('before must be a bounded canonical operation id');
172
+ }
173
+ return errors;
174
+ };
175
+
176
+ /** Whether a release tag is a release redeploy's own tag. */
177
+ export const isReleaseRedeployTag = (tagArg: unknown): boolean => (
178
+ typeof tagArg === 'string'
179
+ && tagArg.startsWith(releaseRedeployContract.tagPrefix)
180
+ && tagArg.length > releaseRedeployContract.tagPrefix.length
181
+ && /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/.test(tagArg)
182
+ );
@@ -206,9 +206,24 @@ export interface IImageReleaseLegacyImportEvidence extends IImageReleaseTrustedE
206
206
  sourceRootDigest: TSha256Digest;
207
207
  }
208
208
 
209
+ /**
210
+ * Cloudly's own copy of one of the service's accepted releases under a release redeploy's tag.
211
+ *
212
+ * Nothing was built or pushed: the release's root was tagged again in the same repository, so
213
+ * `sourceRootDigest` equals `registryRootDigest`, and `sourceReleaseId` names the release row the
214
+ * root belongs to. `sourceTag` is the tag the release was accepted under before.
215
+ */
216
+ export interface IImageReleaseRedeployEvidence extends IImageReleaseTrustedEvidenceBase {
217
+ source: 'cloudly-release-redeploy';
218
+ sourceReleaseId: string;
219
+ sourceTag: string;
220
+ sourceRootDigest: TSha256Digest;
221
+ }
222
+
209
223
  export type IImageReleaseTrustedEvidence =
210
224
  | IImageReleaseRegistryPushEvidence
211
- | IImageReleaseLegacyImportEvidence;
225
+ | IImageReleaseLegacyImportEvidence
226
+ | IImageReleaseRedeployEvidence;
212
227
 
213
228
  export const selectTrustedImageReleaseEvidence = (argsArg: {
214
229
  release: IImageRelease;
@@ -222,8 +237,9 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
222
237
  digest: TSha256Digest;
223
238
  /**
224
239
  * The one evidence source the operation's image may have come from: a legacy-import operation
225
- * accepts only Cloudly's import, every other operation only its actor's registry push. Omitted,
226
- * it is the registry push, the only source an operation without a legacy import has.
240
+ * accepts only Cloudly's import, a release-redeploy operation only Cloudly's redeploy copy, every
241
+ * other operation only its actor's registry push. Omitted, it is the registry push, the only
242
+ * source an operation without a legacy import or a release redeploy has.
227
243
  */
228
244
  evidenceSource?: IImageReleaseTrustedEvidence['source'];
229
245
  }): { evidence?: IImageReleaseTrustedEvidence; errors: string[] } => {
@@ -248,6 +264,13 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
248
264
  evidenceArg.sourceRootDigest,
249
265
  ]
250
266
  : []),
267
+ ...(evidenceArg.source === 'cloudly-release-redeploy'
268
+ ? [
269
+ evidenceArg.sourceReleaseId,
270
+ evidenceArg.sourceTag,
271
+ evidenceArg.sourceRootDigest,
272
+ ]
273
+ : []),
251
274
  ].join('\u0000');
252
275
  const uniqueEvidence = new Map<string, IImageReleaseTrustedEvidence>();
253
276
  const errors: string[] = [];
@@ -284,6 +307,16 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
284
307
  errors.push('trusted release evidence is ambiguous');
285
308
  }
286
309
  const evidence = evidenceMatches.length === 1 ? evidenceMatches[0] : undefined;
310
+ // A redeploy copies one root of this release row under a new tag; it never makes a new root,
311
+ // and only a row that was accepted before (promoted successfully) can be redeployed.
312
+ if (evidence?.source === 'cloudly-release-redeploy'
313
+ && (argsArg.release.data.acceptedAt === undefined
314
+ || evidence.sourceRootDigest !== argsArg.release.data.registryRootDigest
315
+ || evidence.sourceReleaseId !== argsArg.release.id
316
+ || evidence.sourceTag === evidence.tag
317
+ || !argsArg.release.data.observedTags.includes(evidence.sourceTag))) {
318
+ errors.push('release redeploy evidence must name this accepted release, its root and an earlier tag of it');
319
+ }
287
320
  if (evidence && evidence.manifestMediaType !== argsArg.release.data.manifestMediaType) {
288
321
  errors.push('trusted release and release root media types disagree');
289
322
  }
package/ts/data/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from './deploymentlegacyimport.js';
18
18
  export * from './deploymentmachineuser.js';
19
19
  export * from './deploymentoperation.js';
20
20
  export * from './deploymentpreflight.js';
21
+ export * from './deploymentreleaseredeploy.js';
21
22
  export * from './migration.js';
22
23
  export * from './dns.js';
23
24
  export * from './docker.js';
@@ -80,6 +81,8 @@ export * from './clusternodehostname.js';
80
81
  export * from './settings.js';
81
82
  export * from './sparklegacyretirement.js';
82
83
  export * from './sparknodereport.js';
84
+ export * from './nodeaction.js';
85
+ export * from './sparkfleettarget.js';
83
86
  export * from './service.js';
84
87
  export * from './servicecredential.js';
85
88
  export * from './serviceplatformownership.js';