@serve.zone/interfaces 16.1.0 → 16.3.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 +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/deploymentadoption.d.ts +44 -1
- package/dist_ts/data/deploymentadoption.js +178 -2
- package/dist_ts/data/deploymentoperation.js +16 -1
- package/dist_ts/data/deploymentpreflight.d.ts +3 -0
- package/dist_ts/data/deploymentpreflight.js +1 -1
- package/dist_ts/data/service.d.ts +27 -0
- package/dist_ts/data/service.js +100 -2
- package/dist_ts/requests/service.d.ts +19 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/deploymentadoption.ts +244 -1
- package/ts/data/deploymentoperation.ts +23 -0
- package/ts/data/deploymentpreflight.ts +3 -0
- package/ts/data/service.ts +126 -0
- package/ts/requests/service.ts +32 -0
package/dist_ts/data/service.js
CHANGED
|
@@ -1,2 +1,100 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
1
|
+
export const serviceSecretFileLimits = {
|
|
2
|
+
maximumFiles: 32,
|
|
3
|
+
maximumValueBytes: 500 * 1024,
|
|
4
|
+
maximumIdentity: 2_147_483_647,
|
|
5
|
+
};
|
|
6
|
+
export const normalizeServiceAbsolutePath = (pathArg) => {
|
|
7
|
+
if (typeof pathArg !== 'string'
|
|
8
|
+
|| !pathArg.startsWith('/')
|
|
9
|
+
|| pathArg.includes('\0')
|
|
10
|
+
|| pathArg.length > 255) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const segments = [];
|
|
14
|
+
for (const segment of pathArg.split('/')) {
|
|
15
|
+
if (!segment || segment === '.')
|
|
16
|
+
continue;
|
|
17
|
+
if (segment === '..') {
|
|
18
|
+
if (segments.length === 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
segments.pop();
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!/^[A-Za-z0-9._-]+$/.test(segment))
|
|
24
|
+
return undefined;
|
|
25
|
+
segments.push(segment);
|
|
26
|
+
}
|
|
27
|
+
return `/${segments.join('/')}`;
|
|
28
|
+
};
|
|
29
|
+
const isCanonicalSecretTargetPath = (targetPathArg) => (normalizeServiceAbsolutePath(targetPathArg) === targetPathArg
|
|
30
|
+
&& /^\/run\/secrets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(targetPathArg)
|
|
31
|
+
&& targetPathArg !== '/run/secrets/secret.json');
|
|
32
|
+
const pathsOverlap = (leftArg, rightArg) => {
|
|
33
|
+
const left = normalizeServiceAbsolutePath(leftArg);
|
|
34
|
+
const right = normalizeServiceAbsolutePath(rightArg);
|
|
35
|
+
if (!left || !right)
|
|
36
|
+
return false;
|
|
37
|
+
if (left === '/' || right === '/')
|
|
38
|
+
return true;
|
|
39
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
40
|
+
};
|
|
41
|
+
/** Pure, secret-value-free validation shared by Cloudly and Coreflow. */
|
|
42
|
+
export const validateServiceSecretFiles = (secretFilesArg, volumeMountPathsArg = []) => {
|
|
43
|
+
if (secretFilesArg === undefined)
|
|
44
|
+
return [];
|
|
45
|
+
if (!Array.isArray(secretFilesArg))
|
|
46
|
+
return ['secretFiles must be an array'];
|
|
47
|
+
if (secretFilesArg.length > serviceSecretFileLimits.maximumFiles) {
|
|
48
|
+
return [`secretFiles must contain at most ${serviceSecretFileLimits.maximumFiles} entries`];
|
|
49
|
+
}
|
|
50
|
+
const errors = [];
|
|
51
|
+
if (volumeMountPathsArg.some((mountPathArg) => (normalizeServiceAbsolutePath(mountPathArg) !== mountPathArg))) {
|
|
52
|
+
errors.push('volume mount paths must be canonical absolute paths');
|
|
53
|
+
}
|
|
54
|
+
const sourceKeys = [];
|
|
55
|
+
const targetPaths = [];
|
|
56
|
+
for (const [index, candidate] of secretFilesArg.entries()) {
|
|
57
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
58
|
+
errors.push(`secretFiles[${index}] must be an object`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const mapping = candidate;
|
|
62
|
+
if (typeof mapping.sourceKey !== 'string'
|
|
63
|
+
|| mapping.sourceKey.length > 253
|
|
64
|
+
|| !/^[A-Z_][A-Z0-9_]*$/.test(mapping.sourceKey)) {
|
|
65
|
+
errors.push(`secretFiles[${index}].sourceKey must be a canonical environment key`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
sourceKeys.push(mapping.sourceKey);
|
|
69
|
+
}
|
|
70
|
+
if (typeof mapping.targetPath !== 'string'
|
|
71
|
+
|| !isCanonicalSecretTargetPath(mapping.targetPath)) {
|
|
72
|
+
errors.push(`secretFiles[${index}].targetPath must be a unique canonical file below /run/secrets and may not be secret.json`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
targetPaths.push(mapping.targetPath);
|
|
76
|
+
if (volumeMountPathsArg.some((mountPathArg) => (typeof mountPathArg === 'string' && pathsOverlap(mapping.targetPath, mountPathArg)))) {
|
|
77
|
+
errors.push(`secretFiles[${index}].targetPath overlaps a declared volume mount`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const identityField of ['uid', 'gid']) {
|
|
81
|
+
const identity = mapping[identityField];
|
|
82
|
+
if (!Number.isSafeInteger(identity)
|
|
83
|
+
|| identity < 0
|
|
84
|
+
|| identity > serviceSecretFileLimits.maximumIdentity) {
|
|
85
|
+
errors.push(`secretFiles[${index}].${identityField} must be a bounded numeric identity`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (mapping.mode !== 0o400 && mapping.mode !== 0o440) {
|
|
89
|
+
errors.push(`secretFiles[${index}].mode must be 0400 or 0440`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (new Set(sourceKeys).size !== sourceKeys.length) {
|
|
93
|
+
errors.push('secretFiles sourceKey values must be unique');
|
|
94
|
+
}
|
|
95
|
+
if (new Set(targetPaths).size !== targetPaths.length) {
|
|
96
|
+
errors.push('secretFiles targetPath values must be unique');
|
|
97
|
+
}
|
|
98
|
+
return errors;
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFpREEsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUc7SUFDckMsWUFBWSxFQUFFLEVBQUU7SUFDaEIsaUJBQWlCLEVBQUUsR0FBRyxHQUFHLElBQUk7SUFDN0IsZUFBZSxFQUFFLGFBQWE7Q0FDdEIsQ0FBQztBQUVYLE1BQU0sQ0FBQyxNQUFNLDRCQUE0QixHQUFHLENBQUMsT0FBZSxFQUFzQixFQUFFO0lBQ2xGLElBQUksT0FBTyxPQUFPLEtBQUssUUFBUTtXQUMxQixDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDO1dBQ3hCLE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDO1dBQ3RCLE9BQU8sQ0FBQyxNQUFNLEdBQUcsR0FBRyxFQUFFLENBQUM7UUFDMUIsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUNELE1BQU0sUUFBUSxHQUFhLEVBQUUsQ0FBQztJQUM5QixLQUFLLE1BQU0sT0FBTyxJQUFJLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUN6QyxJQUFJLENBQUMsT0FBTyxJQUFJLE9BQU8sS0FBSyxHQUFHO1lBQUUsU0FBUztRQUMxQyxJQUFJLE9BQU8sS0FBSyxJQUFJLEVBQUUsQ0FBQztZQUNyQixJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssQ0FBQztnQkFBRSxPQUFPLFNBQVMsQ0FBQztZQUM1QyxRQUFRLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDZixTQUFTO1FBQ1gsQ0FBQztRQUNELElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDO1lBQUUsT0FBTyxTQUFTLENBQUM7UUFDekQsUUFBUSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN6QixDQUFDO0lBQ0QsT0FBTyxJQUFJLFFBQVEsQ0FBQyxJ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
|
|
@@ -7,7 +7,25 @@ import type { IImageRelease } from '../data/immutableimage.js';
|
|
|
7
7
|
import type { IDeploymentPreflightReport, IDeploymentPreflightRequestData } from '../data/deploymentpreflight.js';
|
|
8
8
|
import type { IDeploymentReservationInput, IServiceDeploymentOperation, IServiceDeploymentStatus } from '../data/deploymentoperation.js';
|
|
9
9
|
import type { TSha256Digest } from '../data/immutableimage.js';
|
|
10
|
-
import type { IServiceDeploymentAdoptionInput, IServiceDeploymentAdoptionPreflightInput, IServiceDeploymentAdoptionReport, IServiceDeploymentAdoptionResult } from '../data/deploymentadoption.js';
|
|
10
|
+
import type { IServiceDeploymentAdoptionInput, IServiceDeploymentAdoptionImagePromotionInput, IServiceDeploymentAdoptionImagePromotionResult, IServiceDeploymentAdoptionRolloutInput, IServiceDeploymentAdoptionRolloutResult, IServiceDeploymentAdoptionPreflightInput, IServiceDeploymentAdoptionReport, IServiceDeploymentAdoptionResult } from '../data/deploymentadoption.js';
|
|
11
|
+
export interface IRequest_Any_Cloudly_BootstrapServiceDeploymentAdoptionRollout extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_BootstrapServiceDeploymentAdoptionRollout> {
|
|
12
|
+
method: 'bootstrapServiceDeploymentAdoptionRollout';
|
|
13
|
+
request: IServiceDeploymentAdoptionRolloutInput & {
|
|
14
|
+
identity: IIdentityCredential;
|
|
15
|
+
};
|
|
16
|
+
response: {
|
|
17
|
+
result: IServiceDeploymentAdoptionRolloutResult;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface IRequest_Any_Cloudly_PromoteServiceDeploymentAdoptionImage extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_PromoteServiceDeploymentAdoptionImage> {
|
|
21
|
+
method: 'promoteServiceDeploymentAdoptionImage';
|
|
22
|
+
request: IServiceDeploymentAdoptionImagePromotionInput & {
|
|
23
|
+
identity: IIdentityCredential;
|
|
24
|
+
};
|
|
25
|
+
response: {
|
|
26
|
+
result: IServiceDeploymentAdoptionImagePromotionResult;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
11
29
|
export interface IRequest_Any_Cloudly_GetServiceDeploymentAdoptionPreflight extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IRequest_Any_Cloudly_GetServiceDeploymentAdoptionPreflight> {
|
|
12
30
|
method: 'getServiceDeploymentAdoptionPreflight';
|
|
13
31
|
request: IServiceDeploymentAdoptionPreflightInput & {
|
package/package.json
CHANGED
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -4,13 +4,19 @@ import type {
|
|
|
4
4
|
} from './deploymentpreflight.js';
|
|
5
5
|
import type { IDeployment } from './deployment.js';
|
|
6
6
|
import type {
|
|
7
|
+
IImageRolloutStatus,
|
|
7
8
|
IImmutableImageDeploymentPlan,
|
|
8
9
|
IImmutableRolloutReportEnvelope,
|
|
9
10
|
IImmutableImageTargetScope,
|
|
10
11
|
TOciManifestMediaType,
|
|
11
12
|
TSha256Digest,
|
|
12
13
|
} from './immutableimage.js';
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
normalizeImmutableReleaseTag,
|
|
16
|
+
normalizeSha256Digest,
|
|
17
|
+
validateImageRolloutStatus,
|
|
18
|
+
validateImmutableImageDeploymentPlan,
|
|
19
|
+
} from './immutableimage.js';
|
|
14
20
|
import type { IRegistryTarget } from './registry.js';
|
|
15
21
|
|
|
16
22
|
export type TServiceDeploymentAdoptionDecision = 'GO' | 'NO-GO';
|
|
@@ -58,6 +64,56 @@ extends IServiceDeploymentAdoptionPreflightInput {
|
|
|
58
64
|
expectedAdoptionDigest: TServiceDeploymentAdoptionDigest;
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
export interface IServiceDeploymentAdoptionImagePromotionInput
|
|
68
|
+
extends IServiceDeploymentAdoptionPreflightInput {
|
|
69
|
+
targetReleaseTag: string;
|
|
70
|
+
expectedSourceImageDigest?: TSha256Digest;
|
|
71
|
+
idempotencyKey: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface IServiceDeploymentAdoptionImagePromotionResult {
|
|
75
|
+
status: 'promoted' | 'already-promoted';
|
|
76
|
+
organizationId: string;
|
|
77
|
+
serviceId: string;
|
|
78
|
+
sourceTag: string;
|
|
79
|
+
releaseTag: string;
|
|
80
|
+
registryHost: string;
|
|
81
|
+
repository: string;
|
|
82
|
+
digest: TSha256Digest;
|
|
83
|
+
mediaType:
|
|
84
|
+
| 'application/vnd.oci.image.index.v1+json'
|
|
85
|
+
| 'application/vnd.docker.distribution.manifest.list.v2+json';
|
|
86
|
+
platforms: IOciPlatformDescriptorSummary[];
|
|
87
|
+
releaseId: string;
|
|
88
|
+
digestPinnedImageReference: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface IServiceDeploymentAdoptionRolloutInput
|
|
92
|
+
extends IServiceDeploymentAdoptionPreflightInput {
|
|
93
|
+
releaseTag: string;
|
|
94
|
+
expectedTargetImageDigest: TSha256Digest;
|
|
95
|
+
idempotencyKey: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface IServiceDeploymentAdoptionRolloutResult {
|
|
99
|
+
status:
|
|
100
|
+
| 'rollout-started'
|
|
101
|
+
| 'rollout-in-progress'
|
|
102
|
+
| 'rollout-succeeded'
|
|
103
|
+
| 'rollout-failed';
|
|
104
|
+
idempotentReplay: boolean;
|
|
105
|
+
operationId: string;
|
|
106
|
+
organizationId: string;
|
|
107
|
+
serviceId: string;
|
|
108
|
+
registryHost: string;
|
|
109
|
+
repository: string;
|
|
110
|
+
releaseTag: string;
|
|
111
|
+
digest: TSha256Digest;
|
|
112
|
+
releaseId: string;
|
|
113
|
+
plan: IImmutableImageDeploymentPlan;
|
|
114
|
+
rolloutStatus: IImageRolloutStatus;
|
|
115
|
+
}
|
|
116
|
+
|
|
61
117
|
interface IServiceDeploymentAdoptionReportBase {
|
|
62
118
|
schemaVersion: 1;
|
|
63
119
|
mutationPerformed: false;
|
|
@@ -194,6 +250,7 @@ export const validateServiceDeploymentAdoptionRetentionAttestation = (
|
|
|
194
250
|
};
|
|
195
251
|
|
|
196
252
|
const boundaryIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/;
|
|
253
|
+
const immutableSemverReleaseTagRegex = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[A-Za-z-][0-9A-Za-z-]*))*)?$/;
|
|
197
254
|
const indexMediaTypes = new Set<TOciManifestMediaType>([
|
|
198
255
|
'application/vnd.oci.image.index.v1+json',
|
|
199
256
|
'application/vnd.docker.distribution.manifest.list.v2+json',
|
|
@@ -229,6 +286,15 @@ const isCanonicalDigest = (valueArg: unknown): valueArg is TSha256Digest =>
|
|
|
229
286
|
const isNonNegativeInteger = (valueArg: unknown): valueArg is number =>
|
|
230
287
|
Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
|
|
231
288
|
|
|
289
|
+
export const normalizeServiceDeploymentAdoptionReleaseTag = (
|
|
290
|
+
tagArg: unknown,
|
|
291
|
+
): string | undefined => {
|
|
292
|
+
const normalizedTag = normalizeImmutableReleaseTag(tagArg);
|
|
293
|
+
return normalizedTag && immutableSemverReleaseTagRegex.test(normalizedTag)
|
|
294
|
+
? normalizedTag
|
|
295
|
+
: undefined;
|
|
296
|
+
};
|
|
297
|
+
|
|
232
298
|
export const validateServiceDeploymentAdoptionPreflightInput = (
|
|
233
299
|
inputArg: unknown,
|
|
234
300
|
): string[] => {
|
|
@@ -257,6 +323,183 @@ export const validateServiceDeploymentAdoptionInput = (inputArg: unknown): strin
|
|
|
257
323
|
return errors;
|
|
258
324
|
};
|
|
259
325
|
|
|
326
|
+
export const validateServiceDeploymentAdoptionImagePromotionInput = (
|
|
327
|
+
inputArg: unknown,
|
|
328
|
+
): string[] => {
|
|
329
|
+
const errors = validateServiceDeploymentAdoptionPreflightInput(inputArg);
|
|
330
|
+
if (!isRecord(inputArg)) return errors;
|
|
331
|
+
if (!normalizeServiceDeploymentAdoptionReleaseTag(inputArg.targetReleaseTag)) {
|
|
332
|
+
errors.push('targetReleaseTag must be a canonical OCI-safe semantic version tag');
|
|
333
|
+
}
|
|
334
|
+
if (!isBoundedIdentifier(inputArg.idempotencyKey)) {
|
|
335
|
+
errors.push('idempotencyKey must be a bounded canonical identifier');
|
|
336
|
+
}
|
|
337
|
+
if (inputArg.expectedSourceImageDigest !== undefined
|
|
338
|
+
&& !isCanonicalDigest(inputArg.expectedSourceImageDigest)) {
|
|
339
|
+
errors.push('expectedSourceImageDigest must be a canonical sha256 digest when provided');
|
|
340
|
+
}
|
|
341
|
+
return errors;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export const validateServiceDeploymentAdoptionImagePromotionResult = (
|
|
345
|
+
resultArg: unknown,
|
|
346
|
+
): string[] => {
|
|
347
|
+
if (!isRecord(resultArg)) return ['adoption image promotion result must be an object'];
|
|
348
|
+
const errors: string[] = [];
|
|
349
|
+
if (resultArg.status !== 'promoted' && resultArg.status !== 'already-promoted') {
|
|
350
|
+
errors.push('status must identify a promoted or already-promoted result');
|
|
351
|
+
}
|
|
352
|
+
for (const key of ['organizationId', 'serviceId', 'releaseId'] as const) {
|
|
353
|
+
if (!isBoundedIdentifier(resultArg[key])) {
|
|
354
|
+
errors.push(`${key} must be a bounded canonical identifier`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (typeof resultArg.sourceTag !== 'string'
|
|
358
|
+
|| resultArg.sourceTag.trim() !== resultArg.sourceTag
|
|
359
|
+
|| resultArg.sourceTag.length < 1
|
|
360
|
+
|| resultArg.sourceTag.length > 128) {
|
|
361
|
+
errors.push('sourceTag must be a bounded canonical registry tag');
|
|
362
|
+
}
|
|
363
|
+
if (!normalizeServiceDeploymentAdoptionReleaseTag(resultArg.releaseTag)) {
|
|
364
|
+
errors.push('releaseTag must be a canonical OCI-safe semantic version tag');
|
|
365
|
+
}
|
|
366
|
+
if (typeof resultArg.registryHost !== 'string'
|
|
367
|
+
|| resultArg.registryHost.trim() !== resultArg.registryHost
|
|
368
|
+
|| !resultArg.registryHost) {
|
|
369
|
+
errors.push('registryHost must be a non-empty canonical string');
|
|
370
|
+
}
|
|
371
|
+
if (typeof resultArg.repository !== 'string'
|
|
372
|
+
|| resultArg.repository.trim() !== resultArg.repository
|
|
373
|
+
|| !resultArg.repository) {
|
|
374
|
+
errors.push('repository must be a non-empty canonical string');
|
|
375
|
+
}
|
|
376
|
+
if (!isCanonicalDigest(resultArg.digest)) {
|
|
377
|
+
errors.push('digest must be a canonical sha256 digest');
|
|
378
|
+
}
|
|
379
|
+
if (!indexMediaTypes.has(resultArg.mediaType as TOciManifestMediaType)) {
|
|
380
|
+
errors.push('mediaType must identify an OCI image index');
|
|
381
|
+
}
|
|
382
|
+
if (!Array.isArray(resultArg.platforms)) {
|
|
383
|
+
errors.push('platforms must contain OCI platform evidence');
|
|
384
|
+
} else {
|
|
385
|
+
for (const architecture of ['amd64', 'arm64']) {
|
|
386
|
+
if (!resultArg.platforms.some((platformArg) => isRecord(platformArg)
|
|
387
|
+
&& platformArg.os === 'linux'
|
|
388
|
+
&& platformArg.architecture === architecture
|
|
389
|
+
&& platformArg.runnable === true
|
|
390
|
+
&& platformArg.manifestPresent === true
|
|
391
|
+
&& isCanonicalDigest(platformArg.digest))) {
|
|
392
|
+
errors.push(`platforms must contain runnable linux/${architecture} evidence`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (isCanonicalDigest(resultArg.digest)
|
|
397
|
+
&& typeof resultArg.registryHost === 'string'
|
|
398
|
+
&& typeof resultArg.repository === 'string'
|
|
399
|
+
&& resultArg.digestPinnedImageReference
|
|
400
|
+
!== `${resultArg.registryHost}/${resultArg.repository}@${resultArg.digest}`) {
|
|
401
|
+
errors.push('digestPinnedImageReference must bind the exact registry digest');
|
|
402
|
+
}
|
|
403
|
+
return errors;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export const validateServiceDeploymentAdoptionRolloutInput = (
|
|
407
|
+
inputArg: unknown,
|
|
408
|
+
): string[] => {
|
|
409
|
+
const errors = validateServiceDeploymentAdoptionPreflightInput(inputArg);
|
|
410
|
+
if (!isRecord(inputArg)) return errors;
|
|
411
|
+
if (!normalizeServiceDeploymentAdoptionReleaseTag(inputArg.releaseTag)) {
|
|
412
|
+
errors.push('releaseTag must be a canonical OCI-safe semantic version tag');
|
|
413
|
+
}
|
|
414
|
+
if (!isCanonicalDigest(inputArg.expectedTargetImageDigest)) {
|
|
415
|
+
errors.push('expectedTargetImageDigest must be a canonical sha256 digest');
|
|
416
|
+
}
|
|
417
|
+
if (!isBoundedIdentifier(inputArg.idempotencyKey)) {
|
|
418
|
+
errors.push('idempotencyKey must be a bounded canonical identifier');
|
|
419
|
+
}
|
|
420
|
+
return errors;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export const validateServiceDeploymentAdoptionRolloutResult = (
|
|
424
|
+
resultArg: unknown,
|
|
425
|
+
): string[] => {
|
|
426
|
+
if (!isRecord(resultArg)) return ['adoption rollout result must be an object'];
|
|
427
|
+
const errors: string[] = [];
|
|
428
|
+
if (![
|
|
429
|
+
'rollout-started',
|
|
430
|
+
'rollout-in-progress',
|
|
431
|
+
'rollout-succeeded',
|
|
432
|
+
'rollout-failed',
|
|
433
|
+
].includes(resultArg.status as string)) {
|
|
434
|
+
errors.push('status must identify a supported adoption rollout state');
|
|
435
|
+
}
|
|
436
|
+
if (typeof resultArg.idempotentReplay !== 'boolean') {
|
|
437
|
+
errors.push('idempotentReplay must be boolean');
|
|
438
|
+
}
|
|
439
|
+
for (const key of ['operationId', 'organizationId', 'serviceId', 'releaseId'] as const) {
|
|
440
|
+
if (!isBoundedIdentifier(resultArg[key])) errors.push(`${key} must be a bounded identifier`);
|
|
441
|
+
}
|
|
442
|
+
if (!normalizeServiceDeploymentAdoptionReleaseTag(resultArg.releaseTag)) {
|
|
443
|
+
errors.push('releaseTag must be a canonical OCI-safe semantic version tag');
|
|
444
|
+
}
|
|
445
|
+
if (!isCanonicalDigest(resultArg.digest)) {
|
|
446
|
+
errors.push('digest must be a canonical sha256 digest');
|
|
447
|
+
}
|
|
448
|
+
if (!isRecord(resultArg.plan)) {
|
|
449
|
+
errors.push('plan must be present');
|
|
450
|
+
} else {
|
|
451
|
+
errors.push(...validateImmutableImageDeploymentPlan(
|
|
452
|
+
resultArg.plan as unknown as IImmutableImageDeploymentPlan,
|
|
453
|
+
).map((errorArg) => `plan: ${errorArg}`));
|
|
454
|
+
if (resultArg.plan.operationId !== resultArg.operationId
|
|
455
|
+
|| resultArg.plan.releaseId !== resultArg.releaseId
|
|
456
|
+
|| resultArg.plan.releaseTag !== resultArg.releaseTag
|
|
457
|
+
|| resultArg.plan.requestedDigest !== resultArg.digest
|
|
458
|
+
|| resultArg.plan.registryHost !== resultArg.registryHost
|
|
459
|
+
|| resultArg.plan.repository !== resultArg.repository
|
|
460
|
+
|| resultArg.plan.mode !== 'promotion'
|
|
461
|
+
|| resultArg.plan.rolloutGeneration !== 1) {
|
|
462
|
+
errors.push('plan must bind the exact generation-one adoption rollout result');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!isRecord(resultArg.rolloutStatus)) {
|
|
466
|
+
errors.push('rolloutStatus must be present');
|
|
467
|
+
} else {
|
|
468
|
+
errors.push(...validateImageRolloutStatus(
|
|
469
|
+
resultArg.rolloutStatus as unknown as IImageRolloutStatus,
|
|
470
|
+
).map((errorArg) => `rolloutStatus: ${errorArg}`));
|
|
471
|
+
if (isRecord(resultArg.plan)
|
|
472
|
+
&& (resultArg.rolloutStatus.rolloutId !== resultArg.plan.rolloutId
|
|
473
|
+
|| resultArg.rolloutStatus.rolloutGeneration !== resultArg.plan.rolloutGeneration
|
|
474
|
+
|| resultArg.rolloutStatus.expectedDigest !== resultArg.digest)) {
|
|
475
|
+
errors.push('rolloutStatus must bind the exact adoption rollout plan');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (isRecord(resultArg.rolloutStatus)) {
|
|
479
|
+
const rolloutStatus = resultArg.rolloutStatus.status;
|
|
480
|
+
const successful = rolloutStatus === 'succeeded';
|
|
481
|
+
const failed = rolloutStatus === 'failed'
|
|
482
|
+
|| rolloutStatus === 'mismatched'
|
|
483
|
+
|| rolloutStatus === 'rolling-back'
|
|
484
|
+
|| rolloutStatus === 'rolled-back';
|
|
485
|
+
if ((resultArg.status === 'rollout-succeeded') !== successful) {
|
|
486
|
+
errors.push('rollout-succeeded must exactly match a succeeded rolloutStatus');
|
|
487
|
+
}
|
|
488
|
+
if ((resultArg.status === 'rollout-failed') !== failed) {
|
|
489
|
+
errors.push('rollout-failed must exactly match a failed or mismatched rolloutStatus');
|
|
490
|
+
}
|
|
491
|
+
if ((resultArg.status === 'rollout-started' || resultArg.status === 'rollout-in-progress')
|
|
492
|
+
&& (successful || failed)) {
|
|
493
|
+
errors.push('an active rollout result must not contain a terminal rolloutStatus');
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (typeof resultArg.registryHost !== 'string' || !resultArg.registryHost
|
|
497
|
+
|| typeof resultArg.repository !== 'string' || !resultArg.repository) {
|
|
498
|
+
errors.push('registryHost and repository must be non-empty strings');
|
|
499
|
+
}
|
|
500
|
+
return errors;
|
|
501
|
+
};
|
|
502
|
+
|
|
260
503
|
export const validateServiceDeploymentAdoptionReport = (reportArg: unknown): string[] => {
|
|
261
504
|
if (!isRecord(reportArg)) return ['adoption report must be an object'];
|
|
262
505
|
const errors: string[] = [];
|
|
@@ -2,6 +2,7 @@ import type { IDeploymentPreflightProposedConfiguration, IDeploymentPreflightRun
|
|
|
2
2
|
import type { IImageRolloutStatus, IImmutableImageDeploymentPlan, TSha256Digest } from './immutableimage.js';
|
|
3
3
|
import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
|
|
4
4
|
import type { IRegistryTarget } from './registry.js';
|
|
5
|
+
import { normalizeServiceAbsolutePath, validateServiceSecretFiles } from './service.js';
|
|
5
6
|
import type { TServiceTargetPortRef } from './serviceports.js';
|
|
6
7
|
|
|
7
8
|
export type TDeploymentOperationMode = 'existing-service' | 'greenfield';
|
|
@@ -178,6 +179,27 @@ export const validateDeploymentProposedConfiguration = (
|
|
|
178
179
|
))) {
|
|
179
180
|
errors.push('secretReferenceIds must be unique canonical identifiers');
|
|
180
181
|
}
|
|
182
|
+
const volumeMountPaths = Array.isArray(configurationArg.volumeMounts)
|
|
183
|
+
? configurationArg.volumeMounts.flatMap((mountArg) => (
|
|
184
|
+
isRecord(mountArg) && typeof mountArg.mountPath === 'string' ? [mountArg.mountPath] : []
|
|
185
|
+
))
|
|
186
|
+
: [];
|
|
187
|
+
errors.push(...validateServiceSecretFiles(configurationArg.secretFiles, volumeMountPaths));
|
|
188
|
+
if (Array.isArray(configurationArg.secretFiles)
|
|
189
|
+
&& Array.isArray(configurationArg.environmentVariableNames)) {
|
|
190
|
+
const publicEnvironmentNames = new Set(
|
|
191
|
+
configurationArg.environmentVariableNames.filter((nameArg): nameArg is string => (
|
|
192
|
+
typeof nameArg === 'string'
|
|
193
|
+
)),
|
|
194
|
+
);
|
|
195
|
+
if (configurationArg.secretFiles.some((fileArg) => (
|
|
196
|
+
isRecord(fileArg)
|
|
197
|
+
&& typeof fileArg.sourceKey === 'string'
|
|
198
|
+
&& publicEnvironmentNames.has(fileArg.sourceKey)
|
|
199
|
+
))) {
|
|
200
|
+
errors.push('secretFiles source keys must not be declared as environment variables');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
181
203
|
if (!Array.isArray(configurationArg.containerPorts)
|
|
182
204
|
|| configurationArg.containerPorts.length > 64
|
|
183
205
|
|| configurationArg.containerPorts.some((portArg) => !Number.isSafeInteger(portArg)
|
|
@@ -190,6 +212,7 @@ export const validateDeploymentProposedConfiguration = (
|
|
|
190
212
|
|| configurationArg.volumeMounts.some((mountArg) => !isRecord(mountArg)
|
|
191
213
|
|| typeof mountArg.mountPath !== 'string'
|
|
192
214
|
|| !/^\/[A-Za-z0-9._/-]{0,254}$/.test(mountArg.mountPath)
|
|
215
|
+
|| normalizeServiceAbsolutePath(mountArg.mountPath) !== mountArg.mountPath
|
|
193
216
|
|| (mountArg.storageClass !== 'corestore' && mountArg.storageClass !== 'ephemeral')
|
|
194
217
|
|| (mountArg.capability !== undefined
|
|
195
218
|
&& mountArg.capability !== 'database'
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from './immutableimage.js';
|
|
5
5
|
import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
|
|
6
6
|
import type { TServiceDeploymentCapability } from './user.js';
|
|
7
|
+
import type { IServiceSecretFile } from './service.js';
|
|
7
8
|
import type {
|
|
8
9
|
IDeploymentRouteRequest,
|
|
9
10
|
IServiceDeploymentOperation,
|
|
@@ -131,6 +132,8 @@ export interface IDeploymentPreflightProposedConfiguration {
|
|
|
131
132
|
publicDomains: string[];
|
|
132
133
|
environmentVariableNames: string[];
|
|
133
134
|
secretReferenceIds: string[];
|
|
135
|
+
/** Secret-bundle keys that must be delivered only as Docker secret files. */
|
|
136
|
+
secretFiles?: IServiceSecretFile[];
|
|
134
137
|
containerPorts: number[];
|
|
135
138
|
volumeMounts: IDeploymentPreflightProposedVolumeMount[];
|
|
136
139
|
requiredCapabilities: Array<'database' | 'objectstorage'>;
|
package/ts/data/service.ts
CHANGED
|
@@ -29,6 +29,130 @@ export interface IServiceVolume {
|
|
|
29
29
|
options?: Record<string, string>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A single secret-bundle key delivered as a file instead of an environment
|
|
34
|
+
* variable. Targets are intentionally restricted to Docker's secret tmpfs so
|
|
35
|
+
* a deployment declaration cannot overwrite application or system files.
|
|
36
|
+
*/
|
|
37
|
+
export interface IServiceSecretFile {
|
|
38
|
+
/** Key in the service's merged secret bundles. */
|
|
39
|
+
sourceKey: string;
|
|
40
|
+
/** Absolute file path below /run/secrets/. */
|
|
41
|
+
targetPath: string;
|
|
42
|
+
/** Numeric container user identity. */
|
|
43
|
+
uid: number;
|
|
44
|
+
/** Numeric container group identity. */
|
|
45
|
+
gid: number;
|
|
46
|
+
/** Read-only POSIX mode. Supported values are 0400 and 0440. */
|
|
47
|
+
mode: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const serviceSecretFileLimits = {
|
|
51
|
+
maximumFiles: 32,
|
|
52
|
+
maximumValueBytes: 500 * 1024,
|
|
53
|
+
maximumIdentity: 2_147_483_647,
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
export const normalizeServiceAbsolutePath = (pathArg: string): string | undefined => {
|
|
57
|
+
if (typeof pathArg !== 'string'
|
|
58
|
+
|| !pathArg.startsWith('/')
|
|
59
|
+
|| pathArg.includes('\0')
|
|
60
|
+
|| pathArg.length > 255) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const segments: string[] = [];
|
|
64
|
+
for (const segment of pathArg.split('/')) {
|
|
65
|
+
if (!segment || segment === '.') continue;
|
|
66
|
+
if (segment === '..') {
|
|
67
|
+
if (segments.length === 0) return undefined;
|
|
68
|
+
segments.pop();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!/^[A-Za-z0-9._-]+$/.test(segment)) return undefined;
|
|
72
|
+
segments.push(segment);
|
|
73
|
+
}
|
|
74
|
+
return `/${segments.join('/')}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isCanonicalSecretTargetPath = (targetPathArg: string): boolean => (
|
|
78
|
+
normalizeServiceAbsolutePath(targetPathArg) === targetPathArg
|
|
79
|
+
&& /^\/run\/secrets\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(targetPathArg)
|
|
80
|
+
&& targetPathArg !== '/run/secrets/secret.json'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const pathsOverlap = (leftArg: string, rightArg: string): boolean => {
|
|
84
|
+
const left = normalizeServiceAbsolutePath(leftArg);
|
|
85
|
+
const right = normalizeServiceAbsolutePath(rightArg);
|
|
86
|
+
if (!left || !right) return false;
|
|
87
|
+
if (left === '/' || right === '/') return true;
|
|
88
|
+
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Pure, secret-value-free validation shared by Cloudly and Coreflow. */
|
|
92
|
+
export const validateServiceSecretFiles = (
|
|
93
|
+
secretFilesArg: unknown,
|
|
94
|
+
volumeMountPathsArg: string[] = [],
|
|
95
|
+
): string[] => {
|
|
96
|
+
if (secretFilesArg === undefined) return [];
|
|
97
|
+
if (!Array.isArray(secretFilesArg)) return ['secretFiles must be an array'];
|
|
98
|
+
if (secretFilesArg.length > serviceSecretFileLimits.maximumFiles) {
|
|
99
|
+
return [`secretFiles must contain at most ${serviceSecretFileLimits.maximumFiles} entries`];
|
|
100
|
+
}
|
|
101
|
+
const errors: string[] = [];
|
|
102
|
+
if (volumeMountPathsArg.some((mountPathArg) => (
|
|
103
|
+
normalizeServiceAbsolutePath(mountPathArg) !== mountPathArg
|
|
104
|
+
))) {
|
|
105
|
+
errors.push('volume mount paths must be canonical absolute paths');
|
|
106
|
+
}
|
|
107
|
+
const sourceKeys: string[] = [];
|
|
108
|
+
const targetPaths: string[] = [];
|
|
109
|
+
for (const [index, candidate] of secretFilesArg.entries()) {
|
|
110
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
111
|
+
errors.push(`secretFiles[${index}] must be an object`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const mapping = candidate as Partial<IServiceSecretFile>;
|
|
115
|
+
if (typeof mapping.sourceKey !== 'string'
|
|
116
|
+
|| mapping.sourceKey.length > 253
|
|
117
|
+
|| !/^[A-Z_][A-Z0-9_]*$/.test(mapping.sourceKey)) {
|
|
118
|
+
errors.push(`secretFiles[${index}].sourceKey must be a canonical environment key`);
|
|
119
|
+
} else {
|
|
120
|
+
sourceKeys.push(mapping.sourceKey);
|
|
121
|
+
}
|
|
122
|
+
if (typeof mapping.targetPath !== 'string'
|
|
123
|
+
|| !isCanonicalSecretTargetPath(mapping.targetPath)) {
|
|
124
|
+
errors.push(
|
|
125
|
+
`secretFiles[${index}].targetPath must be a unique canonical file below /run/secrets and may not be secret.json`,
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
targetPaths.push(mapping.targetPath);
|
|
129
|
+
if (volumeMountPathsArg.some((mountPathArg) => (
|
|
130
|
+
typeof mountPathArg === 'string' && pathsOverlap(mapping.targetPath as string, mountPathArg)
|
|
131
|
+
))) {
|
|
132
|
+
errors.push(`secretFiles[${index}].targetPath overlaps a declared volume mount`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const identityField of ['uid', 'gid'] as const) {
|
|
136
|
+
const identity = mapping[identityField];
|
|
137
|
+
if (!Number.isSafeInteger(identity)
|
|
138
|
+
|| (identity as number) < 0
|
|
139
|
+
|| (identity as number) > serviceSecretFileLimits.maximumIdentity) {
|
|
140
|
+
errors.push(`secretFiles[${index}].${identityField} must be a bounded numeric identity`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (mapping.mode !== 0o400 && mapping.mode !== 0o440) {
|
|
144
|
+
errors.push(`secretFiles[${index}].mode must be 0400 or 0440`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (new Set(sourceKeys).size !== sourceKeys.length) {
|
|
148
|
+
errors.push('secretFiles sourceKey values must be unique');
|
|
149
|
+
}
|
|
150
|
+
if (new Set(targetPaths).size !== targetPaths.length) {
|
|
151
|
+
errors.push('secretFiles targetPath values must be unique');
|
|
152
|
+
}
|
|
153
|
+
return errors;
|
|
154
|
+
};
|
|
155
|
+
|
|
32
156
|
/**
|
|
33
157
|
* Where a service is allowed to run.
|
|
34
158
|
* - absent or mode 'replicated': the service runs on every node (legacy
|
|
@@ -105,6 +229,8 @@ export interface IService {
|
|
|
105
229
|
* and thus live past the service lifecycle
|
|
106
230
|
*/
|
|
107
231
|
additionalSecretBundleIds?: string[];
|
|
232
|
+
/** Secret keys mounted as files and excluded from Env and secret.json. */
|
|
233
|
+
secretFiles?: IServiceSecretFile[];
|
|
108
234
|
|
|
109
235
|
/**
|
|
110
236
|
* Service category determines deployment behavior
|