@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.
- package/changelog.md +20 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusternode.d.ts +15 -2
- package/dist_ts/data/clusternode.js +1 -1
- package/dist_ts/data/deploymentoperation.d.ts +17 -3
- package/dist_ts/data/deploymentoperation.js +4 -1
- package/dist_ts/data/deploymentreleaseredeploy.d.ts +88 -0
- package/dist_ts/data/deploymentreleaseredeploy.js +120 -0
- package/dist_ts/data/immutableimage.d.ts +17 -3
- package/dist_ts/data/immutableimage.js +18 -1
- package/dist_ts/data/index.d.ts +3 -0
- package/dist_ts/data/index.js +4 -1
- package/dist_ts/data/nodeaction.d.ts +439 -0
- package/dist_ts/data/nodeaction.js +434 -0
- package/dist_ts/data/sparkfleettarget.d.ts +80 -0
- package/dist_ts/data/sparkfleettarget.js +71 -0
- package/dist_ts/data/sparknodereport.d.ts +19 -0
- package/dist_ts/data/sparknodereport.js +21 -4
- package/dist_ts/data/user.d.ts +7 -1
- package/dist_ts/data/user.js +6 -1
- package/dist_ts/requests/deployment.d.ts +5 -0
- package/dist_ts/requests/node.d.ts +100 -0
- package/dist_ts/requests/service.d.ts +42 -0
- package/package.json +2 -2
- package/readme.md +56 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +17 -2
- package/ts/data/deploymentoperation.ts +21 -2
- package/ts/data/deploymentreleaseredeploy.ts +182 -0
- package/ts/data/immutableimage.ts +36 -3
- package/ts/data/index.ts +3 -0
- package/ts/data/nodeaction.ts +755 -0
- package/ts/data/sparkfleettarget.ts +138 -0
- package/ts/data/sparknodereport.ts +42 -3
- package/ts/data/user.ts +12 -1
- package/ts/requests/deployment.ts +5 -0
- package/ts/requests/node.ts +118 -0
- 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,
|
|
226
|
-
* it is the registry push, the only
|
|
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';
|