@mark1russell7/kind 0.2.2 → 0.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/README.md +37 -1
- package/build/index.d.ts +126 -5
- package/build/index.js +376 -36
- package/build/index.js.map +1 -1
- package/build/{judge-reWPMVr4.js → judge-CRMYoEOw.js} +90 -20
- package/build/judge-CRMYoEOw.js.map +1 -0
- package/build/{judge-Dwa6MqP4.d.ts → judge-DWF4QdZ8.d.ts} +74 -9
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/agent.mjs +102 -2
- package/image/watch.mjs +50 -4
- package/package.json +3 -1
- package/src/eds.ts +40 -0
- package/build/judge-reWPMVr4.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as stoppingOf, A as middle, B as kernelCounts, C as propagationOf, D as unitOf, E as reusesReset, F as deletionOf, G as requestsOf, H as podsAt, I as dropOf, J as signalOf, K as restartsAt, L as emptyOf, M as backendsAt, N as byClients, O as RELATIVE, P as byNode, Q as slicesOf, R as failuresOf, S as observationEnd, T as referenceOf, U as podsOf, V as outcomesOf, W as recordsOf, X as sliceDropOf, Y as signalsOf, Z as sliceEmptyOf, _ as checkRun, a as calibrationOf, at as REDIS, b as judgeKind, c as predictionText, ct as seconds, d as FORMAT_V1, et as transitionOf, f as judge, g as LAST_SLACK, h as KIND_NOISE, i as readRecord, it as PORT, j as quantileOf, k as medianInterval, l as timeSpread, lt as secondsOf, m as upgradeCell, n as kindCellRecordSchema, nt as HEALTH_PORT, o as cellsTable, ot as SERVER, p as upgrade, q as rulesOf, r as kindCellRecordV1Schema, rt as LOG_LABEL, s as medianText, st as bandOf, t as STAGES, tt as CLIENT, u as FORMAT, ut as showBand, v as deletionsOf, w as propagationSource, x as observationAfter, y as factsOf, z as intakeOf } from "./judge-CRMYoEOw.js";
|
|
2
2
|
import { valueIn } from "@mark1russell7/quantity";
|
|
3
3
|
import { execFile, execFileSync } from "node:child_process";
|
|
4
4
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { gunzipSync } from "node:zlib";
|
|
9
10
|
import { failSpan, lint, service } from "@mark1russell7/service";
|
|
10
11
|
import { createRequire } from "node:module";
|
|
11
12
|
import { tmpdir } from "node:os";
|
|
@@ -227,6 +228,57 @@ const KUBECTL = {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* This release of istioctl is 1.31.3, from 2026-10-22. It holds the charts of its own version, thus the
|
|
233
|
+
* lab needs no other tool to install Istio. The sums are from the `.sha256` assets of the release. The
|
|
234
|
+
* release has a zip file for Windows, and the lab reads only a gzipped tar file. Thus Windows has no asset.
|
|
235
|
+
*/
|
|
236
|
+
const ISTIOCTL = {
|
|
237
|
+
name: "istioctl",
|
|
238
|
+
version: "1.31.3",
|
|
239
|
+
entry: "istioctl",
|
|
240
|
+
assets: {
|
|
241
|
+
"linux-x64": {
|
|
242
|
+
url: "https://github.com/istio/istio/releases/download/1.31.3/istioctl-1.31.3-linux-amd64.tar.gz",
|
|
243
|
+
sha256: "382c3004ce2e9c5ed3064b092ae0da7f6bbe30c766a1be282f45725add004860"
|
|
244
|
+
},
|
|
245
|
+
"linux-arm64": {
|
|
246
|
+
url: "https://github.com/istio/istio/releases/download/1.31.3/istioctl-1.31.3-linux-arm64.tar.gz",
|
|
247
|
+
sha256: "7476801623dbb900073cb1dba6a23b6a15c564b7e92b208aed36829315fefff1"
|
|
248
|
+
},
|
|
249
|
+
"darwin-arm64": {
|
|
250
|
+
url: "https://github.com/istio/istio/releases/download/1.31.3/istioctl-1.31.3-osx-arm64.tar.gz",
|
|
251
|
+
sha256: "881c3b443d9e458e8f19feed6ffa187dcfcb03dc032cb56068f7d1d0cd976c82"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* This function gives the bytes of one file of a gzipped tar file. A header of the ustar format has
|
|
257
|
+
* 512 bytes. It has the name at 0, the size in octal at 124, the type at 156 and the prefix of the
|
|
258
|
+
* name at 345. The data follows the header, in blocks of 512 bytes. The match is on the last part of the
|
|
259
|
+
* name. A link, a folder and an extended header are not a file, thus the search skips them.
|
|
260
|
+
*/
|
|
261
|
+
const tarEntry = (gzipped, entry) => {
|
|
262
|
+
const tar = gunzipSync(gzipped);
|
|
263
|
+
const text = (from, length) => {
|
|
264
|
+
const field = tar.subarray(from, from + length);
|
|
265
|
+
const end = field.indexOf(0);
|
|
266
|
+
return field.subarray(0, end === -1 ? field.length : end).toString("latin1");
|
|
267
|
+
};
|
|
268
|
+
let at = 0;
|
|
269
|
+
while (at + 512 <= tar.length) {
|
|
270
|
+
const name = text(at, 100);
|
|
271
|
+
if (name === "") break;
|
|
272
|
+
const size = Number.parseInt(text(at + 124, 12).trim() || "0", 8);
|
|
273
|
+
if (!Number.isFinite(size)) throw new Error(`the tar entry ${name} has no size in octal`);
|
|
274
|
+
const type = String.fromCharCode(tar[at + 156] ?? 0);
|
|
275
|
+
const prefix = text(at + 345, 155);
|
|
276
|
+
const full = prefix === "" ? name : `${prefix}/${name}`;
|
|
277
|
+
if ((type === "0" || type === "\0") && full.split("/").at(-1) === entry) return new Uint8Array(tar.subarray(at + 512, at + 512 + size));
|
|
278
|
+
at += 512 + Math.ceil(size / 512) * 512;
|
|
279
|
+
}
|
|
280
|
+
throw new Error(`the tar file has no file ${entry}`);
|
|
281
|
+
};
|
|
230
282
|
/** This folder is the cache of the tools: `.cache/` in this package, which git ignores. `KIND_CACHE` names another folder. */
|
|
231
283
|
const cacheFolder = () => process.env["KIND_CACHE"] ?? fileURLToPath(new URL("../.cache/", import.meta.url));
|
|
232
284
|
/** This function gives the platform key of this process, as the assets name it. */
|
|
@@ -235,29 +287,43 @@ const platformKey = (platform = process.platform, arch = process.arch) => `${pla
|
|
|
235
287
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
236
288
|
/** This function gives the path of a tool in the cache, for a platform. */
|
|
237
289
|
const toolPath = (tool, key = platformKey(), cache = cacheFolder()) => join(cache, "tools", `${tool.name}-${tool.version}-${key}`, key.startsWith("win32") ? `${tool.name}.exe` : tool.name);
|
|
290
|
+
/** This function writes a file through a partial file, thus a stop in the middle leaves no file at the path. */
|
|
291
|
+
const writeWhole = (path, bytes, executable) => {
|
|
292
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
293
|
+
const partial = `${path}.partial`;
|
|
294
|
+
writeFileSync(partial, bytes);
|
|
295
|
+
if (executable) chmodSync(partial, 493);
|
|
296
|
+
renameSync(partial, path);
|
|
297
|
+
};
|
|
238
298
|
/**
|
|
239
299
|
* This function gives the path of a tool, after a download when the cache does not have it. It
|
|
240
|
-
* refuses bytes whose sha256 is not the pinned value, from the cache or from the download.
|
|
300
|
+
* refuses bytes whose sha256 is not the pinned value, from the cache or from the download. For a
|
|
301
|
+
* tool in a tar file, the cache keeps the tar file beside the tool. Each use examines the tar file,
|
|
302
|
+
* and writes the tool again when its bytes are not those of the entry.
|
|
241
303
|
*/
|
|
242
304
|
const ensureTool = async (tool, key = platformKey(), cache = cacheFolder()) => {
|
|
243
305
|
const asset = tool.assets[key];
|
|
244
306
|
if (asset === void 0) throw new Error(`${tool.name} ${tool.version} has no pinned asset for ${key}`);
|
|
245
307
|
const path = toolPath(tool, key, cache);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
308
|
+
const executable = !key.startsWith("win32");
|
|
309
|
+
const kept = tool.entry === void 0 ? path : `${path}.tar.gz`;
|
|
310
|
+
let bytes;
|
|
311
|
+
if (existsSync(kept)) {
|
|
312
|
+
bytes = readFileSync(kept);
|
|
313
|
+
const found = sha256(bytes);
|
|
314
|
+
if (found !== asset.sha256) throw new Error(`${kept} has the sha256 ${found}, not the pinned ${asset.sha256}`);
|
|
315
|
+
if (tool.entry === void 0) return path;
|
|
316
|
+
} else {
|
|
317
|
+
const response = await fetch(asset.url, { redirect: "follow" });
|
|
318
|
+
if (!response.ok) throw new Error(`the download of ${asset.url} answered ${response.status}`);
|
|
319
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
320
|
+
const found = sha256(bytes);
|
|
321
|
+
if (found !== asset.sha256) throw new Error(`${asset.url} has the sha256 ${found}, not the pinned ${asset.sha256}`);
|
|
322
|
+
writeWhole(kept, bytes, executable && tool.entry === void 0);
|
|
323
|
+
if (tool.entry === void 0) return path;
|
|
250
324
|
}
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
253
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
254
|
-
const found = sha256(bytes);
|
|
255
|
-
if (found !== asset.sha256) throw new Error(`${asset.url} has the sha256 ${found}, not the pinned ${asset.sha256}`);
|
|
256
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
257
|
-
const partial = `${path}.partial`;
|
|
258
|
-
writeFileSync(partial, bytes);
|
|
259
|
-
if (!key.startsWith("win32")) chmodSync(partial, 493);
|
|
260
|
-
renameSync(partial, path);
|
|
325
|
+
const binary = tarEntry(bytes, tool.entry);
|
|
326
|
+
if (!existsSync(path) || sha256(readFileSync(path)) !== sha256(binary)) writeWhole(path, binary, executable);
|
|
261
327
|
return path;
|
|
262
328
|
};
|
|
263
329
|
//#endregion
|
|
@@ -507,6 +573,37 @@ const compose = (name, declaration, parts = {}) => {
|
|
|
507
573
|
return svc;
|
|
508
574
|
};
|
|
509
575
|
//#endregion
|
|
576
|
+
//#region src/eds.ts
|
|
577
|
+
/**
|
|
578
|
+
* This function gives the name of the outbound cluster of a Service port in a sidecar of Istio:
|
|
579
|
+
* `outbound|<port>||<service>.<namespace>.svc.cluster.local`.
|
|
580
|
+
*/
|
|
581
|
+
const outboundCluster = (service, namespace, port) => `outbound|${port}||${service}.${namespace}.svc.cluster.local`;
|
|
582
|
+
/** This function gives the filter of `/clusters` that selects one cluster: a regular expression of RE2 with anchors. */
|
|
583
|
+
const clusterFilter = (cluster) => `^${cluster.replace(/[.|\\^$*+?()[\]{}]/g, (c) => `\\${c}`)}$`;
|
|
584
|
+
/**
|
|
585
|
+
* This function gives the hosts of a cluster from the text of `/clusters`. Each host of the text has
|
|
586
|
+
* a line `<cluster>::<ip:port>::health_flags::<flags>`. The flags are `healthy`, or the names of the
|
|
587
|
+
* flags that the host has (`clusters_handler.cc` and `host_utility.cc` of Envoy v1.39.4).
|
|
588
|
+
*/
|
|
589
|
+
const hostsOf = (text, cluster) => {
|
|
590
|
+
const healthy = [];
|
|
591
|
+
const other = {};
|
|
592
|
+
const prefix = `${cluster}::`;
|
|
593
|
+
for (const line of text.split("\n")) {
|
|
594
|
+
if (!line.startsWith(prefix)) continue;
|
|
595
|
+
const parts = line.slice(prefix.length).split("::");
|
|
596
|
+
if (parts.length !== 3 || parts[1] !== "health_flags") continue;
|
|
597
|
+
const [host, , flags] = parts;
|
|
598
|
+
if (flags.trim() === "healthy") healthy.push(host);
|
|
599
|
+
else other[host] = flags.trim();
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
b: healthy.sort(),
|
|
603
|
+
u: other
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
510
607
|
//#region src/faults.ts
|
|
511
608
|
const needs = (time, what) => {
|
|
512
609
|
if (time === void 0) throw new Error(`the trigger needs ${what}`);
|
|
@@ -818,7 +915,8 @@ const FILES = [
|
|
|
818
915
|
["compose.ts", "src/compose.ts"],
|
|
819
916
|
["backends.ts", "src/backends.ts"],
|
|
820
917
|
["conntrack.ts", "src/conntrack.ts"],
|
|
821
|
-
["drops.ts", "src/drops.ts"]
|
|
918
|
+
["drops.ts", "src/drops.ts"],
|
|
919
|
+
["eds.ts", "src/eds.ts"]
|
|
822
920
|
];
|
|
823
921
|
const shell = process.platform === "win32";
|
|
824
922
|
/** This function gives each file of a staged context, with its path in the context, in a stable sequence. */
|
|
@@ -912,6 +1010,132 @@ const labImage = () => {
|
|
|
912
1010
|
return built;
|
|
913
1011
|
};
|
|
914
1012
|
//#endregion
|
|
1013
|
+
//#region src/istio.ts
|
|
1014
|
+
/**
|
|
1015
|
+
* This module installs Istio 1.31.3 in a kind cluster with istioctl, from images pinned by digest.
|
|
1016
|
+
* The release notes of 1.31.2 and 1.31.3 change no default that a cell of T15 reads. Each value
|
|
1017
|
+
* of the install that is not a default of Istio is in `istioOperator`, with its reason.
|
|
1018
|
+
*/
|
|
1019
|
+
/** This version is the release of Istio of the lab: the tag of istioctl and of each image. */
|
|
1020
|
+
const ISTIO_VERSION = "1.31.3";
|
|
1021
|
+
/**
|
|
1022
|
+
* These are the images of the flavor `sidecar`, pinned by the digest of their index on Docker Hub
|
|
1023
|
+
* (read on 2026-10-24). `gcr.io/istio-release` has no final tag of 1.31. The lab pulls each one by
|
|
1024
|
+
* its digest, tags it with that digest (`localPin`), and loads it into each node. Thus no node pulls
|
|
1025
|
+
* from a registry.
|
|
1026
|
+
*/
|
|
1027
|
+
const ISTIO_IMAGES = {
|
|
1028
|
+
pilot: "istio/pilot:1.31.3@sha256:6ad1e816c4d4767ba3f30a6286a96d121851ddb23d33e20736758acb5f3f4ea9",
|
|
1029
|
+
proxy: "istio/proxyv2:1.31.3@sha256:eb74eacea98d3fb1e239ea84016085155478586a554158676b9b67d960151084"
|
|
1030
|
+
};
|
|
1031
|
+
/**
|
|
1032
|
+
* This format is the JSON access log of each sidecar. Each field is a command of the formatter of
|
|
1033
|
+
* Envoy v1.39.4. The fields give the status, the response flags and their details, and the gRPC
|
|
1034
|
+
* status as a number. They also give the tries of the router, the upstream host and cluster, and
|
|
1035
|
+
* the reasons of a failed connection. The watcher reads `code`, `flags`, `grpc` and `attempts`
|
|
1036
|
+
* (`image/watch.mjs`).
|
|
1037
|
+
*/
|
|
1038
|
+
const ACCESS_LOG_FORMAT = {
|
|
1039
|
+
start: "%START_TIME%",
|
|
1040
|
+
code: "%RESPONSE_CODE%",
|
|
1041
|
+
flags: "%RESPONSE_FLAGS%",
|
|
1042
|
+
details: "%RESPONSE_CODE_DETAILS%",
|
|
1043
|
+
grpc: "%GRPC_STATUS_NUMBER%",
|
|
1044
|
+
attempts: "%UPSTREAM_REQUEST_ATTEMPT_COUNT%",
|
|
1045
|
+
host: "%UPSTREAM_HOST%",
|
|
1046
|
+
cluster: "%UPSTREAM_CLUSTER%",
|
|
1047
|
+
local: "%DOWNSTREAM_LOCAL_ADDRESS%",
|
|
1048
|
+
remote: "%DOWNSTREAM_REMOTE_ADDRESS%",
|
|
1049
|
+
duration: "%DURATION%",
|
|
1050
|
+
failure: "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
|
|
1051
|
+
termination: "%CONNECTION_TERMINATION_DETAILS%"
|
|
1052
|
+
};
|
|
1053
|
+
/**
|
|
1054
|
+
* This function gives the IstioOperator of a flavor. The values that are not the defaults of Istio:
|
|
1055
|
+
* - The profile `minimal` is istiod only. The profile `default` also has an ingress gateway, which no cell uses.
|
|
1056
|
+
* - Each image is a full reference, thus the charts use it as it is (`contains "/"` in the
|
|
1057
|
+
* templates of istiod and of the injector). The init container of the injector has the image of the proxy too (`proxy_init`).
|
|
1058
|
+
* - `imagePullPolicy: Never`: each image is on each node. An image that the lab did not load fails the install at once.
|
|
1059
|
+
* - istiod requests 256 MiB, not 2 GiB, and has one replica with no autoscaler. Each node of kind
|
|
1060
|
+
* reports the whole runner, and no cell scales istiod.
|
|
1061
|
+
* - The access log of each sidecar goes to its output as JSON (`ACCESS_LOG_FORMAT`).
|
|
1062
|
+
*
|
|
1063
|
+
* The flavor `ambient` comes in a later step, thus it is refused.
|
|
1064
|
+
*/
|
|
1065
|
+
const istioOperator = (flavor, images) => {
|
|
1066
|
+
if (flavor !== "sidecar") throw new Error(`the lab installs the mesh flavor sidecar only, not ${flavor}`);
|
|
1067
|
+
return {
|
|
1068
|
+
apiVersion: "install.istio.io/v1alpha1",
|
|
1069
|
+
kind: "IstioOperator",
|
|
1070
|
+
metadata: {
|
|
1071
|
+
name: "lab",
|
|
1072
|
+
namespace: "istio-system"
|
|
1073
|
+
},
|
|
1074
|
+
spec: {
|
|
1075
|
+
profile: "minimal",
|
|
1076
|
+
hub: "docker.io/istio",
|
|
1077
|
+
tag: ISTIO_VERSION,
|
|
1078
|
+
meshConfig: {
|
|
1079
|
+
accessLogFile: "/dev/stdout",
|
|
1080
|
+
accessLogEncoding: "JSON",
|
|
1081
|
+
accessLogFormat: JSON.stringify(ACCESS_LOG_FORMAT)
|
|
1082
|
+
},
|
|
1083
|
+
values: {
|
|
1084
|
+
global: {
|
|
1085
|
+
imagePullPolicy: "Never",
|
|
1086
|
+
proxy: { image: images.proxy },
|
|
1087
|
+
proxy_init: { image: images.proxy }
|
|
1088
|
+
},
|
|
1089
|
+
pilot: {
|
|
1090
|
+
image: images.pilot,
|
|
1091
|
+
autoscaleEnabled: false,
|
|
1092
|
+
replicaCount: 1,
|
|
1093
|
+
resources: { requests: {
|
|
1094
|
+
cpu: "500m",
|
|
1095
|
+
memory: "256Mi"
|
|
1096
|
+
} }
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
};
|
|
1102
|
+
/**
|
|
1103
|
+
* This function installs the mesh of a flavor in a cluster with `istioctl install` and the operator
|
|
1104
|
+
* of the flavor. Then it waits for istiod. The kubeconfig of the cluster goes to istioctl.
|
|
1105
|
+
*/
|
|
1106
|
+
const installMesh = async (cluster, flavor, images) => {
|
|
1107
|
+
const istioctl = await ensureTool(ISTIOCTL);
|
|
1108
|
+
const file = join(cluster.dir, `istio-${flavor}.json`);
|
|
1109
|
+
writeFileSync(file, `${JSON.stringify(istioOperator(flavor, images), null, 2)}\n`);
|
|
1110
|
+
await command(istioctl, [
|
|
1111
|
+
"install",
|
|
1112
|
+
"--kubeconfig",
|
|
1113
|
+
cluster.kubeconfig,
|
|
1114
|
+
"-y",
|
|
1115
|
+
"-f",
|
|
1116
|
+
file,
|
|
1117
|
+
"--readiness-timeout",
|
|
1118
|
+
"10m0s"
|
|
1119
|
+
], { timeout: 9e5 });
|
|
1120
|
+
await cluster.kubectl([
|
|
1121
|
+
"-n",
|
|
1122
|
+
"istio-system",
|
|
1123
|
+
"rollout",
|
|
1124
|
+
"status",
|
|
1125
|
+
"deployment/istiod",
|
|
1126
|
+
"--timeout=300s"
|
|
1127
|
+
]);
|
|
1128
|
+
};
|
|
1129
|
+
/** This function gives the key of the mesh of a cell for its cluster: its flavor, or `none`. */
|
|
1130
|
+
const meshKey = (cell) => cell.mesh?.flavor ?? "none";
|
|
1131
|
+
/** This function gives the outbound cluster of the server of a cell in each client sidecar: the port 80 of its Service. */
|
|
1132
|
+
const serverCluster = (namespace) => outboundCluster("lab", namespace, 80);
|
|
1133
|
+
/** This time is the interval of the EDS poll of each client sidecar, the same as the poll of the rules. */
|
|
1134
|
+
const EDS_EVERY = {
|
|
1135
|
+
value: 50,
|
|
1136
|
+
unit: "ms"
|
|
1137
|
+
};
|
|
1138
|
+
//#endregion
|
|
915
1139
|
//#region src/conntrack.ts
|
|
916
1140
|
/** This function gives the count of entries that `conntrack -D` deleted, from its last line: "N flow entries have been deleted". */
|
|
917
1141
|
const deletedOf = (text) => {
|
|
@@ -1093,6 +1317,29 @@ const openKindLab = async (cluster, image) => {
|
|
|
1093
1317
|
`printf '%s' '${file}' > /tmp/rules.json`
|
|
1094
1318
|
]);
|
|
1095
1319
|
},
|
|
1320
|
+
async eds(spec) {
|
|
1321
|
+
for (const [node, { agent }] of sidecars) {
|
|
1322
|
+
const pods = spec === null ? [] : spec.pods.filter((pod) => pod.node === node).map(({ pod, pid }) => ({
|
|
1323
|
+
pod,
|
|
1324
|
+
pid
|
|
1325
|
+
}));
|
|
1326
|
+
const file = spec === null || pods.length === 0 ? null : JSON.stringify({
|
|
1327
|
+
cluster: spec.cluster,
|
|
1328
|
+
every: valueIn(spec.every, "ms"),
|
|
1329
|
+
pods
|
|
1330
|
+
});
|
|
1331
|
+
if (file === null) await output(agent, [
|
|
1332
|
+
"rm",
|
|
1333
|
+
"-f",
|
|
1334
|
+
"/tmp/eds.json"
|
|
1335
|
+
]);
|
|
1336
|
+
else await output(agent, [
|
|
1337
|
+
"sh",
|
|
1338
|
+
"-c",
|
|
1339
|
+
`printf '%s' '${file}' > /tmp/eds.json.partial && mv /tmp/eds.json.partial /tmp/eds.json`
|
|
1340
|
+
]);
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1096
1343
|
async forgetHandshakes(ip, port) {
|
|
1097
1344
|
const out = {};
|
|
1098
1345
|
for (const [node, { agent }] of sidecars) {
|
|
@@ -1251,6 +1498,18 @@ const whole = (amount, what) => {
|
|
|
1251
1498
|
/** This function gives the namespace of a cell: `lab-` and 10 digits of the sha256 of its name. */
|
|
1252
1499
|
const namespaceOf = (cell) => `lab-${createHash("sha256").update(cell.name).digest("hex").slice(0, 10)}`;
|
|
1253
1500
|
/**
|
|
1501
|
+
* This function gives the Namespace object of a cell. In the sidecar mode of a mesh, the label
|
|
1502
|
+
* `istio-injection=enabled` makes the injector of Istio put a sidecar in each pod of the namespace.
|
|
1503
|
+
*/
|
|
1504
|
+
const namespaceObject = (cell, namespace) => ({
|
|
1505
|
+
apiVersion: "v1",
|
|
1506
|
+
kind: "Namespace",
|
|
1507
|
+
metadata: {
|
|
1508
|
+
name: namespace,
|
|
1509
|
+
...cell.mesh?.mode === "sidecar" ? { labels: { "istio-injection": "enabled" } } : {}
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
/**
|
|
1254
1513
|
* This function gives a probe of the pod, with each field of the cell. The probe of the service is an
|
|
1255
1514
|
* HTTP GET of its health server (`path`). The probe of grpc-go (`path` null) is the gRPC probe of the
|
|
1256
1515
|
* kubelet. It is a Check of the service "" on the port of the server, and the answer is SERVING until
|
|
@@ -1808,11 +2067,17 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
1808
2067
|
const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1809
2068
|
/** This function tells if a cell has Redis: only a declaration of the service can have it. */
|
|
1810
2069
|
const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !== null;
|
|
1811
|
-
/** This
|
|
2070
|
+
/** This function gives the key of the cluster of a cell: its kube-proxy mode, its node count, and the flavor of its mesh. */
|
|
2071
|
+
const clusterKey = (cell) => cell.mesh === void 0 ? `${cell.mode}-${cell.workers}` : `${cell.mode}-${cell.workers}-${cell.mesh.flavor}`;
|
|
2072
|
+
/**
|
|
2073
|
+
* This tracker keeps the latest state of the cell from the lines: each pod, each slice and each
|
|
2074
|
+
* client. It also has, for each node, the backends of its rules and the hosts of its client sidecar.
|
|
2075
|
+
*/
|
|
1812
2076
|
var Tracker = class {
|
|
1813
2077
|
pods = /* @__PURE__ */ new Map();
|
|
1814
2078
|
slices = /* @__PURE__ */ new Map();
|
|
1815
2079
|
backends = /* @__PURE__ */ new Map();
|
|
2080
|
+
hosts = /* @__PURE__ */ new Map();
|
|
1816
2081
|
clients = /* @__PURE__ */ new Map();
|
|
1817
2082
|
/** This map is the backend of the last answer of each client, by client. */
|
|
1818
2083
|
answers = /* @__PURE__ */ new Map();
|
|
@@ -1827,6 +2092,7 @@ var Tracker = class {
|
|
|
1827
2092
|
if (line["type"] === "DELETED") this.slices.delete(String(line["name"]));
|
|
1828
2093
|
else this.slices.set(String(line["name"]), line);
|
|
1829
2094
|
} else if (line.ev === "rules") this.backends.set(String(line["node"]), line["b"]);
|
|
2095
|
+
else if (line.ev === "eds") this.hosts.set(String(line["node"]), line["b"]);
|
|
1830
2096
|
else if (line.ev === "req") {
|
|
1831
2097
|
const request = line;
|
|
1832
2098
|
const client = this.clients.get(request.p) ?? {
|
|
@@ -1861,6 +2127,9 @@ const unsteady = (ctx) => {
|
|
|
1861
2127
|
if (ready.join() !== ips.join()) return `the EndpointSlice has ${ready.join(" ")}, not ${ips.join(" ")}`;
|
|
1862
2128
|
const expected = ips.map((ip) => `${ip}:8080`).sort().join();
|
|
1863
2129
|
for (const node of ctx.cluster.nodes) if ((tracker.backends.get(node) ?? []).join() !== expected) return `the rules of ${node} have ${(tracker.backends.get(node) ?? []).join(" ")}`;
|
|
2130
|
+
if (cell.mesh?.mode === "sidecar") {
|
|
2131
|
+
for (const node of ctx.cluster.nodes) if ((tracker.hosts.get(node) ?? []).join() !== expected) return `the client sidecar of ${node} has ${(tracker.hosts.get(node) ?? []).join(" ") || "no host"}`;
|
|
2132
|
+
}
|
|
1864
2133
|
const nodes = new Set([...tracker.clients.values()].map((client) => client.node));
|
|
1865
2134
|
if (nodes.size !== ctx.cluster.nodes.length) return `clients answer on ${nodes.size} nodes`;
|
|
1866
2135
|
for (const [pod, client] of tracker.clients) {
|
|
@@ -1923,11 +2192,7 @@ const quiet = async (ctx, ours) => {
|
|
|
1923
2192
|
* the backoff of grpc-js longer, up to 120 s.
|
|
1924
2193
|
*/
|
|
1925
2194
|
const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
1926
|
-
await cluster.apply([
|
|
1927
|
-
apiVersion: "v1",
|
|
1928
|
-
kind: "Namespace",
|
|
1929
|
-
metadata: { name: namespace }
|
|
1930
|
-
}]);
|
|
2195
|
+
await cluster.apply([namespaceObject(cell, namespace)]);
|
|
1931
2196
|
await lab.watch([namespace]);
|
|
1932
2197
|
if (hasRedis(cell)) {
|
|
1933
2198
|
await cluster.apply(redisObjects(namespace, images.redis));
|
|
@@ -1979,8 +2244,40 @@ const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
|
1979
2244
|
port: 80,
|
|
1980
2245
|
every: RULES_EVERY
|
|
1981
2246
|
});
|
|
2247
|
+
if (cell.mesh?.mode === "sidecar") await pollEndpoints(lab, cluster, namespace);
|
|
1982
2248
|
return clusterIp;
|
|
1983
2249
|
};
|
|
2250
|
+
/**
|
|
2251
|
+
* This function starts the EDS poll of the client sidecar on each node: the hosts of the server
|
|
2252
|
+
* cluster, each 50 ms. The agent enters the network namespace of the client pod through the PID of
|
|
2253
|
+
* its client process. The sidecar shares that namespace.
|
|
2254
|
+
*/
|
|
2255
|
+
const pollEndpoints = async (lab, cluster, namespace) => {
|
|
2256
|
+
const listed = JSON.parse(await cluster.kubectl([
|
|
2257
|
+
"-n",
|
|
2258
|
+
namespace,
|
|
2259
|
+
"get",
|
|
2260
|
+
"pods",
|
|
2261
|
+
"-l",
|
|
2262
|
+
`app=${CLIENT}`,
|
|
2263
|
+
"-o",
|
|
2264
|
+
"json"
|
|
2265
|
+
]));
|
|
2266
|
+
const pods = [];
|
|
2267
|
+
for (const item of listed.items.filter((each) => each.metadata.deletionTimestamp === void 0)) {
|
|
2268
|
+
const node = item.spec.nodeName ?? "";
|
|
2269
|
+
pods.push({
|
|
2270
|
+
node,
|
|
2271
|
+
pod: item.metadata.name,
|
|
2272
|
+
pid: await lab.pidOf(node, namespace, item.metadata.name, CLIENT)
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
await lab.eds({
|
|
2276
|
+
cluster: serverCluster(namespace),
|
|
2277
|
+
every: EDS_EVERY,
|
|
2278
|
+
pods
|
|
2279
|
+
});
|
|
2280
|
+
};
|
|
1984
2281
|
/** This function starts the prober again when the pods of the cell changed: it watches the namespace of each pod. */
|
|
1985
2282
|
const probeThePods = async (ctx) => {
|
|
1986
2283
|
const pods = [...ctx.tracker.pods.values()].filter((pod) => !pod.del && pod.node !== null && pod.cs.every((status) => status.st === "running"));
|
|
@@ -2143,12 +2440,18 @@ const over = (ctx, run, lines) => {
|
|
|
2143
2440
|
"node-drain",
|
|
2144
2441
|
"scale-in",
|
|
2145
2442
|
"freeze-process"
|
|
2146
|
-
].includes(kind) && ctx.cell.template === "T07") return kind === "scale-in" || kind === "node-drain" ? run.replicas.some((replica) => !ctx.tracker.pods.has(replica.pod)) : !ctx.tracker.pods.has(run.target.pod);
|
|
2443
|
+
].includes(kind) && (ctx.cell.template === "T07" || ctx.cell.template === "T15")) return kind === "scale-in" || kind === "node-drain" ? run.replicas.some((replica) => !ctx.tracker.pods.has(replica.pod)) : !ctx.tracker.pods.has(run.target.pod);
|
|
2147
2444
|
if (kind === "rolling-update" || kind === "rollout") return run.replicas.every((replica) => !ctx.tracker.pods.has(replica.pod));
|
|
2148
2445
|
if (kind === "app-stall") return lines.some((line) => line.ev === "stall" && line["phase"] === "end");
|
|
2149
2446
|
return true;
|
|
2150
2447
|
};
|
|
2151
|
-
/** This function
|
|
2448
|
+
/** This function gives the replica that the trigger stopped. For a scale-in or a drain, the controller chooses it. */
|
|
2449
|
+
const stoppedOf = (cell, replicas, target, lines) => deletes(cell) ? replicas.find((replica) => lines.some((line) => line.ev === "pod" && line["name"] === replica.pod && line["del"] === true)) ?? target : target;
|
|
2450
|
+
/**
|
|
2451
|
+
* This function does one run of a cell: at rest, the trigger, the heal, then the observation. The
|
|
2452
|
+
* observation lasts until the cell is at rest again, and until each predicted window can close
|
|
2453
|
+
* (`observationEnd`).
|
|
2454
|
+
*/
|
|
2152
2455
|
const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
2153
2456
|
const { lab, cell, cluster, namespace } = ctx;
|
|
2154
2457
|
const plan = planOf(cell);
|
|
@@ -2220,6 +2523,21 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
2220
2523
|
replicas,
|
|
2221
2524
|
restarts
|
|
2222
2525
|
}, lab.lines)) await sleep(250);
|
|
2526
|
+
const until = observationEnd(cell, {
|
|
2527
|
+
start,
|
|
2528
|
+
acts: {
|
|
2529
|
+
fault,
|
|
2530
|
+
heal,
|
|
2531
|
+
resume
|
|
2532
|
+
},
|
|
2533
|
+
target: stoppedOf(cell, replicas, target, lab.lines),
|
|
2534
|
+
replicas,
|
|
2535
|
+
lines: lab.lines
|
|
2536
|
+
});
|
|
2537
|
+
if (until !== null) {
|
|
2538
|
+
const wait = Math.min(until - await lab.now(), limit - Date.now());
|
|
2539
|
+
if (wait > 0) await sleep(wait);
|
|
2540
|
+
}
|
|
2223
2541
|
const end = await lab.now();
|
|
2224
2542
|
const timedOut = Date.now() >= limit;
|
|
2225
2543
|
for (const step of plan.restore) try {
|
|
@@ -2230,7 +2548,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
2230
2548
|
ctx.expected = cell.pod.replicas;
|
|
2231
2549
|
for (const note of notes) ctx.log(` ${note}`);
|
|
2232
2550
|
const lines = lab.take();
|
|
2233
|
-
const stopped =
|
|
2551
|
+
const stopped = stoppedOf(cell, replicas, target, lines);
|
|
2234
2552
|
if (lines.some((line) => line.ev === "pod" && line["cs"].some((status) => status.rc > 0))) {
|
|
2235
2553
|
await cluster.kubectl([
|
|
2236
2554
|
"-n",
|
|
@@ -2327,17 +2645,22 @@ const runKindCells = async (cells, options) => {
|
|
|
2327
2645
|
const images = {
|
|
2328
2646
|
lab: image,
|
|
2329
2647
|
redis: redisImage,
|
|
2330
|
-
grpc: grpcImage
|
|
2648
|
+
grpc: grpcImage,
|
|
2649
|
+
mesh: cells.some((cell) => cell.mesh !== void 0) ? {
|
|
2650
|
+
pilot: await localPin(ISTIO_IMAGES.pilot),
|
|
2651
|
+
proxy: await localPin(ISTIO_IMAGES.proxy)
|
|
2652
|
+
} : void 0
|
|
2331
2653
|
};
|
|
2332
2654
|
const groups = /* @__PURE__ */ new Map();
|
|
2333
|
-
for (const cell of cells) groups.set(
|
|
2655
|
+
for (const cell of cells) groups.set(clusterKey(cell), [...groups.get(clusterKey(cell)) ?? [], cell]);
|
|
2334
2656
|
const results = [];
|
|
2335
2657
|
for (const [key, group] of groups) {
|
|
2336
2658
|
const partial = /* @__PURE__ */ new Map();
|
|
2337
2659
|
for (let env = 0; env < environments; env += 1) {
|
|
2338
2660
|
const s = scenario(`lab-${key}-${env}`, group[0].mode, group[0].workers);
|
|
2339
2661
|
const dir = join(options.work ?? join(tmpdir(), "systems-kind-work"), s.name);
|
|
2340
|
-
|
|
2662
|
+
const mesh = group[0].mesh;
|
|
2663
|
+
options.log(`cluster ${s.name}: ${s.kubeProxy.mode}, ${s.workers} workers${mesh === void 0 ? "" : `, Istio ${mesh.flavor}`}`);
|
|
2341
2664
|
const cluster = await createCluster(s, dir);
|
|
2342
2665
|
let lab = null;
|
|
2343
2666
|
let loader = null;
|
|
@@ -2345,6 +2668,13 @@ const runKindCells = async (cells, options) => {
|
|
|
2345
2668
|
await cluster.load(image);
|
|
2346
2669
|
await cluster.load(redisImage);
|
|
2347
2670
|
if (grpcImage !== void 0 && group.some((cell) => stackOf(cell) === "grpc")) await cluster.load(grpcImage);
|
|
2671
|
+
if (mesh !== void 0) {
|
|
2672
|
+
if (images.mesh === void 0) throw new Error(`${group[0].name}: the run has no images of Istio`);
|
|
2673
|
+
for (const each of [images.mesh.pilot, images.mesh.proxy]) await cluster.load(each);
|
|
2674
|
+
const began = Date.now();
|
|
2675
|
+
await installMesh(cluster, mesh.flavor, images.mesh);
|
|
2676
|
+
options.log(` Istio ${mesh.flavor} installed in ${Math.round((Date.now() - began) / 100) / 10} s`);
|
|
2677
|
+
}
|
|
2348
2678
|
lab = await openKindLab(cluster, image);
|
|
2349
2679
|
const { GenericContainer } = await import("testcontainers");
|
|
2350
2680
|
const Shared = class extends GenericContainer {
|
|
@@ -2396,7 +2726,11 @@ const runKindCells = async (cells, options) => {
|
|
|
2396
2726
|
images: {
|
|
2397
2727
|
lab: image,
|
|
2398
2728
|
redis: redisImage,
|
|
2399
|
-
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {}
|
|
2729
|
+
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
|
|
2730
|
+
...cell.mesh === void 0 ? {} : {
|
|
2731
|
+
"istio-pilot": ISTIO_IMAGES.pilot,
|
|
2732
|
+
"istio-proxy": ISTIO_IMAGES.proxy
|
|
2733
|
+
}
|
|
2400
2734
|
}
|
|
2401
2735
|
});
|
|
2402
2736
|
entry.loads.push(...loads);
|
|
@@ -2411,6 +2745,7 @@ const runKindCells = async (cells, options) => {
|
|
|
2411
2745
|
} finally {
|
|
2412
2746
|
off();
|
|
2413
2747
|
await lab.rules(null).catch(() => void 0);
|
|
2748
|
+
await lab.eds(null).catch(() => void 0);
|
|
2414
2749
|
await lab.stopProbe();
|
|
2415
2750
|
await cluster.kubectl([
|
|
2416
2751
|
"delete",
|
|
@@ -2449,14 +2784,17 @@ const runKindCells = async (cells, options) => {
|
|
|
2449
2784
|
/** This function gives the estimate of a cell in seconds, for the arithmetic of the shards. */
|
|
2450
2785
|
const estimate = (cell, environments = 2) => {
|
|
2451
2786
|
const hold = cell.trigger.hold === void 0 ? 0 : valueIn(cell.trigger.hold, "s");
|
|
2452
|
-
const
|
|
2787
|
+
const trigger = {
|
|
2453
2788
|
"liveness-restart": 100,
|
|
2454
2789
|
crash: 30,
|
|
2455
2790
|
"app-stall": hold + 40,
|
|
2456
2791
|
"cpu-throttle": hold + 40,
|
|
2457
2792
|
rollout: hold + 50
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2793
|
+
};
|
|
2794
|
+
const after = observationAfter(cell);
|
|
2795
|
+
const observed = after === null ? 0 : hold + valueIn(after, "s");
|
|
2796
|
+
const each = 25 + Math.max(observed, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
|
|
2797
|
+
return (cell.mesh === void 0 ? 60 : 180) * environments + (cell.runs + environments) * each;
|
|
2460
2798
|
};
|
|
2461
2799
|
/**
|
|
2462
2800
|
* This function gives an estimate of the time of a cell: its setup, then each run and each
|
|
@@ -2469,11 +2807,13 @@ const estimatedTime = (cell, environments = 2) => ({
|
|
|
2469
2807
|
});
|
|
2470
2808
|
/**
|
|
2471
2809
|
* This function gives the share of a shard of a catalog: cells in sequence. Thus the cells of one
|
|
2472
|
-
* mode stay together and share clusters. Each shard gets an equal share of the
|
|
2810
|
+
* mode and one mesh flavor stay together and share clusters. Each shard gets an equal share of the
|
|
2811
|
+
* estimated time. In one mode, the cells with no mesh come first, in the sequence of the catalog.
|
|
2473
2812
|
*/
|
|
2474
2813
|
const shardOf = (cells, index, count) => {
|
|
2475
2814
|
if (!(Number.isInteger(index) && Number.isInteger(count) && count >= 1 && index >= 0 && index < count)) throw new RangeError(`shard ${index} of ${count} is not a shard`);
|
|
2476
|
-
const
|
|
2815
|
+
const rank = (cell) => `${cell.mode}/${meshKey(cell) === "none" ? "" : meshKey(cell)}`;
|
|
2816
|
+
const ordered = [...cells].sort((a, b) => rank(a) === rank(b) ? 0 : rank(a) < rank(b) ? -1 : 1);
|
|
2477
2817
|
const total = ordered.reduce((sum, cell) => sum + estimate(cell), 0);
|
|
2478
2818
|
const out = [];
|
|
2479
2819
|
let before = 0;
|
|
@@ -2485,6 +2825,6 @@ const shardOf = (cells, index, count) => {
|
|
|
2485
2825
|
return out;
|
|
2486
2826
|
};
|
|
2487
2827
|
//#endregion
|
|
2488
|
-
export { CLIENT, CONTROLLER, DOCUMENTED, DRAIN_RULES, FLAG_CHECK, FORMAT, FORMAT_V1, HEALTH_PORT, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, LAST_SLACK, LOG_LABEL, NODE_IMAGES, O04, O09, O10, O16, PORT, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, STAGES, WATCHDOG, backendsAt, backendsOf, bandOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, checkRun, cidrOverlap, cidrRange, clientObjects, compose, createCluster, deletes, deletionOf, deletionWindow, deletionsOf, describeCell, dropOf, emptyOf, ensureTool, estimatedTime, factsOf, failuresOf, goDuration, intakeAfter, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeletBand, labImage, latchBand, lintCell, localPin, medianInterval, medianText, middle, namespaceOf, nodeNames, openKindLab, outcomesOf, pausedWindow, planOf, platformKey, podsAt, podsOf, predictionText, probeOf, propagationOf, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestsOf, restartBand, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverObjects, sha256, shardOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stackOf, stageLabImage, stallRemovalBand, stoppingOf, timeSpread, toolPath, transitionOf, unitOf, upgrade, upgradeCell, verdictsOf, writeResult };
|
|
2828
|
+
export { ACCESS_LOG_FORMAT, CLIENT, CONTROLLER, DOCUMENTED, DRAIN_RULES, EDS_EVERY, FLAG_CHECK, FORMAT, FORMAT_V1, HEALTH_PORT, ISTIOCTL, ISTIO_IMAGES, ISTIO_VERSION, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, LAST_SLACK, LOG_LABEL, NODE_IMAGES, O04, O09, O10, O16, PORT, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, STAGES, WATCHDOG, backendsAt, backendsOf, bandOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, checkRun, cidrOverlap, cidrRange, clientObjects, clusterFilter, compose, createCluster, deletes, deletionOf, deletionWindow, deletionsOf, describeCell, dropOf, emptyOf, ensureTool, estimatedTime, factsOf, failuresOf, goDuration, hostsOf, installMesh, intakeAfter, intakeOf, istioOperator, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeletBand, labImage, latchBand, lintCell, localPin, medianInterval, medianText, meshKey, middle, namespaceObject, namespaceOf, nodeNames, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, pausedWindow, planOf, platformKey, podsAt, podsOf, predictionText, probeOf, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestsOf, restartBand, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverCluster, serverObjects, sha256, shardOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, stackOf, stageLabImage, stallRemovalBand, stoppingOf, tarEntry, timeSpread, toolPath, transitionOf, unitOf, upgrade, upgradeCell, verdictsOf, writeResult };
|
|
2489
2829
|
|
|
2490
2830
|
//# sourceMappingURL=index.js.map
|