@mark1russell7/kind 0.2.3 → 0.4.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 +177 -5
- package/build/index.js +478 -33
- package/build/index.js.map +1 -1
- package/build/{judge-B3CHoiTW.js → judge-Bkbs7VVG.js} +147 -29
- package/build/judge-Bkbs7VVG.js.map +1 -0
- package/build/{judge-CctoUEo8.d.ts → judge-DMuI3aDn.d.ts} +82 -14
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/agent.mjs +133 -2
- package/image/client-grpc.mjs +2 -0
- package/image/watch.mjs +50 -4
- package/package.json +4 -1
- package/src/eds.ts +40 -0
- package/src/lds.ts +31 -0
- package/build/judge-B3CHoiTW.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as slicesOf, A as medianInterval, B as intakeOf, C as propagationOf, D as unitOf, E as reusesReset, F as byNode, G as recordsOf, H as outcomesOf, I as deletionOf, J as rulesOf, K as requestsOf, L as dropOf, M as quantileOf, N as backendsAt, O as updatesOf, P as byClients, Q as sliceEmptyOf, R as emptyOf, S as observationEnd, T as referenceOf, U as podsAt, V as kernelCounts, W as podsOf, X as signalsOf, Y as signalOf, Z as sliceDropOf, _ as checkRun, a as calibrationOf, at as PORT, b as judgeKind, c as predictionText, ct as bandOf, d as FORMAT_V1, dt as showBand, et as stoppingOf, f as judge, g as LAST_SLACK, h as KIND_NOISE, i as readRecord, it as LOG_LABEL, j as middle, k as RELATIVE, l as timeSpread, lt as seconds, m as upgradeCell, n as kindCellRecordSchema, nt as CLIENT, o as cellsTable, ot as REDIS, p as upgrade, q as restartsAt, r as kindCellRecordV1Schema, rt as HEALTH_PORT, s as medianText, st as SERVER, t as STAGES, tt as transitionOf, u as FORMAT, ut as secondsOf, v as deletionsOf, w as propagationSource, x as observationAfter, y as factsOf, z as failuresOf } from "./judge-Bkbs7VVG.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}`);
|
|
@@ -522,6 +619,29 @@ const NODE = "{node}";
|
|
|
522
619
|
const VERSION = "{version}";
|
|
523
620
|
/** This annotation of a pod is its deletion cost for its ReplicaSet: a lower cost goes first at a scale-in. */
|
|
524
621
|
const DELETION_COST = "controller.kubernetes.io/pod-deletion-cost";
|
|
622
|
+
/** This name is the name of the AuthorizationPolicy of the trigger `authorization-policy`. */
|
|
623
|
+
const POLICY = "lab-deny";
|
|
624
|
+
/** This service account is the account of the clients: their pods name no other account. */
|
|
625
|
+
const CLIENT_ACCOUNT = "default";
|
|
626
|
+
/**
|
|
627
|
+
* This function gives the AuthorizationPolicy of the trigger `authorization-policy`. It denies each
|
|
628
|
+
* request to the server pods from the principal of the service account `account`. Istio gives a
|
|
629
|
+
* pod of the mesh the principal `cluster.local/ns/<namespace>/sa/<account>` of its account. The
|
|
630
|
+
* RBAC filter of the server sidecar reads the principal of the peer from its mTLS certificate.
|
|
631
|
+
*/
|
|
632
|
+
const policyObject = (account) => ({
|
|
633
|
+
apiVersion: "security.istio.io/v1",
|
|
634
|
+
kind: "AuthorizationPolicy",
|
|
635
|
+
metadata: {
|
|
636
|
+
name: POLICY,
|
|
637
|
+
namespace: "{namespace}"
|
|
638
|
+
},
|
|
639
|
+
spec: {
|
|
640
|
+
selector: { matchLabels: { app: "lab" } },
|
|
641
|
+
action: "DENY",
|
|
642
|
+
rules: [{ from: [{ source: { principals: [`cluster.local/ns/{namespace}/sa/${account}`] } }] }]
|
|
643
|
+
}
|
|
644
|
+
});
|
|
525
645
|
/** This function gives the fault plan of a cell. */
|
|
526
646
|
const planOf = (cell) => {
|
|
527
647
|
const ns = ["-n", "{namespace}"];
|
|
@@ -784,6 +904,32 @@ const planOf = (cell) => {
|
|
|
784
904
|
restore: []
|
|
785
905
|
};
|
|
786
906
|
}
|
|
907
|
+
case "authorization-policy": return {
|
|
908
|
+
inject: [{
|
|
909
|
+
do: "apply",
|
|
910
|
+
object: policyObject(trigger.account ?? "default")
|
|
911
|
+
}],
|
|
912
|
+
hold: needs(trigger.hold, "a hold"),
|
|
913
|
+
heal: [{
|
|
914
|
+
do: "kubectl",
|
|
915
|
+
args: [
|
|
916
|
+
...ns,
|
|
917
|
+
"delete",
|
|
918
|
+
"authorizationpolicy",
|
|
919
|
+
POLICY
|
|
920
|
+
]
|
|
921
|
+
}],
|
|
922
|
+
restore: [{
|
|
923
|
+
do: "kubectl",
|
|
924
|
+
args: [
|
|
925
|
+
...ns,
|
|
926
|
+
"delete",
|
|
927
|
+
"authorizationpolicy",
|
|
928
|
+
POLICY,
|
|
929
|
+
"--ignore-not-found"
|
|
930
|
+
]
|
|
931
|
+
}]
|
|
932
|
+
};
|
|
787
933
|
}
|
|
788
934
|
};
|
|
789
935
|
/** This function tells if a trigger stops a pod of the service with its grace: a deletion, an eviction, a scale-in or a rollout. */
|
|
@@ -818,7 +964,9 @@ const FILES = [
|
|
|
818
964
|
["compose.ts", "src/compose.ts"],
|
|
819
965
|
["backends.ts", "src/backends.ts"],
|
|
820
966
|
["conntrack.ts", "src/conntrack.ts"],
|
|
821
|
-
["drops.ts", "src/drops.ts"]
|
|
967
|
+
["drops.ts", "src/drops.ts"],
|
|
968
|
+
["eds.ts", "src/eds.ts"],
|
|
969
|
+
["lds.ts", "src/lds.ts"]
|
|
822
970
|
];
|
|
823
971
|
const shell = process.platform === "win32";
|
|
824
972
|
/** This function gives each file of a staged context, with its path in the context, in a stable sequence. */
|
|
@@ -912,6 +1060,166 @@ const labImage = () => {
|
|
|
912
1060
|
return built;
|
|
913
1061
|
};
|
|
914
1062
|
//#endregion
|
|
1063
|
+
//#region src/istio.ts
|
|
1064
|
+
/**
|
|
1065
|
+
* This module installs Istio 1.31.3 in a kind cluster with istioctl, from images pinned by digest.
|
|
1066
|
+
* The release notes of 1.31.2 and 1.31.3 change no default that a cell of T15 reads. Each value
|
|
1067
|
+
* of the install that is not a default of Istio is in `istioOperator`, with its reason.
|
|
1068
|
+
*/
|
|
1069
|
+
/** This version is the release of Istio of the lab: the tag of istioctl and of each image. */
|
|
1070
|
+
const ISTIO_VERSION = "1.31.3";
|
|
1071
|
+
/**
|
|
1072
|
+
* These are the images of the flavor `sidecar`, pinned by the digest of their index on Docker Hub
|
|
1073
|
+
* (read on 2026-10-24). `gcr.io/istio-release` has no final tag of 1.31. The lab pulls each one by
|
|
1074
|
+
* its digest, tags it with that digest (`localPin`), and loads it into each node. Thus no node pulls
|
|
1075
|
+
* from a registry.
|
|
1076
|
+
*/
|
|
1077
|
+
const ISTIO_IMAGES = {
|
|
1078
|
+
pilot: "istio/pilot:1.31.3@sha256:6ad1e816c4d4767ba3f30a6286a96d121851ddb23d33e20736758acb5f3f4ea9",
|
|
1079
|
+
proxy: "istio/proxyv2:1.31.3@sha256:eb74eacea98d3fb1e239ea84016085155478586a554158676b9b67d960151084"
|
|
1080
|
+
};
|
|
1081
|
+
/**
|
|
1082
|
+
* This format is the JSON access log of each sidecar. Each field is a command of the formatter of
|
|
1083
|
+
* Envoy v1.39.4. The fields give the status, the response flags and their details, and the gRPC
|
|
1084
|
+
* status as a number. They also give the tries of the router, the upstream host and cluster, and
|
|
1085
|
+
* the reasons of a failed connection. The watcher reads `code`, `flags`, `grpc` and `attempts`
|
|
1086
|
+
* (`image/watch.mjs`).
|
|
1087
|
+
*/
|
|
1088
|
+
const ACCESS_LOG_FORMAT = {
|
|
1089
|
+
start: "%START_TIME%",
|
|
1090
|
+
code: "%RESPONSE_CODE%",
|
|
1091
|
+
flags: "%RESPONSE_FLAGS%",
|
|
1092
|
+
details: "%RESPONSE_CODE_DETAILS%",
|
|
1093
|
+
grpc: "%GRPC_STATUS_NUMBER%",
|
|
1094
|
+
attempts: "%UPSTREAM_REQUEST_ATTEMPT_COUNT%",
|
|
1095
|
+
host: "%UPSTREAM_HOST%",
|
|
1096
|
+
cluster: "%UPSTREAM_CLUSTER%",
|
|
1097
|
+
local: "%DOWNSTREAM_LOCAL_ADDRESS%",
|
|
1098
|
+
remote: "%DOWNSTREAM_REMOTE_ADDRESS%",
|
|
1099
|
+
duration: "%DURATION%",
|
|
1100
|
+
failure: "%UPSTREAM_TRANSPORT_FAILURE_REASON%",
|
|
1101
|
+
termination: "%CONNECTION_TERMINATION_DETAILS%"
|
|
1102
|
+
};
|
|
1103
|
+
/**
|
|
1104
|
+
* This function gives the IstioOperator of a flavor. The values that are not the defaults of Istio:
|
|
1105
|
+
* - The profile `minimal` is istiod only. The profile `default` also has an ingress gateway, which no cell uses.
|
|
1106
|
+
* - Each image is a full reference, thus the charts use it as it is (`contains "/"` in the
|
|
1107
|
+
* templates of istiod and of the injector). The init container of the injector has the image of the proxy too (`proxy_init`).
|
|
1108
|
+
* - `imagePullPolicy: Never`: each image is on each node. An image that the lab did not load fails the install at once.
|
|
1109
|
+
* - istiod requests 256 MiB, not 2 GiB, and has one replica with no autoscaler. Each node of kind
|
|
1110
|
+
* reports the whole runner, and no cell scales istiod.
|
|
1111
|
+
* - The access log of each sidecar goes to its output as JSON (`ACCESS_LOG_FORMAT`).
|
|
1112
|
+
*
|
|
1113
|
+
* The flavor `ambient` comes in a later step, thus it is refused.
|
|
1114
|
+
*/
|
|
1115
|
+
const istioOperator = (flavor, images) => {
|
|
1116
|
+
if (flavor !== "sidecar") throw new Error(`the lab installs the mesh flavor sidecar only, not ${flavor}`);
|
|
1117
|
+
return {
|
|
1118
|
+
apiVersion: "install.istio.io/v1alpha1",
|
|
1119
|
+
kind: "IstioOperator",
|
|
1120
|
+
metadata: {
|
|
1121
|
+
name: "lab",
|
|
1122
|
+
namespace: "istio-system"
|
|
1123
|
+
},
|
|
1124
|
+
spec: {
|
|
1125
|
+
profile: "minimal",
|
|
1126
|
+
hub: "docker.io/istio",
|
|
1127
|
+
tag: ISTIO_VERSION,
|
|
1128
|
+
meshConfig: {
|
|
1129
|
+
accessLogFile: "/dev/stdout",
|
|
1130
|
+
accessLogEncoding: "JSON",
|
|
1131
|
+
accessLogFormat: JSON.stringify(ACCESS_LOG_FORMAT)
|
|
1132
|
+
},
|
|
1133
|
+
values: {
|
|
1134
|
+
global: {
|
|
1135
|
+
imagePullPolicy: "Never",
|
|
1136
|
+
proxy: { image: images.proxy },
|
|
1137
|
+
proxy_init: { image: images.proxy }
|
|
1138
|
+
},
|
|
1139
|
+
pilot: {
|
|
1140
|
+
image: images.pilot,
|
|
1141
|
+
autoscaleEnabled: false,
|
|
1142
|
+
replicaCount: 1,
|
|
1143
|
+
resources: { requests: {
|
|
1144
|
+
cpu: "500m",
|
|
1145
|
+
memory: "256Mi"
|
|
1146
|
+
} }
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
};
|
|
1152
|
+
/**
|
|
1153
|
+
* This function installs the mesh of a flavor in a cluster with `istioctl install` and the operator
|
|
1154
|
+
* of the flavor. Then it waits for istiod. The kubeconfig of the cluster goes to istioctl.
|
|
1155
|
+
*/
|
|
1156
|
+
const installMesh = async (cluster, flavor, images) => {
|
|
1157
|
+
const istioctl = await ensureTool(ISTIOCTL);
|
|
1158
|
+
const file = join(cluster.dir, `istio-${flavor}.json`);
|
|
1159
|
+
writeFileSync(file, `${JSON.stringify(istioOperator(flavor, images), null, 2)}\n`);
|
|
1160
|
+
await command(istioctl, [
|
|
1161
|
+
"install",
|
|
1162
|
+
"--kubeconfig",
|
|
1163
|
+
cluster.kubeconfig,
|
|
1164
|
+
"-y",
|
|
1165
|
+
"-f",
|
|
1166
|
+
file,
|
|
1167
|
+
"--readiness-timeout",
|
|
1168
|
+
"10m0s"
|
|
1169
|
+
], { timeout: 9e5 });
|
|
1170
|
+
await cluster.kubectl([
|
|
1171
|
+
"-n",
|
|
1172
|
+
"istio-system",
|
|
1173
|
+
"rollout",
|
|
1174
|
+
"status",
|
|
1175
|
+
"deployment/istiod",
|
|
1176
|
+
"--timeout=300s"
|
|
1177
|
+
]);
|
|
1178
|
+
};
|
|
1179
|
+
/** This function gives the key of the mesh of a cell for its cluster: its flavor, or `none`. */
|
|
1180
|
+
const meshKey = (cell) => cell.mesh?.flavor ?? "none";
|
|
1181
|
+
/** This function gives the outbound cluster of the server of a cell in each client sidecar: the port 80 of its Service. */
|
|
1182
|
+
const serverCluster = (namespace) => outboundCluster("lab", namespace, 80);
|
|
1183
|
+
/** This time is the interval of the EDS poll of each client sidecar, the same as the poll of the rules. */
|
|
1184
|
+
const EDS_EVERY = {
|
|
1185
|
+
value: 50,
|
|
1186
|
+
unit: "ms"
|
|
1187
|
+
};
|
|
1188
|
+
/** This time is the interval of the LDS poll of each server sidecar, the same as the poll of EDS. */
|
|
1189
|
+
const LDS_EVERY = {
|
|
1190
|
+
value: 50,
|
|
1191
|
+
unit: "ms"
|
|
1192
|
+
};
|
|
1193
|
+
/** This function tells if the lab polls the LDS updates of the server sidecars of a cell: a policy in a sidecar mesh. */
|
|
1194
|
+
const pollsListeners = (cell) => cell.mesh?.mode === "sidecar" && cell.trigger.kind === "authorization-policy";
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/lds.ts
|
|
1197
|
+
/**
|
|
1198
|
+
* This filter of RE2 selects the two counters of the listener manager of Envoy that an update of LDS
|
|
1199
|
+
* moves (`/stats?filter=`, `source/server/admin/stats_params.cc` of Envoy v1.39.4). An update of a
|
|
1200
|
+
* listener counts `listener_modified`. An update in place, of its filter chains only, also counts
|
|
1201
|
+
* `listener_in_place_updated` (`listener_manager_impl.cc:644`, `:692`).
|
|
1202
|
+
*/
|
|
1203
|
+
const LDS_FILTER = "^listener_manager\\.listener_(in_place_updated|modified)$";
|
|
1204
|
+
/**
|
|
1205
|
+
* This function gives the listener counts of the text of `/stats`, one `name: value` on each line
|
|
1206
|
+
* (`stats_render.cc:15`). It gives null when the text has no counter `listener_modified`.
|
|
1207
|
+
*/
|
|
1208
|
+
const listenerCountsOf = (text) => {
|
|
1209
|
+
let mod = null;
|
|
1210
|
+
let inPlace = 0;
|
|
1211
|
+
for (const line of text.split("\n")) {
|
|
1212
|
+
const match = /^listener_manager\.listener_(in_place_updated|modified): (\d+)$/.exec(line.trim());
|
|
1213
|
+
if (match === null) continue;
|
|
1214
|
+
if (match[1] === "modified") mod = Number(match[2]);
|
|
1215
|
+
else inPlace = Number(match[2]);
|
|
1216
|
+
}
|
|
1217
|
+
return mod === null ? null : {
|
|
1218
|
+
mod,
|
|
1219
|
+
inPlace
|
|
1220
|
+
};
|
|
1221
|
+
};
|
|
1222
|
+
//#endregion
|
|
915
1223
|
//#region src/conntrack.ts
|
|
916
1224
|
/** This function gives the count of entries that `conntrack -D` deleted, from its last line: "N flow entries have been deleted". */
|
|
917
1225
|
const deletedOf = (text) => {
|
|
@@ -1019,6 +1327,33 @@ const openKindLab = async (cluster, image) => {
|
|
|
1019
1327
|
return found;
|
|
1020
1328
|
};
|
|
1021
1329
|
const firstNode = cluster.nodes[0];
|
|
1330
|
+
/**
|
|
1331
|
+
* This function writes the file of the pollers of one kind on each agent: `fields`, the interval
|
|
1332
|
+
* and the pods of its node. A node without a pod, or a spec of null, gets no file.
|
|
1333
|
+
*/
|
|
1334
|
+
const pollersOf = async (kind, fields, spec) => {
|
|
1335
|
+
for (const [node, { agent }] of sidecars) {
|
|
1336
|
+
const pods = spec === null ? [] : spec.pods.filter((pod) => pod.node === node).map(({ pod, pid }) => ({
|
|
1337
|
+
pod,
|
|
1338
|
+
pid
|
|
1339
|
+
}));
|
|
1340
|
+
const file = spec === null || pods.length === 0 ? null : JSON.stringify({
|
|
1341
|
+
...fields,
|
|
1342
|
+
every: valueIn(spec.every, "ms"),
|
|
1343
|
+
pods
|
|
1344
|
+
});
|
|
1345
|
+
if (file === null) await output(agent, [
|
|
1346
|
+
"rm",
|
|
1347
|
+
"-f",
|
|
1348
|
+
`/tmp/${kind}.json`
|
|
1349
|
+
]);
|
|
1350
|
+
else await output(agent, [
|
|
1351
|
+
"sh",
|
|
1352
|
+
"-c",
|
|
1353
|
+
`printf '%s' '${file}' > /tmp/${kind}.json.partial && mv /tmp/${kind}.json.partial /tmp/${kind}.json`
|
|
1354
|
+
]);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1022
1357
|
const lab = {
|
|
1023
1358
|
cluster,
|
|
1024
1359
|
image,
|
|
@@ -1093,6 +1428,12 @@ const openKindLab = async (cluster, image) => {
|
|
|
1093
1428
|
`printf '%s' '${file}' > /tmp/rules.json`
|
|
1094
1429
|
]);
|
|
1095
1430
|
},
|
|
1431
|
+
async eds(spec) {
|
|
1432
|
+
await pollersOf("eds", spec === null ? null : { cluster: spec.cluster }, spec);
|
|
1433
|
+
},
|
|
1434
|
+
async lds(spec) {
|
|
1435
|
+
await pollersOf("lds", {}, spec);
|
|
1436
|
+
},
|
|
1096
1437
|
async forgetHandshakes(ip, port) {
|
|
1097
1438
|
const out = {};
|
|
1098
1439
|
for (const [node, { agent }] of sidecars) {
|
|
@@ -1251,6 +1592,18 @@ const whole = (amount, what) => {
|
|
|
1251
1592
|
/** This function gives the namespace of a cell: `lab-` and 10 digits of the sha256 of its name. */
|
|
1252
1593
|
const namespaceOf = (cell) => `lab-${createHash("sha256").update(cell.name).digest("hex").slice(0, 10)}`;
|
|
1253
1594
|
/**
|
|
1595
|
+
* This function gives the Namespace object of a cell. In the sidecar mode of a mesh, the label
|
|
1596
|
+
* `istio-injection=enabled` makes the injector of Istio put a sidecar in each pod of the namespace.
|
|
1597
|
+
*/
|
|
1598
|
+
const namespaceObject = (cell, namespace) => ({
|
|
1599
|
+
apiVersion: "v1",
|
|
1600
|
+
kind: "Namespace",
|
|
1601
|
+
metadata: {
|
|
1602
|
+
name: namespace,
|
|
1603
|
+
...cell.mesh?.mode === "sidecar" ? { labels: { "istio-injection": "enabled" } } : {}
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
/**
|
|
1254
1607
|
* This function gives a probe of the pod, with each field of the cell. The probe of the service is an
|
|
1255
1608
|
* HTTP GET of its health server (`path`). The probe of grpc-go (`path` null) is the gRPC probe of the
|
|
1256
1609
|
* kubelet. It is a Check of the service "" on the port of the server, and the answer is SERVING until
|
|
@@ -1808,11 +2161,17 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
1808
2161
|
const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1809
2162
|
/** This function tells if a cell has Redis: only a declaration of the service can have it. */
|
|
1810
2163
|
const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !== null;
|
|
1811
|
-
/** This
|
|
2164
|
+
/** This function gives the key of the cluster of a cell: its kube-proxy mode, its node count, and the flavor of its mesh. */
|
|
2165
|
+
const clusterKey = (cell) => cell.mesh === void 0 ? `${cell.mode}-${cell.workers}` : `${cell.mode}-${cell.workers}-${cell.mesh.flavor}`;
|
|
2166
|
+
/**
|
|
2167
|
+
* This tracker keeps the latest state of the cell from the lines: each pod, each slice and each
|
|
2168
|
+
* client. It also has, for each node, the backends of its rules and the hosts of its client sidecar.
|
|
2169
|
+
*/
|
|
1812
2170
|
var Tracker = class {
|
|
1813
2171
|
pods = /* @__PURE__ */ new Map();
|
|
1814
2172
|
slices = /* @__PURE__ */ new Map();
|
|
1815
2173
|
backends = /* @__PURE__ */ new Map();
|
|
2174
|
+
hosts = /* @__PURE__ */ new Map();
|
|
1816
2175
|
clients = /* @__PURE__ */ new Map();
|
|
1817
2176
|
/** This map is the backend of the last answer of each client, by client. */
|
|
1818
2177
|
answers = /* @__PURE__ */ new Map();
|
|
@@ -1827,6 +2186,7 @@ var Tracker = class {
|
|
|
1827
2186
|
if (line["type"] === "DELETED") this.slices.delete(String(line["name"]));
|
|
1828
2187
|
else this.slices.set(String(line["name"]), line);
|
|
1829
2188
|
} else if (line.ev === "rules") this.backends.set(String(line["node"]), line["b"]);
|
|
2189
|
+
else if (line.ev === "eds") this.hosts.set(String(line["node"]), line["b"]);
|
|
1830
2190
|
else if (line.ev === "req") {
|
|
1831
2191
|
const request = line;
|
|
1832
2192
|
const client = this.clients.get(request.p) ?? {
|
|
@@ -1861,6 +2221,9 @@ const unsteady = (ctx) => {
|
|
|
1861
2221
|
if (ready.join() !== ips.join()) return `the EndpointSlice has ${ready.join(" ")}, not ${ips.join(" ")}`;
|
|
1862
2222
|
const expected = ips.map((ip) => `${ip}:8080`).sort().join();
|
|
1863
2223
|
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(" ")}`;
|
|
2224
|
+
if (cell.mesh?.mode === "sidecar") {
|
|
2225
|
+
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"}`;
|
|
2226
|
+
}
|
|
1864
2227
|
const nodes = new Set([...tracker.clients.values()].map((client) => client.node));
|
|
1865
2228
|
if (nodes.size !== ctx.cluster.nodes.length) return `clients answer on ${nodes.size} nodes`;
|
|
1866
2229
|
for (const [pod, client] of tracker.clients) {
|
|
@@ -1923,11 +2286,7 @@ const quiet = async (ctx, ours) => {
|
|
|
1923
2286
|
* the backoff of grpc-js longer, up to 120 s.
|
|
1924
2287
|
*/
|
|
1925
2288
|
const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
1926
|
-
await cluster.apply([
|
|
1927
|
-
apiVersion: "v1",
|
|
1928
|
-
kind: "Namespace",
|
|
1929
|
-
metadata: { name: namespace }
|
|
1930
|
-
}]);
|
|
2289
|
+
await cluster.apply([namespaceObject(cell, namespace)]);
|
|
1931
2290
|
await lab.watch([namespace]);
|
|
1932
2291
|
if (hasRedis(cell)) {
|
|
1933
2292
|
await cluster.apply(redisObjects(namespace, images.redis));
|
|
@@ -1979,8 +2338,62 @@ const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
|
1979
2338
|
port: 80,
|
|
1980
2339
|
every: RULES_EVERY
|
|
1981
2340
|
});
|
|
2341
|
+
if (cell.mesh?.mode === "sidecar") await pollEndpoints(lab, cluster, namespace);
|
|
1982
2342
|
return clusterIp;
|
|
1983
2343
|
};
|
|
2344
|
+
/**
|
|
2345
|
+
* This function starts the EDS poll of the client sidecar on each node: the hosts of the server
|
|
2346
|
+
* cluster, each 50 ms. The agent enters the network namespace of the client pod through the PID of
|
|
2347
|
+
* its client process. The sidecar shares that namespace.
|
|
2348
|
+
*/
|
|
2349
|
+
const pollEndpoints = async (lab, cluster, namespace) => {
|
|
2350
|
+
const listed = JSON.parse(await cluster.kubectl([
|
|
2351
|
+
"-n",
|
|
2352
|
+
namespace,
|
|
2353
|
+
"get",
|
|
2354
|
+
"pods",
|
|
2355
|
+
"-l",
|
|
2356
|
+
`app=${CLIENT}`,
|
|
2357
|
+
"-o",
|
|
2358
|
+
"json"
|
|
2359
|
+
]));
|
|
2360
|
+
const pods = [];
|
|
2361
|
+
for (const item of listed.items.filter((each) => each.metadata.deletionTimestamp === void 0)) {
|
|
2362
|
+
const node = item.spec.nodeName ?? "";
|
|
2363
|
+
pods.push({
|
|
2364
|
+
node,
|
|
2365
|
+
pod: item.metadata.name,
|
|
2366
|
+
pid: await lab.pidOf(node, namespace, item.metadata.name, CLIENT)
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
await lab.eds({
|
|
2370
|
+
cluster: serverCluster(namespace),
|
|
2371
|
+
every: EDS_EVERY,
|
|
2372
|
+
pods
|
|
2373
|
+
});
|
|
2374
|
+
};
|
|
2375
|
+
/**
|
|
2376
|
+
* This function starts the LDS poll of the sidecar of each server pod, each 50 ms, for a cell that
|
|
2377
|
+
* polls them (`pollsListeners`). It starts the pollers again only when the server pods changed. The
|
|
2378
|
+
* agent enters the network namespace of the pod through the PID of its app.
|
|
2379
|
+
*/
|
|
2380
|
+
const pollListeners = async (ctx) => {
|
|
2381
|
+
if (!pollsListeners(ctx.cell)) return;
|
|
2382
|
+
const servers = ctx.tracker.servers().filter((pod) => !pod.del && pod.node !== null);
|
|
2383
|
+
const key = servers.map((pod) => `${pod.name}/${pod.cs.map((status) => status.id).join("+")}`).sort().join();
|
|
2384
|
+
if (key === ctx.listened) return;
|
|
2385
|
+
const pods = [];
|
|
2386
|
+
for (const pod of servers) pods.push({
|
|
2387
|
+
node: pod.node,
|
|
2388
|
+
pod: pod.name,
|
|
2389
|
+
pid: await ctx.lab.pidOf(pod.node, ctx.namespace, pod.name, "app")
|
|
2390
|
+
});
|
|
2391
|
+
await ctx.lab.lds({
|
|
2392
|
+
every: LDS_EVERY,
|
|
2393
|
+
pods
|
|
2394
|
+
});
|
|
2395
|
+
ctx.listened = key;
|
|
2396
|
+
};
|
|
1984
2397
|
/** This function starts the prober again when the pods of the cell changed: it watches the namespace of each pod. */
|
|
1985
2398
|
const probeThePods = async (ctx) => {
|
|
1986
2399
|
const pods = [...ctx.tracker.pods.values()].filter((pod) => !pod.del && pod.node !== null && pod.cs.every((status) => status.st === "running"));
|
|
@@ -2128,6 +2541,15 @@ const doStep = async (ctx, step, replicas, background) => {
|
|
|
2128
2541
|
case "wait":
|
|
2129
2542
|
await sleep(valueIn(step.time, "ms"));
|
|
2130
2543
|
return null;
|
|
2544
|
+
case "apply": {
|
|
2545
|
+
const object = JSON.parse(fill([JSON.stringify(step.object)])[0]);
|
|
2546
|
+
const t0 = await lab.now();
|
|
2547
|
+
await cluster.apply([object]);
|
|
2548
|
+
return {
|
|
2549
|
+
t0,
|
|
2550
|
+
t1: await lab.now()
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2131
2553
|
}
|
|
2132
2554
|
};
|
|
2133
2555
|
/** This function tells if a run is over: the trigger is done, and the cell is at rest again. */
|
|
@@ -2143,7 +2565,7 @@ const over = (ctx, run, lines) => {
|
|
|
2143
2565
|
"node-drain",
|
|
2144
2566
|
"scale-in",
|
|
2145
2567
|
"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);
|
|
2568
|
+
].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
2569
|
if (kind === "rolling-update" || kind === "rollout") return run.replicas.every((replica) => !ctx.tracker.pods.has(replica.pod));
|
|
2148
2570
|
if (kind === "app-stall") return lines.some((line) => line.ev === "stall" && line["phase"] === "end");
|
|
2149
2571
|
return true;
|
|
@@ -2179,6 +2601,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
2179
2601
|
});
|
|
2180
2602
|
if (atRest !== null) return empty(`the cell was not at rest: ${atRest}`, await lab.now());
|
|
2181
2603
|
await probeThePods(ctx);
|
|
2604
|
+
await pollListeners(ctx);
|
|
2182
2605
|
await quiet(ctx, ours);
|
|
2183
2606
|
if (await steady(ctx, 3e5) !== null) return empty("the cell was not at rest after the wait for a quiet VM", await lab.now());
|
|
2184
2607
|
const replicas = replicasOf(ctx);
|
|
@@ -2348,17 +2771,22 @@ const runKindCells = async (cells, options) => {
|
|
|
2348
2771
|
const images = {
|
|
2349
2772
|
lab: image,
|
|
2350
2773
|
redis: redisImage,
|
|
2351
|
-
grpc: grpcImage
|
|
2774
|
+
grpc: grpcImage,
|
|
2775
|
+
mesh: cells.some((cell) => cell.mesh !== void 0) ? {
|
|
2776
|
+
pilot: await localPin(ISTIO_IMAGES.pilot),
|
|
2777
|
+
proxy: await localPin(ISTIO_IMAGES.proxy)
|
|
2778
|
+
} : void 0
|
|
2352
2779
|
};
|
|
2353
2780
|
const groups = /* @__PURE__ */ new Map();
|
|
2354
|
-
for (const cell of cells) groups.set(
|
|
2781
|
+
for (const cell of cells) groups.set(clusterKey(cell), [...groups.get(clusterKey(cell)) ?? [], cell]);
|
|
2355
2782
|
const results = [];
|
|
2356
2783
|
for (const [key, group] of groups) {
|
|
2357
2784
|
const partial = /* @__PURE__ */ new Map();
|
|
2358
2785
|
for (let env = 0; env < environments; env += 1) {
|
|
2359
2786
|
const s = scenario(`lab-${key}-${env}`, group[0].mode, group[0].workers);
|
|
2360
2787
|
const dir = join(options.work ?? join(tmpdir(), "systems-kind-work"), s.name);
|
|
2361
|
-
|
|
2788
|
+
const mesh = group[0].mesh;
|
|
2789
|
+
options.log(`cluster ${s.name}: ${s.kubeProxy.mode}, ${s.workers} workers${mesh === void 0 ? "" : `, Istio ${mesh.flavor}`}`);
|
|
2362
2790
|
const cluster = await createCluster(s, dir);
|
|
2363
2791
|
let lab = null;
|
|
2364
2792
|
let loader = null;
|
|
@@ -2366,6 +2794,13 @@ const runKindCells = async (cells, options) => {
|
|
|
2366
2794
|
await cluster.load(image);
|
|
2367
2795
|
await cluster.load(redisImage);
|
|
2368
2796
|
if (grpcImage !== void 0 && group.some((cell) => stackOf(cell) === "grpc")) await cluster.load(grpcImage);
|
|
2797
|
+
if (mesh !== void 0) {
|
|
2798
|
+
if (images.mesh === void 0) throw new Error(`${group[0].name}: the run has no images of Istio`);
|
|
2799
|
+
for (const each of [images.mesh.pilot, images.mesh.proxy]) await cluster.load(each);
|
|
2800
|
+
const began = Date.now();
|
|
2801
|
+
await installMesh(cluster, mesh.flavor, images.mesh);
|
|
2802
|
+
options.log(` Istio ${mesh.flavor} installed in ${Math.round((Date.now() - began) / 100) / 10} s`);
|
|
2803
|
+
}
|
|
2369
2804
|
lab = await openKindLab(cluster, image);
|
|
2370
2805
|
const { GenericContainer } = await import("testcontainers");
|
|
2371
2806
|
const Shared = class extends GenericContainer {
|
|
@@ -2399,6 +2834,7 @@ const runKindCells = async (cells, options) => {
|
|
|
2399
2834
|
log: options.log,
|
|
2400
2835
|
version: 1,
|
|
2401
2836
|
probed: "",
|
|
2837
|
+
listened: "",
|
|
2402
2838
|
expected: cell.pod.replicas
|
|
2403
2839
|
};
|
|
2404
2840
|
const share = Math.ceil(cell.runs / environments) - (env < cell.runs % environments || cell.runs % environments === 0 ? 0 : 1);
|
|
@@ -2417,7 +2853,11 @@ const runKindCells = async (cells, options) => {
|
|
|
2417
2853
|
images: {
|
|
2418
2854
|
lab: image,
|
|
2419
2855
|
redis: redisImage,
|
|
2420
|
-
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {}
|
|
2856
|
+
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
|
|
2857
|
+
...cell.mesh === void 0 ? {} : {
|
|
2858
|
+
"istio-pilot": ISTIO_IMAGES.pilot,
|
|
2859
|
+
"istio-proxy": ISTIO_IMAGES.proxy
|
|
2860
|
+
}
|
|
2421
2861
|
}
|
|
2422
2862
|
});
|
|
2423
2863
|
entry.loads.push(...loads);
|
|
@@ -2432,6 +2872,8 @@ const runKindCells = async (cells, options) => {
|
|
|
2432
2872
|
} finally {
|
|
2433
2873
|
off();
|
|
2434
2874
|
await lab.rules(null).catch(() => void 0);
|
|
2875
|
+
await lab.eds(null).catch(() => void 0);
|
|
2876
|
+
await lab.lds(null).catch(() => void 0);
|
|
2435
2877
|
await lab.stopProbe();
|
|
2436
2878
|
await cluster.kubectl([
|
|
2437
2879
|
"delete",
|
|
@@ -2475,12 +2917,13 @@ const estimate = (cell, environments = 2) => {
|
|
|
2475
2917
|
crash: 30,
|
|
2476
2918
|
"app-stall": hold + 40,
|
|
2477
2919
|
"cpu-throttle": hold + 40,
|
|
2478
|
-
rollout: hold + 50
|
|
2920
|
+
rollout: hold + 50,
|
|
2921
|
+
"authorization-policy": hold + 20
|
|
2479
2922
|
};
|
|
2480
2923
|
const after = observationAfter(cell);
|
|
2481
2924
|
const observed = after === null ? 0 : hold + valueIn(after, "s");
|
|
2482
2925
|
const each = 25 + Math.max(observed, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
|
|
2483
|
-
return 60 * environments + (cell.runs + environments) * each;
|
|
2926
|
+
return (cell.mesh === void 0 ? 60 : 180) * environments + (cell.runs + environments) * each;
|
|
2484
2927
|
};
|
|
2485
2928
|
/**
|
|
2486
2929
|
* This function gives an estimate of the time of a cell: its setup, then each run and each
|
|
@@ -2493,11 +2936,13 @@ const estimatedTime = (cell, environments = 2) => ({
|
|
|
2493
2936
|
});
|
|
2494
2937
|
/**
|
|
2495
2938
|
* This function gives the share of a shard of a catalog: cells in sequence. Thus the cells of one
|
|
2496
|
-
* mode stay together and share clusters. Each shard gets an equal share of the
|
|
2939
|
+
* mode and one mesh flavor stay together and share clusters. Each shard gets an equal share of the
|
|
2940
|
+
* estimated time. In one mode, the cells with no mesh come first, in the sequence of the catalog.
|
|
2497
2941
|
*/
|
|
2498
2942
|
const shardOf = (cells, index, count) => {
|
|
2499
2943
|
if (!(Number.isInteger(index) && Number.isInteger(count) && count >= 1 && index >= 0 && index < count)) throw new RangeError(`shard ${index} of ${count} is not a shard`);
|
|
2500
|
-
const
|
|
2944
|
+
const rank = (cell) => `${cell.mode}/${meshKey(cell) === "none" ? "" : meshKey(cell)}`;
|
|
2945
|
+
const ordered = [...cells].sort((a, b) => rank(a) === rank(b) ? 0 : rank(a) < rank(b) ? -1 : 1);
|
|
2501
2946
|
const total = ordered.reduce((sum, cell) => sum + estimate(cell), 0);
|
|
2502
2947
|
const out = [];
|
|
2503
2948
|
let before = 0;
|
|
@@ -2509,6 +2954,6 @@ const shardOf = (cells, index, count) => {
|
|
|
2509
2954
|
return out;
|
|
2510
2955
|
};
|
|
2511
2956
|
//#endregion
|
|
2512
|
-
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, observationAfter, observationEnd, 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 };
|
|
2957
|
+
export { ACCESS_LOG_FORMAT, CLIENT, CLIENT_ACCOUNT, 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, LDS_EVERY, LDS_FILTER, LOG_LABEL, NODE_IMAGES, O04, O09, O10, O16, POLICY, 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, listenerCountsOf, localPin, medianInterval, medianText, meshKey, middle, namespaceObject, namespaceOf, nodeNames, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, pausedWindow, planOf, platformKey, podsAt, podsOf, policyObject, pollsListeners, 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, updatesOf, upgrade, upgradeCell, verdictsOf, writeResult };
|
|
2513
2958
|
|
|
2514
2959
|
//# sourceMappingURL=index.js.map
|