@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.
- package/changelog.md +71 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusternode.d.ts +16 -2
- package/dist_ts/data/clusternode.js +11 -1
- package/dist_ts/data/deploymentlegacyimport.d.ts +92 -0
- package/dist_ts/data/deploymentlegacyimport.js +237 -0
- package/dist_ts/data/deploymentoperation.d.ts +63 -38
- package/dist_ts/data/deploymentoperation.js +12 -2
- package/dist_ts/data/deploymentpreflight.d.ts +42 -10
- package/dist_ts/data/deploymentpreflight.js +47 -10
- package/dist_ts/data/immutableimage.d.ts +26 -2
- package/dist_ts/data/immutableimage.js +12 -2
- package/dist_ts/data/index.d.ts +5 -0
- package/dist_ts/data/index.js +6 -1
- package/dist_ts/data/networknode.d.ts +16 -6
- package/dist_ts/data/networknode.js +1 -1
- package/dist_ts/data/runtimeassignmentexec.d.ts +110 -0
- package/dist_ts/data/runtimeassignmentexec.js +164 -0
- package/dist_ts/data/runtimeassignmentlogs.d.ts +121 -0
- package/dist_ts/data/runtimeassignmentlogs.js +139 -0
- package/dist_ts/data/runtimeassignmentstats.d.ts +60 -0
- package/dist_ts/data/runtimeassignmentstats.js +93 -0
- package/dist_ts/data/runtimelocalvolumearchive.d.ts +103 -0
- package/dist_ts/data/runtimelocalvolumearchive.js +111 -0
- package/dist_ts/data/user.d.ts +6 -1
- package/dist_ts/data/user.js +6 -1
- package/dist_ts/requests/runtimesession.d.ts +81 -0
- package/dist_ts/requests/service.d.ts +51 -0
- package/package.json +2 -2
- package/readme.md +109 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusternode.ts +17 -2
- package/ts/data/deploymentlegacyimport.ts +371 -0
- package/ts/data/deploymentoperation.ts +83 -39
- package/ts/data/deploymentpreflight.ts +86 -18
- package/ts/data/immutableimage.ts +42 -3
- package/ts/data/index.ts +5 -0
- package/ts/data/networknode.ts +12 -5
- package/ts/data/runtimeassignmentexec.ts +271 -0
- package/ts/data/runtimeassignmentlogs.ts +249 -0
- package/ts/data/runtimeassignmentstats.ts +149 -0
- package/ts/data/runtimelocalvolumearchive.ts +207 -0
- package/ts/data/user.ts +10 -0
- package/ts/requests/runtimesession.ts +102 -0
- package/ts/requests/service.ts +66 -0
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
} from './immutableimage.js';
|
|
6
6
|
import { normalizeImmutableReleaseTag, normalizeSha256Digest } from './immutableimage.js';
|
|
7
7
|
import type { TServiceDeploymentCapability } from './user.js';
|
|
8
|
+
import type { ILegacyImageImportReference } from './deploymentlegacyimport.js';
|
|
9
|
+
import { validateLegacyImageImportReference } from './deploymentlegacyimport.js';
|
|
8
10
|
import { validateProtocolOfferOn, type IProtocolOffer } from '../protocol.js';
|
|
9
11
|
import type {
|
|
10
12
|
IDeploymentRouteRequest,
|
|
@@ -84,6 +86,19 @@ export const deploymentPreflightBlockerCodes = [
|
|
|
84
86
|
'RELAY_NAME_RESERVED',
|
|
85
87
|
'ROUTE_VERIFICATION_FAILED',
|
|
86
88
|
'TLS_VERIFICATION_FAILED',
|
|
89
|
+
/**
|
|
90
|
+
* A legacy import names no attestation Cloudly holds for that legacy service in the requested
|
|
91
|
+
* organization. Evidence is captured only while the legacy runtime runs, so there is no later
|
|
92
|
+
* substitute for a missing one.
|
|
93
|
+
*/
|
|
94
|
+
'LEGACY_IMPORT_ATTESTATION_MISSING',
|
|
95
|
+
/**
|
|
96
|
+
* The attested legacy image is no longer what the request or the registry states: the asserted
|
|
97
|
+
* root digest differs, the legacy tag moved, or a manifest the attestation names is gone.
|
|
98
|
+
*/
|
|
99
|
+
'LEGACY_IMPORT_SOURCE_MISMATCH',
|
|
100
|
+
/** The declared target platforms are not exactly the platforms the attested image carries. */
|
|
101
|
+
'LEGACY_IMPORT_PLATFORM_MISMATCH',
|
|
87
102
|
] as const;
|
|
88
103
|
|
|
89
104
|
export type TDeploymentPreflightBlockerCode = typeof deploymentPreflightBlockerCodes[number];
|
|
@@ -93,7 +108,9 @@ export interface IDeploymentPreflightBlocker {
|
|
|
93
108
|
message: string;
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
export type TDeploymentPreflightWarningCode =
|
|
111
|
+
export type TDeploymentPreflightWarningCode =
|
|
112
|
+
| 'RECOVERY_REDEPLOY_OVER_FAILED_RUNTIME'
|
|
113
|
+
| 'PLATFORM_BINDING_READINESS_DEFERRED';
|
|
97
114
|
|
|
98
115
|
/**
|
|
99
116
|
* Auditable non-blocking preflight finding. Warnings never veto a decision.
|
|
@@ -102,7 +119,16 @@ export type TDeploymentPreflightWarningCode = 'RECOVERY_REDEPLOY_OVER_FAILED_RUN
|
|
|
102
119
|
* RUNTIME_ATTESTATION_MISMATCH blockers because the current runtime provides
|
|
103
120
|
* no consistent attested digest and the service's most recent deployment
|
|
104
121
|
* operation ended 'failed' or 'cleaned' (a recovery redeploy over a known-bad
|
|
105
|
-
* runtime).
|
|
122
|
+
* runtime).
|
|
123
|
+
* `PLATFORM_BINDING_READINESS_DEFERRED` documents that the promote preflight
|
|
124
|
+
* of a held greenfield service that was never promoted accepted a Corestore
|
|
125
|
+
* `database` or `objectstorage` binding that is enabled but not yet `ready`.
|
|
126
|
+
* A held service receives no credential material, so its binding can become
|
|
127
|
+
* ready only after promotion releases the hold; for a binding that adopts a
|
|
128
|
+
* legacy resource the Corestore inventory proved the resource authorized for
|
|
129
|
+
* exactly that binding. The route preflight still requires the binding
|
|
130
|
+
* `ready`, so no traffic reaches the service before its binding is.
|
|
131
|
+
* No other blocker is ever relaxed.
|
|
106
132
|
*/
|
|
107
133
|
export interface IDeploymentPreflightWarning {
|
|
108
134
|
code: TDeploymentPreflightWarningCode;
|
|
@@ -193,21 +219,39 @@ export interface IDeploymentReleaseEvidence {
|
|
|
193
219
|
attestations: IDeploymentReleaseEvidenceReference[];
|
|
194
220
|
}
|
|
195
221
|
|
|
196
|
-
|
|
222
|
+
/**
|
|
223
|
+
* A preflight states where the deployment's image comes from, exactly as its reservation will:
|
|
224
|
+
* a clean source revision with its release evidence, or — for a greenfield successor only — a
|
|
225
|
+
* legacy import naming an attested legacy image, which has no revision, worktree or build
|
|
226
|
+
* evidence to state.
|
|
227
|
+
*/
|
|
228
|
+
export type IDeploymentPreflightRequestData = IDeploymentPreflightRequestDataBase & (
|
|
229
|
+
| {
|
|
230
|
+
sourceRevision: string;
|
|
231
|
+
sourceDirty: boolean;
|
|
232
|
+
releaseEvidence?: IDeploymentReleaseEvidence;
|
|
233
|
+
legacyImport?: never;
|
|
234
|
+
}
|
|
235
|
+
| {
|
|
236
|
+
legacyImport: ILegacyImageImportReference;
|
|
237
|
+
sourceRevision?: never;
|
|
238
|
+
sourceDirty?: never;
|
|
239
|
+
releaseEvidence?: never;
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
interface IDeploymentPreflightRequestDataBase {
|
|
197
244
|
requestId: string;
|
|
198
245
|
serviceId: string;
|
|
199
246
|
organizationId?: string;
|
|
200
247
|
mode?: TDeploymentOperationMode;
|
|
201
248
|
phase?: TDeploymentOperationPhase;
|
|
202
249
|
operationId?: string;
|
|
203
|
-
sourceRevision: string;
|
|
204
|
-
sourceDirty: boolean;
|
|
205
250
|
version: string;
|
|
206
251
|
intentDigest: string;
|
|
207
252
|
proposedConfiguration: IDeploymentPreflightProposedConfiguration;
|
|
208
253
|
ociIndexDigest?: string;
|
|
209
254
|
releaseTag?: string;
|
|
210
|
-
releaseEvidence?: IDeploymentReleaseEvidence;
|
|
211
255
|
routes?: IDeploymentRouteRequest[];
|
|
212
256
|
/** Legacy v14 discriminator. Equivalent to mode: 'greenfield'. */
|
|
213
257
|
greenfield?: true;
|
|
@@ -249,13 +293,15 @@ export interface IDeploymentPreflightReport {
|
|
|
249
293
|
mode?: TDeploymentOperationMode;
|
|
250
294
|
phase?: TDeploymentOperationPhase;
|
|
251
295
|
operation?: IServiceDeploymentOperation;
|
|
296
|
+
/** The request's image origin as judged: a source revision, or a legacy import. */
|
|
252
297
|
source: {
|
|
253
|
-
revision: string;
|
|
254
|
-
dirty: boolean;
|
|
255
298
|
version: string;
|
|
256
299
|
intentDigest: TSha256Digest;
|
|
257
300
|
configurationDigest: TSha256Digest;
|
|
258
|
-
}
|
|
301
|
+
} & (
|
|
302
|
+
| { revision: string; dirty: boolean; legacyImport?: never }
|
|
303
|
+
| { legacyImport: ILegacyImageImportReference; revision?: never; dirty?: never }
|
|
304
|
+
);
|
|
259
305
|
currentImage: {
|
|
260
306
|
tag?: string;
|
|
261
307
|
digest?: TSha256Digest;
|
|
@@ -339,16 +385,35 @@ export const validateDeploymentPreflightRequest = (
|
|
|
339
385
|
if (typeof requestArg.serviceId !== 'string' || !requestArg.serviceId.trim() || requestArg.serviceId.length > 200) {
|
|
340
386
|
blockers.push({ code: 'INVALID_REQUEST', message: 'A bounded service identifier is required.' });
|
|
341
387
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
388
|
+
const legacyImport = requestArg.legacyImport !== undefined;
|
|
389
|
+
if (legacyImport) {
|
|
390
|
+
const referenceErrors = validateLegacyImageImportReference(requestArg.legacyImport);
|
|
391
|
+
if (referenceErrors.length > 0) {
|
|
392
|
+
blockers.push({
|
|
393
|
+
code: 'INVALID_REQUEST',
|
|
394
|
+
message: `The legacy import reference is malformed: ${referenceErrors.join('; ')}.`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (requestArg.sourceRevision !== undefined
|
|
398
|
+
|| requestArg.sourceDirty !== undefined
|
|
399
|
+
|| requestArg.releaseEvidence !== undefined) {
|
|
400
|
+
blockers.push({
|
|
401
|
+
code: 'INVALID_REQUEST',
|
|
402
|
+
message: 'A legacy import states no source revision, worktree state or release evidence.',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
if (typeof requestArg.sourceRevision !== 'string' || !fullGitRevisionRegex.test(requestArg.sourceRevision)) {
|
|
407
|
+
blockers.push({ code: 'SOURCE_REVISION_INVALID', message: 'Source revision must be a full lower-case Git commit SHA.' });
|
|
408
|
+
if (typeof requestArg.sourceRevision !== 'string') {
|
|
409
|
+
blockers.push({ code: 'INVALID_REQUEST', message: 'Source revision is malformed.' });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (typeof requestArg.sourceDirty !== 'boolean') {
|
|
413
|
+
blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' });
|
|
414
|
+
} else if (requestArg.sourceDirty) {
|
|
415
|
+
blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' });
|
|
346
416
|
}
|
|
347
|
-
}
|
|
348
|
-
if (typeof requestArg.sourceDirty !== 'boolean') {
|
|
349
|
-
blockers.push({ code: 'INVALID_REQUEST', message: 'Source worktree state must be reported.' });
|
|
350
|
-
} else if (requestArg.sourceDirty) {
|
|
351
|
-
blockers.push({ code: 'SOURCE_WORKTREE_DIRTY', message: 'The source worktree contains uncommitted changes.' });
|
|
352
417
|
}
|
|
353
418
|
if (typeof requestArg.version !== 'string'
|
|
354
419
|
|| requestArg.version.length > 100
|
|
@@ -397,6 +462,9 @@ export const validateDeploymentPreflightRequest = (
|
|
|
397
462
|
&& (requestArg.greenfield !== true || mode !== 'greenfield')) {
|
|
398
463
|
blockers.push({ code: 'INVALID_REQUEST', message: 'Legacy greenfield mode is contradictory.' });
|
|
399
464
|
}
|
|
465
|
+
if (legacyImport && mode !== 'greenfield') {
|
|
466
|
+
blockers.push({ code: 'INVALID_REQUEST', message: 'A legacy import reserves a greenfield successor.' });
|
|
467
|
+
}
|
|
400
468
|
if (requestArg.phase !== undefined
|
|
401
469
|
&& requestArg.phase !== 'reserve'
|
|
402
470
|
&& requestArg.phase !== 'promote'
|
|
@@ -173,8 +173,7 @@ export interface IImageRelease {
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
source: 'cloudly-registry';
|
|
176
|
+
interface IImageReleaseTrustedEvidenceBase {
|
|
178
177
|
/** Deterministic ID for an authenticated registry acceptance event/retry. */
|
|
179
178
|
evidenceId: string;
|
|
180
179
|
operationId: string;
|
|
@@ -187,6 +186,30 @@ export interface IImageReleaseTrustedEvidence {
|
|
|
187
186
|
recordedAt: number;
|
|
188
187
|
}
|
|
189
188
|
|
|
189
|
+
/** The deployment machine user's own authenticated registry push to its operation's tag. */
|
|
190
|
+
export interface IImageReleaseRegistryPushEvidence extends IImageReleaseTrustedEvidenceBase {
|
|
191
|
+
source: 'cloudly-registry';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Cloudly's own import of an attested legacy image into a legacy-import operation's tag.
|
|
196
|
+
*
|
|
197
|
+
* Nothing was built or pushed: the source fields name the legacy registry root the attestation
|
|
198
|
+
* proved ran, and `registryRootDigest` is that root or the index Cloudly wrapped around it.
|
|
199
|
+
*/
|
|
200
|
+
export interface IImageReleaseLegacyImportEvidence extends IImageReleaseTrustedEvidenceBase {
|
|
201
|
+
source: 'cloudly-legacy-import';
|
|
202
|
+
legacyServiceId: string;
|
|
203
|
+
attestationId: string;
|
|
204
|
+
sourceRepository: string;
|
|
205
|
+
sourceTag: string;
|
|
206
|
+
sourceRootDigest: TSha256Digest;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type IImageReleaseTrustedEvidence =
|
|
210
|
+
| IImageReleaseRegistryPushEvidence
|
|
211
|
+
| IImageReleaseLegacyImportEvidence;
|
|
212
|
+
|
|
190
213
|
export const selectTrustedImageReleaseEvidence = (argsArg: {
|
|
191
214
|
release: IImageRelease;
|
|
192
215
|
operationId: string;
|
|
@@ -197,7 +220,14 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
|
|
|
197
220
|
repository: string;
|
|
198
221
|
tag: string;
|
|
199
222
|
digest: TSha256Digest;
|
|
223
|
+
/**
|
|
224
|
+
* 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.
|
|
227
|
+
*/
|
|
228
|
+
evidenceSource?: IImageReleaseTrustedEvidence['source'];
|
|
200
229
|
}): { evidence?: IImageReleaseTrustedEvidence; errors: string[] } => {
|
|
230
|
+
const evidenceSource = argsArg.evidenceSource ?? 'cloudly-registry';
|
|
201
231
|
const evidenceFingerprint = (evidenceArg: IImageReleaseTrustedEvidence) => [
|
|
202
232
|
evidenceArg.source,
|
|
203
233
|
evidenceArg.evidenceId,
|
|
@@ -209,6 +239,15 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
|
|
|
209
239
|
evidenceArg.registryRootDigest,
|
|
210
240
|
evidenceArg.manifestMediaType,
|
|
211
241
|
evidenceArg.recordedAt,
|
|
242
|
+
...(evidenceArg.source === 'cloudly-legacy-import'
|
|
243
|
+
? [
|
|
244
|
+
evidenceArg.legacyServiceId,
|
|
245
|
+
evidenceArg.attestationId,
|
|
246
|
+
evidenceArg.sourceRepository,
|
|
247
|
+
evidenceArg.sourceTag,
|
|
248
|
+
evidenceArg.sourceRootDigest,
|
|
249
|
+
]
|
|
250
|
+
: []),
|
|
212
251
|
].join('\u0000');
|
|
213
252
|
const uniqueEvidence = new Map<string, IImageReleaseTrustedEvidence>();
|
|
214
253
|
const errors: string[] = [];
|
|
@@ -221,7 +260,7 @@ export const selectTrustedImageReleaseEvidence = (argsArg: {
|
|
|
221
260
|
}
|
|
222
261
|
}
|
|
223
262
|
const evidenceMatches = [...uniqueEvidence.values()].filter((evidenceArg) => (
|
|
224
|
-
evidenceArg.source ===
|
|
263
|
+
evidenceArg.source === evidenceSource
|
|
225
264
|
&& evidenceArg.operationId === argsArg.operationId
|
|
226
265
|
&& evidenceArg.actorUserId === argsArg.actorUserId
|
|
227
266
|
&& evidenceArg.registryHost === argsArg.registryHost
|
package/ts/data/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './coremail.js';
|
|
|
12
12
|
export * from './coremail.runtime.js';
|
|
13
13
|
export * from './deployment.js';
|
|
14
14
|
export * from './deploymentadoption.js';
|
|
15
|
+
export * from './deploymentlegacyimport.js';
|
|
15
16
|
export * from './deploymentmachineuser.js';
|
|
16
17
|
export * from './deploymentoperation.js';
|
|
17
18
|
export * from './deploymentpreflight.js';
|
|
@@ -52,9 +53,13 @@ export * from './privatenetwork.js';
|
|
|
52
53
|
export * from './registry.js';
|
|
53
54
|
export * from './releaseversion.js';
|
|
54
55
|
export * from './runtimeassignment.js';
|
|
56
|
+
export * from './runtimeassignmentexec.js';
|
|
57
|
+
export * from './runtimeassignmentlogs.js';
|
|
58
|
+
export * from './runtimeassignmentstats.js';
|
|
55
59
|
export * from './runtimeconfig.js';
|
|
56
60
|
export * from './runtimestorageclaim.js';
|
|
57
61
|
export * from './runtimelocalstorageclaim.js';
|
|
62
|
+
export * from './runtimelocalvolumearchive.js';
|
|
58
63
|
export * from './runtimesession.js';
|
|
59
64
|
export * from './runtimeterminalreceipt.js';
|
|
60
65
|
export * from './secret.js';
|
package/ts/data/networknode.ts
CHANGED
|
@@ -4,13 +4,20 @@ import { captureNetwork, networkCounter, networkNodeId, networkRecord, networkSa
|
|
|
4
4
|
type TEgressOwner = IRuntimeNetworkProtectedAuthority['egressAuthorities'][number];
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* One unmet condition of a node's runtime network readiness
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* the egress owners whose current
|
|
7
|
+
* One unmet condition of a node's runtime network readiness. The first is liveness: the node is
|
|
8
|
+
* observed running. The rest follow the order the node producer meets them: the node joins the
|
|
9
|
+
* protected authority's egress, every egress owner reports current protection, a handoff is
|
|
10
|
+
* reserved, the router selection carries it, the node's acknowledged projection carries it, and the
|
|
11
|
+
* node is a managed VPN member. `protection-receipts-pending` names the egress owners whose current
|
|
12
|
+
* receipts are still missing.
|
|
12
13
|
*/
|
|
13
14
|
export type TRuntimeNetworkNodeReadinessGap =
|
|
15
|
+
/**
|
|
16
|
+
* No accepted report newer than `clusterNodeObservationFreshnessMs` shows the node running.
|
|
17
|
+
* `lastObservedAt` is when Cloudly accepted the node's last report, or `null` when the node's
|
|
18
|
+
* status is not `online`, so no report shows it running.
|
|
19
|
+
*/
|
|
20
|
+
| { condition: 'node-observation-stale'; lastObservedAt: number | null }
|
|
14
21
|
| { condition: 'egress-pending' | 'handoff-pending' | 'selection-pending' | 'projection-pending' | 'vpn-member-pending' }
|
|
15
22
|
| { condition: 'protection-receipts-pending'; owners: [TEgressOwner, ...TEgressOwner[]] };
|
|
16
23
|
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { IRuntimeAssignment, IRuntimeAssignmentReference } from './runtimeassignment.js';
|
|
2
|
+
import { getRuntimeAssignmentReference, snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
3
|
+
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
4
|
+
import { snapshotRuntimeSessionBinding } from './runtimesession.js';
|
|
5
|
+
import {
|
|
6
|
+
captureNetwork, networkCounter, networkDigest, networkEqual, networkId, networkRecord, requireNetwork,
|
|
7
|
+
} from '../private/networkcontract.js';
|
|
8
|
+
|
|
9
|
+
/** A terminal's size in character cells. */
|
|
10
|
+
export interface IRuntimeTerminalSize {
|
|
11
|
+
columns: number;
|
|
12
|
+
rows: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A controller's request to run one command inside one running attempt's container.
|
|
17
|
+
*
|
|
18
|
+
* `command` is the argument vector the runtime executes directly: no shell parses it unless its
|
|
19
|
+
* first element names one. With `tty` the process runs on a terminal of `terminalSize`, which is
|
|
20
|
+
* stated exactly then; its output arrives on one stream. Without it stdout and stderr arrive on
|
|
21
|
+
* separate streams. Standard input is the optional stream the TypedRequest carries beside this
|
|
22
|
+
* body; its end is the process's end of input.
|
|
23
|
+
*/
|
|
24
|
+
export interface IOpenRuntimeAssignmentExecRequest {
|
|
25
|
+
session: IRuntimeSessionBinding;
|
|
26
|
+
/** The exact current revision of a `run` assignment on this node. */
|
|
27
|
+
assignment: IRuntimeAssignmentReference;
|
|
28
|
+
command: string[];
|
|
29
|
+
tty: boolean;
|
|
30
|
+
terminalSize: IRuntimeTerminalSize | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The node's answer once the process runs. `execId` names the session in later requests;
|
|
35
|
+
* `idleTimeoutMs` is how long the session may carry no byte in either direction before the node
|
|
36
|
+
* ends it.
|
|
37
|
+
*/
|
|
38
|
+
export interface IOpenRuntimeAssignmentExecResponse {
|
|
39
|
+
execId: string;
|
|
40
|
+
assignment: IRuntimeAssignmentReference;
|
|
41
|
+
tty: boolean;
|
|
42
|
+
idleTimeoutMs: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A new terminal size for a `tty` session. */
|
|
46
|
+
export interface IResizeRuntimeAssignmentExecRequest {
|
|
47
|
+
session: IRuntimeSessionBinding;
|
|
48
|
+
execId: string;
|
|
49
|
+
terminalSize: IRuntimeTerminalSize;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IResizeRuntimeAssignmentExecResponse {
|
|
53
|
+
execId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Ends a session and releases its slot, whether or not its process has exited. */
|
|
57
|
+
export interface ICloseRuntimeAssignmentExecRequest {
|
|
58
|
+
session: IRuntimeSessionBinding;
|
|
59
|
+
execId: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Why the node ended a process before it exited: `closed` by this request, `idle-timeout` after
|
|
64
|
+
* `idleTimeoutMs` without a byte, `container-stopped` because its container stopped.
|
|
65
|
+
*/
|
|
66
|
+
export type TRuntimeAssignmentExecTermination = 'closed' | 'idle-timeout' | 'container-stopped';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How the session ended. `exited` carries the process's `exitCode`, 128 plus the signal number
|
|
70
|
+
* for a process a signal ended; `terminated` states why the node ended it and carries no code.
|
|
71
|
+
*/
|
|
72
|
+
export interface ICloseRuntimeAssignmentExecResponse {
|
|
73
|
+
execId: string;
|
|
74
|
+
outcome: 'exited' | 'terminated';
|
|
75
|
+
exitCode: number | null;
|
|
76
|
+
termination: TRuntimeAssignmentExecTermination | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Exec runs only for a controller on the node's own host (`controller.kind: 'onebox'`, which only a
|
|
81
|
+
* local controller binding carries), and only on the connection that holds its session. A session
|
|
82
|
+
* lives until it is closed, or until the node ends it: after `idleTimeoutMs` without a byte, when
|
|
83
|
+
* its container stops, or when the controller session it was opened under ends, in which case its
|
|
84
|
+
* `execId` is gone with it. An ended session keeps its slot and its outcome until it is closed.
|
|
85
|
+
*/
|
|
86
|
+
export const runtimeAssignmentExecContract = Object.freeze({
|
|
87
|
+
error: 'Runtime assignment exec contract rejected.',
|
|
88
|
+
controllerKind: 'onebox',
|
|
89
|
+
/** Open sessions per node, ended ones not yet closed included; one more is refused. */
|
|
90
|
+
maximumSessions: 4,
|
|
91
|
+
idleTimeoutMs: 900_000,
|
|
92
|
+
maximumCommandArguments: 256,
|
|
93
|
+
/** The UTF-8 bytes of every argument together. */
|
|
94
|
+
maximumCommandBytes: 65_536,
|
|
95
|
+
/** The largest terminal dimension, the width of the runtime's own size field. */
|
|
96
|
+
maximumTerminalDimension: 65_535,
|
|
97
|
+
maximumExitCode: 255,
|
|
98
|
+
} as const);
|
|
99
|
+
|
|
100
|
+
const fail = (): never => { throw new Error(runtimeAssignmentExecContract.error); };
|
|
101
|
+
const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
|
|
102
|
+
const encoder = new TextEncoder();
|
|
103
|
+
const terminations = ['closed', 'idle-timeout', 'container-stopped'] as const;
|
|
104
|
+
|
|
105
|
+
const referenceOf = (valueArg: unknown): IRuntimeAssignmentReference => {
|
|
106
|
+
const value = networkRecord(valueArg, ['assignmentId', 'generation', 'digest']);
|
|
107
|
+
return {
|
|
108
|
+
assignmentId: networkId(value.assignmentId), generation: networkCounter(value.generation),
|
|
109
|
+
digest: networkDigest(value.digest),
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const terminalSizeOf = (valueArg: unknown): IRuntimeTerminalSize => {
|
|
113
|
+
const value = networkRecord(valueArg, ['columns', 'rows']);
|
|
114
|
+
return {
|
|
115
|
+
columns: networkCounter(value.columns, runtimeAssignmentExecContract.maximumTerminalDimension),
|
|
116
|
+
rows: networkCounter(value.rows, runtimeAssignmentExecContract.maximumTerminalDimension),
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const commandOf = (valueArg: unknown): string[] => {
|
|
120
|
+
requireNetwork(Array.isArray(valueArg) && valueArg.length > 0
|
|
121
|
+
&& valueArg.length <= runtimeAssignmentExecContract.maximumCommandArguments);
|
|
122
|
+
let bytes = 0;
|
|
123
|
+
const command = (valueArg as unknown[]).map((argumentArg) => {
|
|
124
|
+
requireNetwork(typeof argumentArg === 'string' && !argumentArg.includes('\0'));
|
|
125
|
+
bytes += encoder.encode(argumentArg).byteLength;
|
|
126
|
+
return argumentArg;
|
|
127
|
+
});
|
|
128
|
+
requireNetwork(command[0].length > 0 && bytes <= runtimeAssignmentExecContract.maximumCommandBytes);
|
|
129
|
+
return command;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const snapshotOpenRuntimeAssignmentExecRequest = (
|
|
133
|
+
valueArg: unknown,
|
|
134
|
+
): IOpenRuntimeAssignmentExecRequest => safe(() => {
|
|
135
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'assignment', 'command', 'tty', 'terminalSize']);
|
|
136
|
+
requireNetwork(typeof value.tty === 'boolean' && (value.tty === (value.terminalSize !== null)));
|
|
137
|
+
return {
|
|
138
|
+
session: snapshotRuntimeSessionBinding(value.session), assignment: referenceOf(value.assignment),
|
|
139
|
+
command: commandOf(value.command), tty: value.tty,
|
|
140
|
+
terminalSize: value.terminalSize === null ? null : terminalSizeOf(value.terminalSize),
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export const snapshotOpenRuntimeAssignmentExecResponse = (
|
|
145
|
+
valueArg: unknown,
|
|
146
|
+
): IOpenRuntimeAssignmentExecResponse => safe(() => {
|
|
147
|
+
const value = networkRecord(captureNetwork(valueArg), ['execId', 'assignment', 'tty', 'idleTimeoutMs']);
|
|
148
|
+
requireNetwork(typeof value.tty === 'boolean'
|
|
149
|
+
&& value.idleTimeoutMs === runtimeAssignmentExecContract.idleTimeoutMs);
|
|
150
|
+
return {
|
|
151
|
+
execId: networkId(value.execId), assignment: referenceOf(value.assignment), tty: value.tty,
|
|
152
|
+
idleTimeoutMs: runtimeAssignmentExecContract.idleTimeoutMs,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const snapshotResizeRuntimeAssignmentExecRequest = (
|
|
157
|
+
valueArg: unknown,
|
|
158
|
+
): IResizeRuntimeAssignmentExecRequest => safe(() => {
|
|
159
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'execId', 'terminalSize']);
|
|
160
|
+
return {
|
|
161
|
+
session: snapshotRuntimeSessionBinding(value.session), execId: networkId(value.execId),
|
|
162
|
+
terminalSize: terminalSizeOf(value.terminalSize),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export const snapshotResizeRuntimeAssignmentExecResponse = (
|
|
167
|
+
valueArg: unknown,
|
|
168
|
+
): IResizeRuntimeAssignmentExecResponse => safe(() => {
|
|
169
|
+
const value = networkRecord(captureNetwork(valueArg), ['execId']);
|
|
170
|
+
return { execId: networkId(value.execId) };
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export const snapshotCloseRuntimeAssignmentExecRequest = (
|
|
174
|
+
valueArg: unknown,
|
|
175
|
+
): ICloseRuntimeAssignmentExecRequest => safe(() => {
|
|
176
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'execId']);
|
|
177
|
+
return { session: snapshotRuntimeSessionBinding(value.session), execId: networkId(value.execId) };
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const snapshotCloseRuntimeAssignmentExecResponse = (
|
|
181
|
+
valueArg: unknown,
|
|
182
|
+
): ICloseRuntimeAssignmentExecResponse => safe(() => {
|
|
183
|
+
const value = networkRecord(captureNetwork(valueArg), ['execId', 'outcome', 'exitCode', 'termination']);
|
|
184
|
+
const execId = networkId(value.execId);
|
|
185
|
+
if (value.outcome === 'exited') {
|
|
186
|
+
requireNetwork(typeof value.exitCode === 'number' && Number.isSafeInteger(value.exitCode)
|
|
187
|
+
&& value.exitCode >= 0 && !Object.is(value.exitCode, -0)
|
|
188
|
+
&& value.exitCode <= runtimeAssignmentExecContract.maximumExitCode && value.termination === null);
|
|
189
|
+
return { execId, outcome: 'exited', exitCode: value.exitCode, termination: null };
|
|
190
|
+
}
|
|
191
|
+
requireNetwork(value.outcome === 'terminated' && value.exitCode === null
|
|
192
|
+
&& (terminations as readonly unknown[]).includes(value.termination));
|
|
193
|
+
return {
|
|
194
|
+
execId, outcome: 'terminated', exitCode: null,
|
|
195
|
+
termination: value.termination as TRuntimeAssignmentExecTermination,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/** Exec is a local controller's alone; `trustedSessionArg` decides, never a request field. */
|
|
200
|
+
const requireLocalSession = (requestSessionArg: IRuntimeSessionBinding, trustedSessionArg: unknown): void => {
|
|
201
|
+
const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
|
|
202
|
+
safe(() => requireNetwork(networkEqual(requestSessionArg, trustedSession)
|
|
203
|
+
&& trustedSession.controller.kind === runtimeAssignmentExecContract.controllerKind));
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Node-side content binding, not authentication. `trustedSessionArg` is the node's CURRENT
|
|
208
|
+
* session, the request must have arrived on that session's own connection, and that session must
|
|
209
|
+
* be a local controller's. `assignmentArg` is the node's current admitted revision of the named
|
|
210
|
+
* assignment: the request must name exactly it, it must be a `run` assignment, and it must belong
|
|
211
|
+
* to the session's controller, node and runtime namespace. Whether its container runs is the
|
|
212
|
+
* node's own check.
|
|
213
|
+
*/
|
|
214
|
+
export const bindOpenRuntimeAssignmentExecRequest = async (
|
|
215
|
+
valueArg: unknown, assignmentArg: unknown, trustedSessionArg: unknown,
|
|
216
|
+
): Promise<{ request: IOpenRuntimeAssignmentExecRequest; assignment: IRuntimeAssignment }> => {
|
|
217
|
+
const request = snapshotOpenRuntimeAssignmentExecRequest(valueArg);
|
|
218
|
+
requireLocalSession(request.session, trustedSessionArg);
|
|
219
|
+
const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
|
|
220
|
+
if ((await validateRuntimeAssignment(assignment)).length) fail();
|
|
221
|
+
safe(() => requireNetwork(assignment.disposition === 'run'
|
|
222
|
+
&& networkEqual(assignment.controller, request.session.controller)
|
|
223
|
+
&& assignment.nodeId === request.session.nodeId
|
|
224
|
+
&& assignment.runtimeNamespace === request.session.runtimeNamespace
|
|
225
|
+
&& networkEqual(request.assignment, getRuntimeAssignmentReference(assignment))));
|
|
226
|
+
return { request, assignment };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** Matches an opened session to the request: the same assignment revision and terminal mode. */
|
|
230
|
+
export const bindOpenRuntimeAssignmentExecResponse = (
|
|
231
|
+
valueArg: unknown, requestArg: unknown,
|
|
232
|
+
): IOpenRuntimeAssignmentExecResponse => safe(() => {
|
|
233
|
+
const response = snapshotOpenRuntimeAssignmentExecResponse(valueArg);
|
|
234
|
+
const request = snapshotOpenRuntimeAssignmentExecRequest(requestArg);
|
|
235
|
+
requireNetwork(networkEqual(response.assignment, request.assignment) && response.tty === request.tty);
|
|
236
|
+
return response;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
/** Node-side content binding of a resize; which sessions exist and are `tty` is the node's check. */
|
|
240
|
+
export const bindResizeRuntimeAssignmentExecRequest = (
|
|
241
|
+
valueArg: unknown, trustedSessionArg: unknown,
|
|
242
|
+
): IResizeRuntimeAssignmentExecRequest => {
|
|
243
|
+
const request = snapshotResizeRuntimeAssignmentExecRequest(valueArg);
|
|
244
|
+
requireLocalSession(request.session, trustedSessionArg);
|
|
245
|
+
return request;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export const bindResizeRuntimeAssignmentExecResponse = (
|
|
249
|
+
valueArg: unknown, requestArg: unknown,
|
|
250
|
+
): IResizeRuntimeAssignmentExecResponse => safe(() => {
|
|
251
|
+
const response = snapshotResizeRuntimeAssignmentExecResponse(valueArg);
|
|
252
|
+
requireNetwork(response.execId === snapshotResizeRuntimeAssignmentExecRequest(requestArg).execId);
|
|
253
|
+
return response;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/** Node-side content binding of a close; which sessions exist is the node's check. */
|
|
257
|
+
export const bindCloseRuntimeAssignmentExecRequest = (
|
|
258
|
+
valueArg: unknown, trustedSessionArg: unknown,
|
|
259
|
+
): ICloseRuntimeAssignmentExecRequest => {
|
|
260
|
+
const request = snapshotCloseRuntimeAssignmentExecRequest(valueArg);
|
|
261
|
+
requireLocalSession(request.session, trustedSessionArg);
|
|
262
|
+
return request;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const bindCloseRuntimeAssignmentExecResponse = (
|
|
266
|
+
valueArg: unknown, requestArg: unknown,
|
|
267
|
+
): ICloseRuntimeAssignmentExecResponse => safe(() => {
|
|
268
|
+
const response = snapshotCloseRuntimeAssignmentExecResponse(valueArg);
|
|
269
|
+
requireNetwork(response.execId === snapshotCloseRuntimeAssignmentExecRequest(requestArg).execId);
|
|
270
|
+
return response;
|
|
271
|
+
});
|