@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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { IRuntimeAssignment } from './runtimeassignment.js';
|
|
2
|
+
import { snapshotRuntimeAssignment, validateRuntimeAssignment } from './runtimeassignment.js';
|
|
3
|
+
import type { IRuntimeConfigLogPolicy } from './runtimeconfig.js';
|
|
4
|
+
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
5
|
+
import { snapshotRuntimeSessionBinding } from './runtimesession.js';
|
|
6
|
+
import {
|
|
7
|
+
captureNetwork, networkCounter, networkEqual, networkId, networkList, networkRecord, requireNetwork,
|
|
8
|
+
} from '../private/networkcontract.js';
|
|
9
|
+
import { isRuntimeLogPolicy } from '../private/runtimepolicy.js';
|
|
10
|
+
|
|
11
|
+
/** The container output a log line was written to. */
|
|
12
|
+
export type TRuntimeAssignmentLogStream = 'stdout' | 'stderr';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One piece of one line a container wrote, as the runtime captured it.
|
|
16
|
+
*
|
|
17
|
+
* `text` holds no line terminator and no `\n`; the node decodes the captured bytes as UTF-8 and
|
|
18
|
+
* replaces an invalid sequence with U+FFFD. A line longer than one entry travels as consecutive
|
|
19
|
+
* entries of the same stream: every piece but the last states `partial: true`, and the receiver
|
|
20
|
+
* joins them. The joined line is at most the config's `logs.maximumLineBytes` UTF-8 bytes; the node
|
|
21
|
+
* cuts a longer one there and follows its last piece with a `line-cut` loss of the same stream.
|
|
22
|
+
*/
|
|
23
|
+
export interface IRuntimeAssignmentLogLine {
|
|
24
|
+
kind: 'line';
|
|
25
|
+
stream: TRuntimeAssignmentLogStream;
|
|
26
|
+
/** When the runtime captured the piece, in nanoseconds since the Unix epoch, as decimal digits. */
|
|
27
|
+
timestampNs: string;
|
|
28
|
+
text: string;
|
|
29
|
+
/** The line continues in the next `line` entry of the same stream. */
|
|
30
|
+
partial: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Why output did not reach the controller.
|
|
35
|
+
*
|
|
36
|
+
* - `buffer-overflow`: the node's buffer held `logs.maximumBufferedBytes` unacknowledged bytes and
|
|
37
|
+
* the oldest were dropped to keep capturing;
|
|
38
|
+
* - `line-cut`: the rest of a line beyond `logs.maximumLineBytes`;
|
|
39
|
+
* - `capture-restart`: the node's capture restarted without its buffer, for example after a reboot
|
|
40
|
+
* emptied its disposable capture directory; how much was lost is unknown;
|
|
41
|
+
* - `not-captured`: the container was started without output capture, so none of its output is
|
|
42
|
+
* available; how much there was is unknown.
|
|
43
|
+
*/
|
|
44
|
+
export type TRuntimeAssignmentLogLossReason = 'buffer-overflow' | 'line-cut' | 'capture-restart' | 'not-captured';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Evidence of output that was lost, in the position of the stream where it was lost.
|
|
48
|
+
*
|
|
49
|
+
* `bytes` and `lines` count exactly what was dropped; both are `null` for `capture-restart` and
|
|
50
|
+
* `not-captured`, whose extent the node cannot know, and both are stated for the other reasons.
|
|
51
|
+
* A `line-cut` counts the cut bytes and no line, because the line itself was delivered; it names
|
|
52
|
+
* the stream of that line. A `buffer-overflow` counts the dropped bytes and the whole lines among
|
|
53
|
+
* them, and names a stream only when every dropped byte came from it.
|
|
54
|
+
*/
|
|
55
|
+
export interface IRuntimeAssignmentLogLoss {
|
|
56
|
+
kind: 'lost';
|
|
57
|
+
reason: TRuntimeAssignmentLogLossReason;
|
|
58
|
+
stream: TRuntimeAssignmentLogStream | null;
|
|
59
|
+
/** When the node detected the loss, in nanoseconds since the Unix epoch, as decimal digits. */
|
|
60
|
+
timestampNs: string;
|
|
61
|
+
bytes: number | null;
|
|
62
|
+
lines: number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type TRuntimeAssignmentLogEntry = IRuntimeAssignmentLogLine | IRuntimeAssignmentLogLoss;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One delivery of one attempt's captured output, in capture order.
|
|
69
|
+
*
|
|
70
|
+
* `captureId` names one capture of the attempt on its node: the node starts a new capture, with a
|
|
71
|
+
* new id and `sequence` 1, whenever it cannot continue the previous one, and opens it with a
|
|
72
|
+
* `capture-restart` loss. Within a capture `sequence` counts batches from 1 without gaps, and the
|
|
73
|
+
* node sends the next batch only after the controller acknowledged the previous one, so a batch is
|
|
74
|
+
* the one unit of retry. `final` states that no batch follows in this capture: the container is gone
|
|
75
|
+
* and its output is drained. Only a final batch may be empty.
|
|
76
|
+
*/
|
|
77
|
+
export interface IRuntimeAssignmentLogBatch {
|
|
78
|
+
assignmentId: string;
|
|
79
|
+
attemptId: string;
|
|
80
|
+
captureId: string;
|
|
81
|
+
sequence: number;
|
|
82
|
+
entries: TRuntimeAssignmentLogEntry[];
|
|
83
|
+
final: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Pallet push over its current authenticated session. */
|
|
87
|
+
export interface IReportRuntimeAssignmentLogsRequest {
|
|
88
|
+
session: IRuntimeSessionBinding;
|
|
89
|
+
batch: IRuntimeAssignmentLogBatch;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The controller holds the batch durably. `replay` answers a batch it already holds, which it does
|
|
94
|
+
* not store again. It never refuses a batch for a gap in `sequence`: the node discards only what
|
|
95
|
+
* was acknowledged, so a missing batch can only be the controller's own loss, which it records.
|
|
96
|
+
*/
|
|
97
|
+
export interface IReportRuntimeAssignmentLogsResponse {
|
|
98
|
+
status: 'accepted' | 'replay';
|
|
99
|
+
assignmentId: string;
|
|
100
|
+
captureId: string;
|
|
101
|
+
sequence: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const runtimeAssignmentLogContract = Object.freeze({
|
|
105
|
+
error: 'Runtime assignment log contract rejected.',
|
|
106
|
+
/** Pallet pushes; the controller persists metadata and payloads and acknowledges by sequence. */
|
|
107
|
+
delivery: 'node-pushes-controller-acknowledges-each-batch',
|
|
108
|
+
/** The most entries in one batch. */
|
|
109
|
+
maximumEntriesPerBatch: 1024,
|
|
110
|
+
/** The most UTF-8 bytes of `text` in one entry; a longer line travels as `partial` pieces. */
|
|
111
|
+
maximumEntryTextBytes: 65_536,
|
|
112
|
+
/**
|
|
113
|
+
* The canonical JSON of a whole report is at most one mebibyte, the bound every runtime network
|
|
114
|
+
* contract captures with; the node closes a batch before the next entry would cross it.
|
|
115
|
+
*/
|
|
116
|
+
maximumReportCanonicalBytes: 1_048_576,
|
|
117
|
+
} as const);
|
|
118
|
+
|
|
119
|
+
const fail = (): never => { throw new Error(runtimeAssignmentLogContract.error); };
|
|
120
|
+
const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
|
|
121
|
+
const encoder = new TextEncoder();
|
|
122
|
+
const streams = ['stdout', 'stderr'] as const;
|
|
123
|
+
const lossReasons = ['buffer-overflow', 'line-cut', 'capture-restart', 'not-captured'] as const;
|
|
124
|
+
const statuses = ['accepted', 'replay'] as const;
|
|
125
|
+
|
|
126
|
+
const literalOf = <TLiteral extends string>(valueArg: unknown, allowedArg: readonly TLiteral[]): TLiteral => {
|
|
127
|
+
requireNetwork((allowedArg as readonly unknown[]).includes(valueArg));
|
|
128
|
+
return valueArg as TLiteral;
|
|
129
|
+
};
|
|
130
|
+
/** Nanoseconds since the Unix epoch as canonical decimal digits: no sign, no leading zero. */
|
|
131
|
+
const timestampOf = (valueArg: unknown): string => {
|
|
132
|
+
requireNetwork(typeof valueArg === 'string' && /^[1-9][0-9]{0,19}$/.test(valueArg)
|
|
133
|
+
&& BigInt(valueArg) <= 18_446_744_073_709_551_615n);
|
|
134
|
+
return valueArg;
|
|
135
|
+
};
|
|
136
|
+
const countOf = (valueArg: unknown): number => {
|
|
137
|
+
requireNetwork(typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
|
|
138
|
+
&& !Object.is(valueArg, -0));
|
|
139
|
+
return valueArg;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const lineOf = (valueArg: unknown): IRuntimeAssignmentLogLine => {
|
|
143
|
+
const value = networkRecord(valueArg, ['kind', 'stream', 'timestampNs', 'text', 'partial']);
|
|
144
|
+
requireNetwork(value.kind === 'line' && typeof value.text === 'string' && !value.text.includes('\n')
|
|
145
|
+
&& encoder.encode(value.text).byteLength <= runtimeAssignmentLogContract.maximumEntryTextBytes
|
|
146
|
+
&& typeof value.partial === 'boolean');
|
|
147
|
+
return {
|
|
148
|
+
kind: 'line', stream: literalOf(value.stream, streams), timestampNs: timestampOf(value.timestampNs),
|
|
149
|
+
text: value.text, partial: value.partial,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const lossOf = (valueArg: unknown): IRuntimeAssignmentLogLoss => {
|
|
154
|
+
const value = networkRecord(valueArg, ['kind', 'reason', 'stream', 'timestampNs', 'bytes', 'lines']);
|
|
155
|
+
requireNetwork(value.kind === 'lost');
|
|
156
|
+
const reason = literalOf(value.reason, lossReasons);
|
|
157
|
+
const stream = value.stream === null ? null : literalOf(value.stream, streams);
|
|
158
|
+
const unknownExtent = reason === 'capture-restart' || reason === 'not-captured';
|
|
159
|
+
if (unknownExtent) {
|
|
160
|
+
requireNetwork(value.bytes === null && value.lines === null && stream === null);
|
|
161
|
+
return { kind: 'lost', reason, stream, timestampNs: timestampOf(value.timestampNs), bytes: null, lines: null };
|
|
162
|
+
}
|
|
163
|
+
const bytes = networkCounter(value.bytes);
|
|
164
|
+
const lines = countOf(value.lines);
|
|
165
|
+
requireNetwork(reason !== 'line-cut' || (stream !== null && lines === 0));
|
|
166
|
+
return { kind: 'lost', reason, stream, timestampNs: timestampOf(value.timestampNs), bytes, lines };
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const entryOf = (valueArg: unknown): TRuntimeAssignmentLogEntry => {
|
|
170
|
+
requireNetwork(valueArg !== null && typeof valueArg === 'object');
|
|
171
|
+
return (valueArg as Record<string, unknown>).kind === 'lost' ? lossOf(valueArg) : lineOf(valueArg);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const batchOf = (valueArg: unknown): IRuntimeAssignmentLogBatch => {
|
|
175
|
+
const value = networkRecord(valueArg, ['assignmentId', 'attemptId', 'captureId', 'sequence', 'entries', 'final']);
|
|
176
|
+
const entries = networkList(value.entries, runtimeAssignmentLogContract.maximumEntriesPerBatch, entryOf);
|
|
177
|
+
requireNetwork(typeof value.final === 'boolean' && (entries.length > 0 || value.final));
|
|
178
|
+
return {
|
|
179
|
+
assignmentId: networkId(value.assignmentId), attemptId: networkId(value.attemptId),
|
|
180
|
+
captureId: networkId(value.captureId), sequence: networkCounter(value.sequence), entries, final: value.final,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Detached exact-schema snapshot of one batch, bounded like a whole report. */
|
|
185
|
+
export const snapshotRuntimeAssignmentLogBatch = (valueArg: unknown): IRuntimeAssignmentLogBatch =>
|
|
186
|
+
safe(() => batchOf(captureNetwork(valueArg)));
|
|
187
|
+
|
|
188
|
+
export const snapshotReportRuntimeAssignmentLogsRequest = (
|
|
189
|
+
valueArg: unknown,
|
|
190
|
+
): IReportRuntimeAssignmentLogsRequest => safe(() => {
|
|
191
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'batch']);
|
|
192
|
+
return { session: snapshotRuntimeSessionBinding(value.session), batch: batchOf(value.batch) };
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export const snapshotReportRuntimeAssignmentLogsResponse = (
|
|
196
|
+
valueArg: unknown,
|
|
197
|
+
): IReportRuntimeAssignmentLogsResponse => safe(() => {
|
|
198
|
+
const value = networkRecord(captureNetwork(valueArg), ['status', 'assignmentId', 'captureId', 'sequence']);
|
|
199
|
+
return {
|
|
200
|
+
status: literalOf(value.status, statuses), assignmentId: networkId(value.assignmentId),
|
|
201
|
+
captureId: networkId(value.captureId), sequence: networkCounter(value.sequence),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Controller-side content binding, not authentication. `trustedSessionArg` is the controller's
|
|
207
|
+
* CURRENT session with the node the report arrived on, never read from the request. `assignmentArg`
|
|
208
|
+
* is the controller's own record of the batch's assignment, any generation of it, and
|
|
209
|
+
* `logPolicyArg` the `logs` policy of the config it runs with. The request's session must equal the
|
|
210
|
+
* trusted one, the assignment must be this node's and carry the batch's attempt, and every line
|
|
211
|
+
* the batch holds, its pieces joined, must fit the policy's `maximumLineBytes`. A line whose first
|
|
212
|
+
* pieces travelled in an earlier batch is judged by the pieces this batch holds.
|
|
213
|
+
*/
|
|
214
|
+
export const bindReportRuntimeAssignmentLogsRequest = async (
|
|
215
|
+
valueArg: unknown, assignmentArg: unknown, logPolicyArg: IRuntimeConfigLogPolicy, trustedSessionArg: unknown,
|
|
216
|
+
): Promise<{ request: IReportRuntimeAssignmentLogsRequest; assignment: IRuntimeAssignment }> => {
|
|
217
|
+
const request = snapshotReportRuntimeAssignmentLogsRequest(valueArg);
|
|
218
|
+
const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
|
|
219
|
+
const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
|
|
220
|
+
if ((await validateRuntimeAssignment(assignment)).length) fail();
|
|
221
|
+
safe(() => {
|
|
222
|
+
requireNetwork(isRuntimeLogPolicy(logPolicyArg));
|
|
223
|
+
requireNetwork(networkEqual(request.session, trustedSession)
|
|
224
|
+
&& networkEqual(assignment.controller, trustedSession.controller)
|
|
225
|
+
&& assignment.nodeId === trustedSession.nodeId
|
|
226
|
+
&& assignment.runtimeNamespace === trustedSession.runtimeNamespace
|
|
227
|
+
&& assignment.assignmentId === request.batch.assignmentId
|
|
228
|
+
&& assignment.attemptId === request.batch.attemptId);
|
|
229
|
+
const joined: Record<TRuntimeAssignmentLogStream, number> = { stdout: 0, stderr: 0 };
|
|
230
|
+
for (const entry of request.batch.entries) {
|
|
231
|
+
if (entry.kind !== 'line') continue;
|
|
232
|
+
joined[entry.stream] += encoder.encode(entry.text).byteLength;
|
|
233
|
+
requireNetwork(joined[entry.stream] <= logPolicyArg.maximumLineBytes);
|
|
234
|
+
if (!entry.partial) joined[entry.stream] = 0;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return { request, assignment };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** Matches an acknowledgement to the exact batch that was sent. */
|
|
241
|
+
export const bindReportRuntimeAssignmentLogsResponse = (
|
|
242
|
+
valueArg: unknown, requestArg: unknown,
|
|
243
|
+
): IReportRuntimeAssignmentLogsResponse => safe(() => {
|
|
244
|
+
const response = snapshotReportRuntimeAssignmentLogsResponse(valueArg);
|
|
245
|
+
const { batch } = snapshotReportRuntimeAssignmentLogsRequest(requestArg);
|
|
246
|
+
requireNetwork(response.assignmentId === batch.assignmentId && response.captureId === batch.captureId
|
|
247
|
+
&& response.sequence === batch.sequence);
|
|
248
|
+
return response;
|
|
249
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
/** Bytes the workload's network namespace received and sent since the sandbox started. */
|
|
10
|
+
export interface IRuntimeAssignmentNetworkStats {
|
|
11
|
+
receivedBytes: number;
|
|
12
|
+
transmittedBytes: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One resource sample of one running attempt, as the runtime reports it.
|
|
17
|
+
*
|
|
18
|
+
* `cpuUsageCoreNanoseconds` is cumulative CPU time across every core since the container started,
|
|
19
|
+
* as decimal digits because it outgrows a JSON number; a reader derives utilisation from two
|
|
20
|
+
* samples and their `timestampNs`. `memoryLimitBytes` is the limit the node enforces, the sealed
|
|
21
|
+
* config's `memoryBytes`. `network` is `null` when the runtime reports no interface of the
|
|
22
|
+
* sandbox, and `writableLayerBytes` is `null` when it reports no usage of the container's writable
|
|
23
|
+
* layer.
|
|
24
|
+
*/
|
|
25
|
+
export interface IRuntimeAssignmentStatsSample {
|
|
26
|
+
/** When the runtime took the sample, in nanoseconds since the Unix epoch, as decimal digits. */
|
|
27
|
+
timestampNs: string;
|
|
28
|
+
cpuUsageCoreNanoseconds: string;
|
|
29
|
+
memoryWorkingSetBytes: number;
|
|
30
|
+
memoryLimitBytes: number;
|
|
31
|
+
network: IRuntimeAssignmentNetworkStats | null;
|
|
32
|
+
writableLayerBytes: number | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Controller read over the node's current authenticated session. */
|
|
36
|
+
export interface IReadRuntimeAssignmentStatsRequest {
|
|
37
|
+
session: IRuntimeSessionBinding;
|
|
38
|
+
/** The exact current revision of an assignment on this node. */
|
|
39
|
+
assignment: IRuntimeAssignmentReference;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `sampled` carries one sample; `not-running` states that the attempt has no running container to
|
|
44
|
+
* sample, and carries none.
|
|
45
|
+
*/
|
|
46
|
+
export interface IReadRuntimeAssignmentStatsResponse {
|
|
47
|
+
status: 'sampled' | 'not-running';
|
|
48
|
+
assignment: IRuntimeAssignmentReference;
|
|
49
|
+
sample: IRuntimeAssignmentStatsSample | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const runtimeAssignmentStatsContract = Object.freeze({
|
|
53
|
+
error: 'Runtime assignment stats contract rejected.',
|
|
54
|
+
/** The node answers one read per assignment at a time and refuses a concurrent one. */
|
|
55
|
+
concurrency: 'one-read-per-assignment',
|
|
56
|
+
} as const);
|
|
57
|
+
|
|
58
|
+
const fail = (): never => { throw new Error(runtimeAssignmentStatsContract.error); };
|
|
59
|
+
const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
|
|
60
|
+
|
|
61
|
+
/** An unsigned 64-bit counter as canonical decimal digits: no sign, no leading zero. */
|
|
62
|
+
const decimalOf = (valueArg: unknown): string => {
|
|
63
|
+
requireNetwork(typeof valueArg === 'string' && /^(0|[1-9][0-9]{0,19})$/.test(valueArg)
|
|
64
|
+
&& BigInt(valueArg) <= 18_446_744_073_709_551_615n);
|
|
65
|
+
return valueArg;
|
|
66
|
+
};
|
|
67
|
+
const bytesOf = (valueArg: unknown): number => {
|
|
68
|
+
requireNetwork(typeof valueArg === 'number' && Number.isSafeInteger(valueArg) && valueArg >= 0
|
|
69
|
+
&& !Object.is(valueArg, -0));
|
|
70
|
+
return valueArg;
|
|
71
|
+
};
|
|
72
|
+
const referenceOf = (valueArg: unknown): IRuntimeAssignmentReference => {
|
|
73
|
+
const value = networkRecord(valueArg, ['assignmentId', 'generation', 'digest']);
|
|
74
|
+
return {
|
|
75
|
+
assignmentId: networkId(value.assignmentId), generation: networkCounter(value.generation),
|
|
76
|
+
digest: networkDigest(value.digest),
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const sampleOf = (valueArg: unknown): IRuntimeAssignmentStatsSample => {
|
|
81
|
+
const value = networkRecord(valueArg, ['timestampNs', 'cpuUsageCoreNanoseconds', 'memoryWorkingSetBytes',
|
|
82
|
+
'memoryLimitBytes', 'network', 'writableLayerBytes']);
|
|
83
|
+
const timestampNs = decimalOf(value.timestampNs);
|
|
84
|
+
requireNetwork(timestampNs !== '0');
|
|
85
|
+
let network: IRuntimeAssignmentNetworkStats | null = null;
|
|
86
|
+
if (value.network !== null) {
|
|
87
|
+
const item = networkRecord(value.network, ['receivedBytes', 'transmittedBytes']);
|
|
88
|
+
network = { receivedBytes: bytesOf(item.receivedBytes), transmittedBytes: bytesOf(item.transmittedBytes) };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
timestampNs,
|
|
92
|
+
cpuUsageCoreNanoseconds: decimalOf(value.cpuUsageCoreNanoseconds),
|
|
93
|
+
memoryWorkingSetBytes: bytesOf(value.memoryWorkingSetBytes),
|
|
94
|
+
memoryLimitBytes: networkCounter(value.memoryLimitBytes),
|
|
95
|
+
network,
|
|
96
|
+
writableLayerBytes: value.writableLayerBytes === null ? null : bytesOf(value.writableLayerBytes),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const snapshotReadRuntimeAssignmentStatsRequest = (
|
|
101
|
+
valueArg: unknown,
|
|
102
|
+
): IReadRuntimeAssignmentStatsRequest => safe(() => {
|
|
103
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'assignment']);
|
|
104
|
+
return { session: snapshotRuntimeSessionBinding(value.session), assignment: referenceOf(value.assignment) };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export const snapshotReadRuntimeAssignmentStatsResponse = (
|
|
108
|
+
valueArg: unknown,
|
|
109
|
+
): IReadRuntimeAssignmentStatsResponse => safe(() => {
|
|
110
|
+
const value = networkRecord(captureNetwork(valueArg), ['status', 'assignment', 'sample']);
|
|
111
|
+
requireNetwork((value.status === 'sampled' && value.sample !== null)
|
|
112
|
+
|| (value.status === 'not-running' && value.sample === null));
|
|
113
|
+
return {
|
|
114
|
+
status: value.status as IReadRuntimeAssignmentStatsResponse['status'],
|
|
115
|
+
assignment: referenceOf(value.assignment),
|
|
116
|
+
sample: value.sample === null ? null : sampleOf(value.sample),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Node-side content binding, not authentication. `trustedSessionArg` is the node's CURRENT
|
|
122
|
+
* session, and the request must have arrived on that session's own connection. `assignmentArg` is
|
|
123
|
+
* the node's current admitted revision of the named assignment: the request must name exactly it,
|
|
124
|
+
* and it must belong to the session's controller, node and runtime namespace.
|
|
125
|
+
*/
|
|
126
|
+
export const bindReadRuntimeAssignmentStatsRequest = async (
|
|
127
|
+
valueArg: unknown, assignmentArg: unknown, trustedSessionArg: unknown,
|
|
128
|
+
): Promise<{ request: IReadRuntimeAssignmentStatsRequest; assignment: IRuntimeAssignment }> => {
|
|
129
|
+
const request = snapshotReadRuntimeAssignmentStatsRequest(valueArg);
|
|
130
|
+
const assignment = safe(() => snapshotRuntimeAssignment(assignmentArg));
|
|
131
|
+
const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
|
|
132
|
+
if ((await validateRuntimeAssignment(assignment)).length) fail();
|
|
133
|
+
safe(() => requireNetwork(networkEqual(request.session, trustedSession)
|
|
134
|
+
&& networkEqual(assignment.controller, trustedSession.controller)
|
|
135
|
+
&& assignment.nodeId === trustedSession.nodeId
|
|
136
|
+
&& assignment.runtimeNamespace === trustedSession.runtimeNamespace
|
|
137
|
+
&& networkEqual(request.assignment, getRuntimeAssignmentReference(assignment))));
|
|
138
|
+
return { request, assignment };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Matches a sample to the exact assignment revision the read named. */
|
|
142
|
+
export const bindReadRuntimeAssignmentStatsResponse = (
|
|
143
|
+
valueArg: unknown, requestArg: unknown,
|
|
144
|
+
): IReadRuntimeAssignmentStatsResponse => safe(() => {
|
|
145
|
+
const response = snapshotReadRuntimeAssignmentStatsResponse(valueArg);
|
|
146
|
+
const request = snapshotReadRuntimeAssignmentStatsRequest(requestArg);
|
|
147
|
+
requireNetwork(networkEqual(response.assignment, request.assignment));
|
|
148
|
+
return response;
|
|
149
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type * as plugins from '../plugins.js';
|
|
2
|
+
import type { TSha256Digest } from './immutableimage.js';
|
|
3
|
+
import type { IRuntimeAuthorityReference } from './runtimeassignment.js';
|
|
4
|
+
import type { IRuntimeLocalStorageClaim } from './runtimelocalstorageclaim.js';
|
|
5
|
+
import {
|
|
6
|
+
getRuntimeLocalStorageClaimReference, snapshotRuntimeLocalStorageClaim, validateRuntimeLocalStorageClaim,
|
|
7
|
+
} from './runtimelocalstorageclaim.js';
|
|
8
|
+
import type { IRuntimeSessionBinding } from './runtimesession.js';
|
|
9
|
+
import { snapshotRuntimeSessionBinding } from './runtimesession.js';
|
|
10
|
+
import {
|
|
11
|
+
captureNetwork, networkDigest, networkEqual, networkRecord, networkReference, requireNetwork,
|
|
12
|
+
} from '../private/networkcontract.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The one archive format a volume travels in: a POSIX.1-2001 pax archive of the volume's tree.
|
|
16
|
+
*
|
|
17
|
+
* The first entry is the volume's root, `./`, and every other name is relative below it. An entry is
|
|
18
|
+
* a directory, a regular file, a symbolic link (stored, never followed), a hard link to an earlier
|
|
19
|
+
* entry of the same archive, or a FIFO. Ownership is numeric: `uid` and `gid`, never user or group
|
|
20
|
+
* names, so the tree means the same on every host. Mode bits, including setuid, setgid and sticky,
|
|
21
|
+
* and the modification time travel exactly. A socket carries no bytes and belongs to the process
|
|
22
|
+
* that created it, so it is omitted; a device node, extended attributes and ACLs are not part of
|
|
23
|
+
* the format, and a volume that holds a device node is refused. The archive ends with the format's
|
|
24
|
+
* two zero blocks, so its length is a whole number of 512-byte blocks.
|
|
25
|
+
*/
|
|
26
|
+
export type TRuntimeLocalVolumeArchiveFormat = 'pax';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A local controller reads one volume as an archive. The node refuses while a running container
|
|
30
|
+
* holds the volume: it reads a volume no container holds, or one whose holding container is
|
|
31
|
+
* stopped, which a node never restarts, and no container is granted the volume while it reads.
|
|
32
|
+
*/
|
|
33
|
+
export interface IReadRuntimeLocalVolumeArchiveRequest {
|
|
34
|
+
session: IRuntimeSessionBinding;
|
|
35
|
+
/** The reference of the node's current generation of a bound `directory` claim. */
|
|
36
|
+
claim: IRuntimeAuthorityReference;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The archive arrives on the stream the TypedRequest answers beside this body. `holder` states
|
|
41
|
+
* whether the volume was read unheld or under a stopped holder. The stream carries no integrity:
|
|
42
|
+
* the node learns the archive's length and digest only while it writes it.
|
|
43
|
+
*/
|
|
44
|
+
export interface IReadRuntimeLocalVolumeArchiveResponse {
|
|
45
|
+
claim: IRuntimeAuthorityReference;
|
|
46
|
+
format: TRuntimeLocalVolumeArchiveFormat;
|
|
47
|
+
holder: 'none' | 'stopped';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A local controller writes one archive as the bytes of an `import` claim that waits in
|
|
52
|
+
* `import-required`: the online form of an operator's offline import. The node receives the stream
|
|
53
|
+
* beside this body, whose integrity must be exactly `byteLength` and `digest`, unpacks it into a
|
|
54
|
+
* private staging tree, and publishes that tree as the volume only after the whole archive
|
|
55
|
+
* verified. The tree keeps the ownership the archive carries, as every imported tree does.
|
|
56
|
+
*/
|
|
57
|
+
export interface IWriteRuntimeLocalVolumeArchiveRequest {
|
|
58
|
+
session: IRuntimeSessionBinding;
|
|
59
|
+
claim: IRuntimeAuthorityReference;
|
|
60
|
+
format: TRuntimeLocalVolumeArchiveFormat;
|
|
61
|
+
byteLength: number;
|
|
62
|
+
digest: TSha256Digest;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `receiving` permits sending and confirms nothing more. The stream's acceptance receipt is what
|
|
67
|
+
* confirms that the node published the volume as `ready`; a rejected or aborted stream leaves the
|
|
68
|
+
* claim in `import-required` with nothing published.
|
|
69
|
+
*/
|
|
70
|
+
export interface IWriteRuntimeLocalVolumeArchiveResponse {
|
|
71
|
+
claim: IRuntimeAuthorityReference;
|
|
72
|
+
status: 'receiving';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Volume archives are a local controller's alone (`controller.kind: 'onebox'`), on the connection
|
|
77
|
+
* that holds its session, and a node moves one archive per volume at a time: a read or write of a
|
|
78
|
+
* volume that is already moving one is refused.
|
|
79
|
+
*/
|
|
80
|
+
export const runtimeLocalVolumeArchiveContract = Object.freeze({
|
|
81
|
+
error: 'Runtime local volume archive contract rejected.',
|
|
82
|
+
controllerKind: 'onebox',
|
|
83
|
+
format: 'pax',
|
|
84
|
+
entryKinds: ['directory', 'file', 'symlink', 'hardlink', 'fifo'],
|
|
85
|
+
omittedEntryKinds: ['socket'],
|
|
86
|
+
ownership: 'numeric-uid-gid',
|
|
87
|
+
blockBytes: 512,
|
|
88
|
+
/** The shortest archive: the root directory's header and the two closing zero blocks. */
|
|
89
|
+
minimumArchiveBytes: 1536,
|
|
90
|
+
concurrency: 'one-archive-per-volume',
|
|
91
|
+
} as const);
|
|
92
|
+
|
|
93
|
+
const fail = (): never => { throw new Error(runtimeLocalVolumeArchiveContract.error); };
|
|
94
|
+
const safe = <T>(operationArg: () => T): T => { try { return operationArg(); } catch { return fail(); } };
|
|
95
|
+
|
|
96
|
+
const archiveBytesOf = (valueArg: unknown): number => {
|
|
97
|
+
requireNetwork(typeof valueArg === 'number' && Number.isSafeInteger(valueArg)
|
|
98
|
+
&& valueArg >= runtimeLocalVolumeArchiveContract.minimumArchiveBytes
|
|
99
|
+
&& valueArg % runtimeLocalVolumeArchiveContract.blockBytes === 0);
|
|
100
|
+
return valueArg;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const snapshotReadRuntimeLocalVolumeArchiveRequest = (
|
|
104
|
+
valueArg: unknown,
|
|
105
|
+
): IReadRuntimeLocalVolumeArchiveRequest => safe(() => {
|
|
106
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'claim']);
|
|
107
|
+
return { session: snapshotRuntimeSessionBinding(value.session), claim: networkReference(value.claim) };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const snapshotReadRuntimeLocalVolumeArchiveResponse = (
|
|
111
|
+
valueArg: unknown,
|
|
112
|
+
): IReadRuntimeLocalVolumeArchiveResponse => safe(() => {
|
|
113
|
+
const value = networkRecord(captureNetwork(valueArg), ['claim', 'format', 'holder']);
|
|
114
|
+
requireNetwork(value.format === 'pax' && (value.holder === 'none' || value.holder === 'stopped'));
|
|
115
|
+
return { claim: networkReference(value.claim), format: 'pax', holder: value.holder };
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const snapshotWriteRuntimeLocalVolumeArchiveRequest = (
|
|
119
|
+
valueArg: unknown,
|
|
120
|
+
): IWriteRuntimeLocalVolumeArchiveRequest => safe(() => {
|
|
121
|
+
const value = networkRecord(captureNetwork(valueArg), ['session', 'claim', 'format', 'byteLength', 'digest']);
|
|
122
|
+
requireNetwork(value.format === 'pax');
|
|
123
|
+
return {
|
|
124
|
+
session: snapshotRuntimeSessionBinding(value.session), claim: networkReference(value.claim), format: 'pax',
|
|
125
|
+
byteLength: archiveBytesOf(value.byteLength), digest: networkDigest(value.digest),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const snapshotWriteRuntimeLocalVolumeArchiveResponse = (
|
|
130
|
+
valueArg: unknown,
|
|
131
|
+
): IWriteRuntimeLocalVolumeArchiveResponse => safe(() => {
|
|
132
|
+
const value = networkRecord(captureNetwork(valueArg), ['claim', 'status']);
|
|
133
|
+
requireNetwork(value.status === 'receiving');
|
|
134
|
+
return { claim: networkReference(value.claim), status: 'receiving' };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** The exact integrity the write's stream must carry: the request's length and digest. */
|
|
138
|
+
export const getRuntimeLocalVolumeArchiveIntegrity = (
|
|
139
|
+
requestArg: unknown,
|
|
140
|
+
): plugins.typedrequestInterfaces.IVirtualStreamIntegrity => {
|
|
141
|
+
const request = snapshotWriteRuntimeLocalVolumeArchiveRequest(requestArg);
|
|
142
|
+
return { algorithm: 'sha256', byteLength: request.byteLength, digest: request.digest };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The claim a request names must be the node's current generation of a bound `directory` claim of
|
|
147
|
+
* the session's controller, cluster, node and runtime namespace, and the session must be a local
|
|
148
|
+
* controller's current one.
|
|
149
|
+
*/
|
|
150
|
+
const bindClaim = async (
|
|
151
|
+
requestArg: { session: IRuntimeSessionBinding; claim: IRuntimeAuthorityReference },
|
|
152
|
+
claimArg: unknown, trustedSessionArg: unknown,
|
|
153
|
+
): Promise<IRuntimeLocalStorageClaim> => {
|
|
154
|
+
const trustedSession = safe(() => snapshotRuntimeSessionBinding(trustedSessionArg));
|
|
155
|
+
const claim = safe(() => snapshotRuntimeLocalStorageClaim(claimArg));
|
|
156
|
+
if ((await validateRuntimeLocalStorageClaim(claim)).length) fail();
|
|
157
|
+
safe(() => requireNetwork(networkEqual(requestArg.session, trustedSession)
|
|
158
|
+
&& trustedSession.controller.kind === runtimeLocalVolumeArchiveContract.controllerKind
|
|
159
|
+
&& networkEqual(requestArg.claim, getRuntimeLocalStorageClaimReference(claim))
|
|
160
|
+
&& claim.lifecycle === 'bound' && claim.source.kind === 'directory'
|
|
161
|
+
&& networkEqual(claim.controller, trustedSession.controller)
|
|
162
|
+
&& claim.clusterId === trustedSession.clusterId
|
|
163
|
+
&& claim.nodeId === trustedSession.nodeId
|
|
164
|
+
&& claim.runtimeNamespace === trustedSession.runtimeNamespace));
|
|
165
|
+
return claim;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Node-side content binding, not authentication. `claimArg` is the node's current generation of the
|
|
170
|
+
* named claim and `trustedSessionArg` its CURRENT session, on whose own connection the request must
|
|
171
|
+
* have arrived. Whether the volume is `ready` and who holds it are the node's ledger checks.
|
|
172
|
+
*/
|
|
173
|
+
export const bindReadRuntimeLocalVolumeArchiveRequest = async (
|
|
174
|
+
valueArg: unknown, claimArg: unknown, trustedSessionArg: unknown,
|
|
175
|
+
): Promise<{ request: IReadRuntimeLocalVolumeArchiveRequest; claim: IRuntimeLocalStorageClaim }> => {
|
|
176
|
+
const request = snapshotReadRuntimeLocalVolumeArchiveRequest(valueArg);
|
|
177
|
+
return { request, claim: await bindClaim(request, claimArg, trustedSessionArg) };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* As the read binding, and the claim must be an `import` claim. That it still waits in
|
|
182
|
+
* `import-required` is the node's ledger check.
|
|
183
|
+
*/
|
|
184
|
+
export const bindWriteRuntimeLocalVolumeArchiveRequest = async (
|
|
185
|
+
valueArg: unknown, claimArg: unknown, trustedSessionArg: unknown,
|
|
186
|
+
): Promise<{ request: IWriteRuntimeLocalVolumeArchiveRequest; claim: IRuntimeLocalStorageClaim }> => {
|
|
187
|
+
const request = snapshotWriteRuntimeLocalVolumeArchiveRequest(valueArg);
|
|
188
|
+
const claim = await bindClaim(request, claimArg, trustedSessionArg);
|
|
189
|
+
if (claim.initialization !== 'import') fail();
|
|
190
|
+
return { request, claim };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const bindReadRuntimeLocalVolumeArchiveResponse = (
|
|
194
|
+
valueArg: unknown, requestArg: unknown,
|
|
195
|
+
): IReadRuntimeLocalVolumeArchiveResponse => safe(() => {
|
|
196
|
+
const response = snapshotReadRuntimeLocalVolumeArchiveResponse(valueArg);
|
|
197
|
+
requireNetwork(networkEqual(response.claim, snapshotReadRuntimeLocalVolumeArchiveRequest(requestArg).claim));
|
|
198
|
+
return response;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const bindWriteRuntimeLocalVolumeArchiveResponse = (
|
|
202
|
+
valueArg: unknown, requestArg: unknown,
|
|
203
|
+
): IWriteRuntimeLocalVolumeArchiveResponse => safe(() => {
|
|
204
|
+
const response = snapshotWriteRuntimeLocalVolumeArchiveResponse(valueArg);
|
|
205
|
+
requireNetwork(networkEqual(response.claim, snapshotWriteRuntimeLocalVolumeArchiveRequest(requestArg).claim));
|
|
206
|
+
return response;
|
|
207
|
+
});
|
package/ts/data/user.ts
CHANGED
|
@@ -13,6 +13,11 @@ export type TServiceDeploymentCapability =
|
|
|
13
13
|
| 'deployment:adopt-existing'
|
|
14
14
|
| 'deployment:status'
|
|
15
15
|
| 'deployment:create-greenfield'
|
|
16
|
+
/**
|
|
17
|
+
* Reserve a greenfield successor whose image Cloudly imports from an attested legacy service,
|
|
18
|
+
* and have Cloudly import it. Granted on an organization-service-slot grant only.
|
|
19
|
+
*/
|
|
20
|
+
| 'deployment:import-legacy'
|
|
16
21
|
| 'deployment:promote-route'
|
|
17
22
|
| 'deployment:rollback-image'
|
|
18
23
|
| 'deployment:rollback-route'
|
|
@@ -53,6 +58,7 @@ const serviceDeploymentCapabilities = new Set<TServiceDeploymentCapability>([
|
|
|
53
58
|
'deployment:adopt-existing',
|
|
54
59
|
'deployment:status',
|
|
55
60
|
'deployment:create-greenfield',
|
|
61
|
+
'deployment:import-legacy',
|
|
56
62
|
'deployment:promote-route',
|
|
57
63
|
'deployment:rollback-image',
|
|
58
64
|
'deployment:rollback-route',
|
|
@@ -106,6 +112,10 @@ export const validateServiceDeploymentGrant = (
|
|
|
106
112
|
&& capabilities.includes('deployment:adopt-existing')) {
|
|
107
113
|
errors.push('deployment:adopt-existing requires an existing-service grant');
|
|
108
114
|
}
|
|
115
|
+
if (capabilities && grant.scope !== 'organization-service-slot'
|
|
116
|
+
&& capabilities.includes('deployment:import-legacy')) {
|
|
117
|
+
errors.push('deployment:import-legacy requires an organization-service-slot grant');
|
|
118
|
+
}
|
|
109
119
|
return errors;
|
|
110
120
|
};
|
|
111
121
|
|