@mark1russell7/kind 0.4.1 → 0.6.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 +27 -3
- package/build/index.d.ts +49 -12
- package/build/index.js +277 -54
- package/build/index.js.map +1 -1
- package/build/{judge-DMuI3aDn.d.ts → judge-B9kVpFE-.d.ts} +67 -30
- package/build/{judge-Bkbs7VVG.js → judge-DNCI5vvt.js} +170 -57
- package/build/judge-DNCI5vvt.js.map +1 -0
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/Dockerfile +1 -1
- package/image/client-grpc.mjs +9 -0
- package/image/watch.mjs +30 -3
- package/package.json +3 -2
- package/src/client-lines.ts +60 -0
- package/build/judge-Bkbs7VVG.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
|
|
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 LOG_LABEL, b as judgeKind, c as predictionText, ct as SERVER, d as FORMAT_V1, dt as secondsOf, et as socketsOf, f as judge, ft as showBand, g as LAST_SLACK, h as KIND_NOISE, i as readRecord, it as HEALTH_PORT, j as middle, k as RELATIVE, l as timeSpread, lt as bandOf, m as upgradeCell, n as kindCellRecordSchema, nt as transitionOf, o as cellsTable, ot as PORT, p as upgrade, q as restartsAt, r as kindCellRecordV1Schema, rt as CLIENT, s as medianText, st as REDIS, t as STAGES, tt as stoppingOf, u as FORMAT, ut as seconds, v as deletionsOf, w as propagationSource, x as observationAfter, y as factsOf, z as failuresOf } from "./judge-DNCI5vvt.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";
|
|
@@ -295,6 +295,22 @@ const writeWhole = (path, bytes, executable) => {
|
|
|
295
295
|
if (executable) chmodSync(partial, 493);
|
|
296
296
|
renameSync(partial, path);
|
|
297
297
|
};
|
|
298
|
+
/** These are the waits in ms before the second and the third try of a download. A runner can fail to connect for a moment. */
|
|
299
|
+
const DOWNLOAD_WAITS = [2e3, 8e3];
|
|
300
|
+
/**
|
|
301
|
+
* This function gives the bytes of a URL. It tries again after each wait when the connection fails or
|
|
302
|
+
* the answer is a 5xx. It refuses each other answer that is not ok at once.
|
|
303
|
+
*/
|
|
304
|
+
const download = async (url, waits = DOWNLOAD_WAITS) => {
|
|
305
|
+
for (let attempt = 0;; attempt++) {
|
|
306
|
+
const response = await fetch(url, { redirect: "follow" }).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
307
|
+
if (response instanceof Response && response.ok) return new Uint8Array(await response.arrayBuffer());
|
|
308
|
+
const failure = response instanceof Response ? /* @__PURE__ */ new Error(`the download of ${url} answered ${response.status}`) : response;
|
|
309
|
+
const wait = waits[attempt];
|
|
310
|
+
if (response instanceof Response && response.status < 500 || wait === void 0) throw failure;
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
312
|
+
}
|
|
313
|
+
};
|
|
298
314
|
/**
|
|
299
315
|
* This function gives the path of a tool, after a download when the cache does not have it. It
|
|
300
316
|
* refuses bytes whose sha256 is not the pinned value, from the cache or from the download. For a
|
|
@@ -314,9 +330,7 @@ const ensureTool = async (tool, key = platformKey(), cache = cacheFolder()) => {
|
|
|
314
330
|
if (found !== asset.sha256) throw new Error(`${kept} has the sha256 ${found}, not the pinned ${asset.sha256}`);
|
|
315
331
|
if (tool.entry === void 0) return path;
|
|
316
332
|
} else {
|
|
317
|
-
|
|
318
|
-
if (!response.ok) throw new Error(`the download of ${asset.url} answered ${response.status}`);
|
|
319
|
-
bytes = new Uint8Array(await response.arrayBuffer());
|
|
333
|
+
bytes = await download(asset.url);
|
|
320
334
|
const found = sha256(bytes);
|
|
321
335
|
if (found !== asset.sha256) throw new Error(`${asset.url} has the sha256 ${found}, not the pinned ${asset.sha256}`);
|
|
322
336
|
writeWhole(kept, bytes, executable && tool.entry === void 0);
|
|
@@ -642,6 +656,12 @@ const policyObject = (account) => ({
|
|
|
642
656
|
rules: [{ from: [{ source: { principals: [`cluster.local/ns/{namespace}/sa/${account}`] } }] }]
|
|
643
657
|
}
|
|
644
658
|
});
|
|
659
|
+
/**
|
|
660
|
+
* This function gives the target of a reset or a black hole of a cell. In T07 and T18 it is Redis,
|
|
661
|
+
* with the replicas as peers. In T15 the fault is on the mesh leg between the client pods and the
|
|
662
|
+
* replicas. It is a rule in each replica, with the client pods as peers.
|
|
663
|
+
*/
|
|
664
|
+
const linkOf = (cell) => cell.template === "T15" ? "servers" : "redis";
|
|
645
665
|
/** This function gives the fault plan of a cell. */
|
|
646
666
|
const planOf = (cell) => {
|
|
647
667
|
const ns = ["-n", "{namespace}"];
|
|
@@ -794,13 +814,13 @@ const planOf = (cell) => {
|
|
|
794
814
|
case "rst": return {
|
|
795
815
|
inject: [{
|
|
796
816
|
do: "inject",
|
|
797
|
-
target:
|
|
817
|
+
target: linkOf(cell),
|
|
798
818
|
action: "rst"
|
|
799
819
|
}],
|
|
800
820
|
hold: needs(trigger.hold, "a hold"),
|
|
801
821
|
heal: [{
|
|
802
822
|
do: "inject",
|
|
803
|
-
target:
|
|
823
|
+
target: linkOf(cell),
|
|
804
824
|
action: "clear-rst"
|
|
805
825
|
}],
|
|
806
826
|
restore: []
|
|
@@ -808,13 +828,13 @@ const planOf = (cell) => {
|
|
|
808
828
|
case "blackhole": return {
|
|
809
829
|
inject: [{
|
|
810
830
|
do: "inject",
|
|
811
|
-
target:
|
|
831
|
+
target: linkOf(cell),
|
|
812
832
|
action: "blackhole"
|
|
813
833
|
}],
|
|
814
834
|
hold: needs(trigger.hold, "a hold"),
|
|
815
835
|
heal: [{
|
|
816
836
|
do: "inject",
|
|
817
|
-
target:
|
|
837
|
+
target: linkOf(cell),
|
|
818
838
|
action: "clear"
|
|
819
839
|
}],
|
|
820
840
|
restore: []
|
|
@@ -930,6 +950,24 @@ const planOf = (cell) => {
|
|
|
930
950
|
]
|
|
931
951
|
}]
|
|
932
952
|
};
|
|
953
|
+
case "idle": return {
|
|
954
|
+
inject: [{
|
|
955
|
+
do: "signal",
|
|
956
|
+
target: "clients",
|
|
957
|
+
signal: "SIGUSR1"
|
|
958
|
+
}],
|
|
959
|
+
hold: needs(trigger.hold, "a hold, the time of the gap"),
|
|
960
|
+
heal: [{
|
|
961
|
+
do: "signal",
|
|
962
|
+
target: "clients",
|
|
963
|
+
signal: "SIGUSR2"
|
|
964
|
+
}],
|
|
965
|
+
restore: [{
|
|
966
|
+
do: "signal",
|
|
967
|
+
target: "clients",
|
|
968
|
+
signal: "SIGUSR2"
|
|
969
|
+
}]
|
|
970
|
+
};
|
|
933
971
|
}
|
|
934
972
|
};
|
|
935
973
|
/** 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. */
|
|
@@ -966,7 +1004,8 @@ const FILES = [
|
|
|
966
1004
|
["conntrack.ts", "src/conntrack.ts"],
|
|
967
1005
|
["drops.ts", "src/drops.ts"],
|
|
968
1006
|
["eds.ts", "src/eds.ts"],
|
|
969
|
-
["lds.ts", "src/lds.ts"]
|
|
1007
|
+
["lds.ts", "src/lds.ts"],
|
|
1008
|
+
["client-lines.ts", "src/client-lines.ts"]
|
|
970
1009
|
];
|
|
971
1010
|
const shell = process.platform === "win32";
|
|
972
1011
|
/** This function gives each file of a staged context, with its path in the context, in a stable sequence. */
|
|
@@ -1078,14 +1117,27 @@ const labImage = () => {
|
|
|
1078
1117
|
/** This version is the release of Istio of the lab: the tag of istioctl and of each image. */
|
|
1079
1118
|
const ISTIO_VERSION = "1.31.3";
|
|
1080
1119
|
/**
|
|
1081
|
-
* These are the images of
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1120
|
+
* These are the images of Istio, pinned by the digest of their index on Docker Hub (read on
|
|
1121
|
+
* 2026-10-24). `gcr.io/istio-release` has no final tag of 1.31. The lab pulls each one by its digest,
|
|
1122
|
+
* tags it with that digest (`localPin`), and loads it into each node. Thus no node pulls from a
|
|
1123
|
+
* registry. The flavor `sidecar` needs istiod and the proxy. The flavor `ambient` also needs
|
|
1124
|
+
* istio-cni and ztunnel (`MESH_IMAGES`).
|
|
1085
1125
|
*/
|
|
1086
1126
|
const ISTIO_IMAGES = {
|
|
1087
1127
|
pilot: "istio/pilot:1.31.3@sha256:6ad1e816c4d4767ba3f30a6286a96d121851ddb23d33e20736758acb5f3f4ea9",
|
|
1088
|
-
proxy: "istio/proxyv2:1.31.3@sha256:eb74eacea98d3fb1e239ea84016085155478586a554158676b9b67d960151084"
|
|
1128
|
+
proxy: "istio/proxyv2:1.31.3@sha256:eb74eacea98d3fb1e239ea84016085155478586a554158676b9b67d960151084",
|
|
1129
|
+
cni: "istio/install-cni:1.31.3@sha256:e98782038634ef4fadaf78b53dd8fa4134fda3d65838ead5fc1edef245451c0f",
|
|
1130
|
+
ztunnel: "istio/ztunnel:1.31.3@sha256:2c27371b3139ecbf4f5a017354bd540889482441712e88cd8e654ead5a3eb5b2"
|
|
1131
|
+
};
|
|
1132
|
+
/** These are the images that each flavor installs, by their names in `ISTIO_IMAGES`. */
|
|
1133
|
+
const MESH_IMAGES = {
|
|
1134
|
+
sidecar: ["pilot", "proxy"],
|
|
1135
|
+
ambient: [
|
|
1136
|
+
"pilot",
|
|
1137
|
+
"proxy",
|
|
1138
|
+
"cni",
|
|
1139
|
+
"ztunnel"
|
|
1140
|
+
]
|
|
1089
1141
|
};
|
|
1090
1142
|
/**
|
|
1091
1143
|
* This format is the JSON access log of each sidecar. Each field is a command of the formatter of
|
|
@@ -1119,10 +1171,15 @@ const ACCESS_LOG_FORMAT = {
|
|
|
1119
1171
|
* reports the whole runner, and no cell scales istiod.
|
|
1120
1172
|
* - The access log of each sidecar goes to its output as JSON (`ACCESS_LOG_FORMAT`).
|
|
1121
1173
|
*
|
|
1122
|
-
* The flavor `ambient`
|
|
1174
|
+
* The flavor `ambient` has the profile `ambient`: istiod, istio-cni and ztunnel, and no gateway
|
|
1175
|
+
* (manifests/profiles/ambient.yaml of Istio 1.31.3). Its two other images are full references too
|
|
1176
|
+
* (templates/daemonset.yaml of the chart of istio-cni, lines 93–96, and of ztunnel, lines 65–68).
|
|
1177
|
+
* The chart of istio-cni takes the pull policy of `global`. The template of ztunnel reads only its own
|
|
1178
|
+
* `imagePullPolicy`, thus the operator sets it there too.
|
|
1123
1179
|
*/
|
|
1124
1180
|
const istioOperator = (flavor, images) => {
|
|
1125
|
-
|
|
1181
|
+
const ambient = flavor === "ambient";
|
|
1182
|
+
if (ambient && (images.cni === void 0 || images.ztunnel === void 0)) throw new Error("the flavor ambient needs the images of istio-cni and ztunnel");
|
|
1126
1183
|
return {
|
|
1127
1184
|
apiVersion: "install.istio.io/v1alpha1",
|
|
1128
1185
|
kind: "IstioOperator",
|
|
@@ -1131,7 +1188,7 @@ const istioOperator = (flavor, images) => {
|
|
|
1131
1188
|
namespace: "istio-system"
|
|
1132
1189
|
},
|
|
1133
1190
|
spec: {
|
|
1134
|
-
profile: "minimal",
|
|
1191
|
+
profile: ambient ? "ambient" : "minimal",
|
|
1135
1192
|
hub: "docker.io/istio",
|
|
1136
1193
|
tag: ISTIO_VERSION,
|
|
1137
1194
|
meshConfig: {
|
|
@@ -1153,37 +1210,77 @@ const istioOperator = (flavor, images) => {
|
|
|
1153
1210
|
cpu: "500m",
|
|
1154
1211
|
memory: "256Mi"
|
|
1155
1212
|
} }
|
|
1156
|
-
}
|
|
1213
|
+
},
|
|
1214
|
+
...ambient ? {
|
|
1215
|
+
cni: { image: images.cni },
|
|
1216
|
+
ztunnel: {
|
|
1217
|
+
image: images.ztunnel,
|
|
1218
|
+
imagePullPolicy: "Never"
|
|
1219
|
+
}
|
|
1220
|
+
} : {}
|
|
1157
1221
|
}
|
|
1158
1222
|
}
|
|
1159
1223
|
};
|
|
1160
1224
|
};
|
|
1225
|
+
/** These are the workloads of each flavor in istio-system that the install waits for. */
|
|
1226
|
+
const MESH_WORKLOADS = {
|
|
1227
|
+
sidecar: ["deployment/istiod"],
|
|
1228
|
+
ambient: [
|
|
1229
|
+
"deployment/istiod",
|
|
1230
|
+
"daemonset/istio-cni-node",
|
|
1231
|
+
"daemonset/ztunnel"
|
|
1232
|
+
]
|
|
1233
|
+
};
|
|
1161
1234
|
/**
|
|
1162
1235
|
* This function installs the mesh of a flavor in a cluster with `istioctl install` and the operator
|
|
1163
|
-
* of the flavor. Then it waits for
|
|
1236
|
+
* of the flavor. Then it waits for each workload of the flavor. The kubeconfig of the cluster goes to
|
|
1237
|
+
* istioctl. When the install fails, the error has the pods of istio-system and the last lines of the
|
|
1238
|
+
* log of istio-cni. Thus the log of the job shows the cause, for example a module that the kernel of
|
|
1239
|
+
* the runner does not have.
|
|
1164
1240
|
*/
|
|
1165
1241
|
const installMesh = async (cluster, flavor, images) => {
|
|
1166
1242
|
const istioctl = await ensureTool(ISTIOCTL);
|
|
1167
1243
|
const file = join(cluster.dir, `istio-${flavor}.json`);
|
|
1168
1244
|
writeFileSync(file, `${JSON.stringify(istioOperator(flavor, images), null, 2)}\n`);
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1245
|
+
try {
|
|
1246
|
+
await command(istioctl, [
|
|
1247
|
+
"install",
|
|
1248
|
+
"--kubeconfig",
|
|
1249
|
+
cluster.kubeconfig,
|
|
1250
|
+
"-y",
|
|
1251
|
+
"-f",
|
|
1252
|
+
file,
|
|
1253
|
+
"--readiness-timeout",
|
|
1254
|
+
"10m0s"
|
|
1255
|
+
], { timeout: 9e5 });
|
|
1256
|
+
for (const workload of MESH_WORKLOADS[flavor]) await cluster.kubectl([
|
|
1257
|
+
"-n",
|
|
1258
|
+
"istio-system",
|
|
1259
|
+
"rollout",
|
|
1260
|
+
"status",
|
|
1261
|
+
workload,
|
|
1262
|
+
"--timeout=300s"
|
|
1263
|
+
]);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
const state = async (args) => (await cluster.kubectl([
|
|
1266
|
+
"-n",
|
|
1267
|
+
"istio-system",
|
|
1268
|
+
...args
|
|
1269
|
+
]).catch((why) => String(why))).slice(-4e3);
|
|
1270
|
+
const pods = await state([
|
|
1271
|
+
"get",
|
|
1272
|
+
"pods",
|
|
1273
|
+
"-o",
|
|
1274
|
+
"wide"
|
|
1275
|
+
]);
|
|
1276
|
+
const cni = flavor === "ambient" ? `\nthe log of istio-cni:\n${await state([
|
|
1277
|
+
"logs",
|
|
1278
|
+
"daemonset/istio-cni-node",
|
|
1279
|
+
"--tail=60",
|
|
1280
|
+
"--all-containers"
|
|
1281
|
+
])}` : "";
|
|
1282
|
+
throw new Error(`the install of Istio ${flavor} failed: ${String(error)}\nthe pods of istio-system:\n${pods}${cni}`);
|
|
1283
|
+
}
|
|
1187
1284
|
};
|
|
1188
1285
|
/** This function gives the key of the mesh of a cell for its cluster: its flavor, or `none`. */
|
|
1189
1286
|
const meshKey = (cell) => cell.mesh?.flavor ?? "none";
|
|
@@ -1199,6 +1296,8 @@ const LDS_EVERY = {
|
|
|
1199
1296
|
value: 50,
|
|
1200
1297
|
unit: "ms"
|
|
1201
1298
|
};
|
|
1299
|
+
/** This function tells if the watcher follows the log of each ztunnel for a cell: a cell in ambient mode. */
|
|
1300
|
+
const followsZtunnel = (cell) => cell.mesh?.mode === "ambient";
|
|
1202
1301
|
/** This function tells if the lab polls the LDS updates of the server sidecars of a cell: a policy in a sidecar mesh. */
|
|
1203
1302
|
const pollsListeners = (cell) => cell.mesh?.mode === "sidecar" && cell.trigger.kind === "authorization-policy";
|
|
1204
1303
|
//#endregion
|
|
@@ -1386,12 +1485,13 @@ const openKindLab = async (cluster, image) => {
|
|
|
1386
1485
|
listeners.add(listener);
|
|
1387
1486
|
return () => listeners.delete(listener);
|
|
1388
1487
|
},
|
|
1389
|
-
async watch(namespaces) {
|
|
1488
|
+
async watch(namespaces, options) {
|
|
1390
1489
|
if (watcher !== null) await watcher.stop({ timeout: 0 }).catch(() => void 0);
|
|
1391
1490
|
const kube = await cluster.credentials();
|
|
1392
1491
|
watcher = await new GenericContainer(image).withNetworkMode(cluster.network).withEnvironment({
|
|
1393
1492
|
KUBE: JSON.stringify(kube),
|
|
1394
|
-
NAMESPACES: namespaces.join(",")
|
|
1493
|
+
NAMESPACES: namespaces.join(","),
|
|
1494
|
+
ZTUNNEL: options?.ztunnel === true ? "1" : "0"
|
|
1395
1495
|
}).withCommand(["node", "watch.mjs"]).withLogConsumer(consume((line) => push(line))).start();
|
|
1396
1496
|
},
|
|
1397
1497
|
async probe(netns) {
|
|
@@ -1514,6 +1614,28 @@ const openKindLab = async (cluster, image) => {
|
|
|
1514
1614
|
});
|
|
1515
1615
|
return stamp;
|
|
1516
1616
|
},
|
|
1617
|
+
async tables(node, pid, table) {
|
|
1618
|
+
const t = await lab.now();
|
|
1619
|
+
const text = await output(sidecarOf(node).injector, [
|
|
1620
|
+
"nsenter",
|
|
1621
|
+
"-t",
|
|
1622
|
+
String(pid),
|
|
1623
|
+
"-n",
|
|
1624
|
+
"iptables-save",
|
|
1625
|
+
"-c",
|
|
1626
|
+
"-t",
|
|
1627
|
+
table
|
|
1628
|
+
]);
|
|
1629
|
+
push({
|
|
1630
|
+
ev: "tables",
|
|
1631
|
+
t,
|
|
1632
|
+
node,
|
|
1633
|
+
pid,
|
|
1634
|
+
table,
|
|
1635
|
+
text: text.slice(0, 8e3)
|
|
1636
|
+
});
|
|
1637
|
+
return text;
|
|
1638
|
+
},
|
|
1517
1639
|
async now() {
|
|
1518
1640
|
return (await lab.act(firstNode, { op: "now" })).t0;
|
|
1519
1641
|
},
|
|
@@ -1600,16 +1722,23 @@ const whole = (amount, what) => {
|
|
|
1600
1722
|
};
|
|
1601
1723
|
/** This function gives the namespace of a cell: `lab-` and 10 digits of the sha256 of its name. */
|
|
1602
1724
|
const namespaceOf = (cell) => `lab-${createHash("sha256").update(cell.name).digest("hex").slice(0, 10)}`;
|
|
1725
|
+
/** These are the labels of the namespace of each mode of a mesh. */
|
|
1726
|
+
const MESH_LABELS = {
|
|
1727
|
+
sidecar: { "istio-injection": "enabled" },
|
|
1728
|
+
ambient: { "istio.io/dataplane-mode": "ambient" }
|
|
1729
|
+
};
|
|
1603
1730
|
/**
|
|
1604
1731
|
* This function gives the Namespace object of a cell. In the sidecar mode of a mesh, the label
|
|
1605
1732
|
* `istio-injection=enabled` makes the injector of Istio put a sidecar in each pod of the namespace.
|
|
1733
|
+
* In the ambient mode, the label `istio.io/dataplane-mode=ambient` makes istio-cni redirect the
|
|
1734
|
+
* traffic of each pod to ztunnel. The listeners of ztunnel are in the network namespace of the pod.
|
|
1606
1735
|
*/
|
|
1607
1736
|
const namespaceObject = (cell, namespace) => ({
|
|
1608
1737
|
apiVersion: "v1",
|
|
1609
1738
|
kind: "Namespace",
|
|
1610
1739
|
metadata: {
|
|
1611
1740
|
name: namespace,
|
|
1612
|
-
...cell.mesh
|
|
1741
|
+
...cell.mesh === void 0 ? {} : { labels: { ...MESH_LABELS[cell.mesh.mode] } }
|
|
1613
1742
|
}
|
|
1614
1743
|
});
|
|
1615
1744
|
/**
|
|
@@ -2296,7 +2425,7 @@ const quiet = async (ctx, ours) => {
|
|
|
2296
2425
|
*/
|
|
2297
2426
|
const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
2298
2427
|
await cluster.apply([namespaceObject(cell, namespace)]);
|
|
2299
|
-
await lab.watch([namespace]);
|
|
2428
|
+
await lab.watch([namespace], { ztunnel: followsZtunnel(cell) });
|
|
2300
2429
|
if (hasRedis(cell)) {
|
|
2301
2430
|
await cluster.apply(redisObjects(namespace, images.redis));
|
|
2302
2431
|
await cluster.kubectl([
|
|
@@ -2432,12 +2561,95 @@ const replicasOf = (ctx) => {
|
|
|
2432
2561
|
}));
|
|
2433
2562
|
return stackOf(ctx.cell) === "grpc" ? byClients(replicas, ctx.tracker.answers) : replicas;
|
|
2434
2563
|
};
|
|
2564
|
+
/**
|
|
2565
|
+
* This time is how long before the commit of its rules the lab starts the injector in each replica.
|
|
2566
|
+
* It is more than the execs of one node before its commit: a read of its tables and the injector.
|
|
2567
|
+
*/
|
|
2568
|
+
const INJECT_LEAD_MS = 700;
|
|
2569
|
+
/**
|
|
2570
|
+
* This function puts the rules of an action in the network namespace of each replica, with each
|
|
2571
|
+
* client pod as a peer: the target `servers`. The rules of all replicas commit at one instant on
|
|
2572
|
+
* CLOCK_MONOTONIC (`--at`). Thus no client meets one replica with the rule and one without it. The
|
|
2573
|
+
* replicas of one node go one after the other, and the nodes at the same time.
|
|
2574
|
+
*
|
|
2575
|
+
* The fault finds the PID of each replica, and the clear uses it again: thus the heal comes soon
|
|
2576
|
+
* after its hold. Before the commit of a clear, the lab reads the filter table of each replica. Its
|
|
2577
|
+
* counters show if the rule matched the clients beside the rules of istio-cni. After the commit of a
|
|
2578
|
+
* fault, the lab reads the nat table, with the redirect of istio-cni. The stamp is the first commit
|
|
2579
|
+
* and the last end.
|
|
2580
|
+
*/
|
|
2581
|
+
const injectServers = async (ctx, action, replicas) => {
|
|
2582
|
+
const clients = [...ctx.tracker.pods.values()].filter((pod) => pod.labels["app"] === "client" && !pod.del && pod.ip !== null).map((pod) => pod.ip).sort();
|
|
2583
|
+
if (clients.length === 0 || replicas.length === 0) throw new Error(`the injector needs clients and replicas: ${clients.length} clients, ${replicas.length} replicas`);
|
|
2584
|
+
const clear = action.startsWith("clear");
|
|
2585
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
2586
|
+
for (const replica of replicas) {
|
|
2587
|
+
const pid = (clear ? ctx.pids.get(replica.pod) : void 0) ?? await ctx.lab.pidOf(replica.node, ctx.namespace, replica.pod, "app");
|
|
2588
|
+
ctx.pids.set(replica.pod, pid);
|
|
2589
|
+
byNode.set(replica.node, [...byNode.get(replica.node) ?? [], pid]);
|
|
2590
|
+
}
|
|
2591
|
+
const at = await ctx.lab.now() + INJECT_LEAD_MS;
|
|
2592
|
+
const args = [
|
|
2593
|
+
action,
|
|
2594
|
+
clients.join(","),
|
|
2595
|
+
"--at",
|
|
2596
|
+
String(Math.round(at * 1e6))
|
|
2597
|
+
];
|
|
2598
|
+
const stamps = await Promise.all([...byNode].map(async ([node, pids]) => {
|
|
2599
|
+
const out = [];
|
|
2600
|
+
for (const pid of pids) {
|
|
2601
|
+
if (clear) await ctx.lab.tables(node, pid, "filter");
|
|
2602
|
+
out.push(await ctx.lab.inject(node, pid, args));
|
|
2603
|
+
}
|
|
2604
|
+
return out;
|
|
2605
|
+
}));
|
|
2606
|
+
if (!clear) for (const [node, pids] of byNode) for (const pid of pids) await ctx.lab.tables(node, pid, "nat");
|
|
2607
|
+
const all = stamps.flat();
|
|
2608
|
+
return {
|
|
2609
|
+
t0: Math.min(...all.map((stamp) => stamp.t0)),
|
|
2610
|
+
t1: Math.max(...all.map((stamp) => stamp.t1))
|
|
2611
|
+
};
|
|
2612
|
+
};
|
|
2613
|
+
/**
|
|
2614
|
+
* This function sends a signal to the process of each client pod: the target `clients`. The nodes go
|
|
2615
|
+
* at the same time. SIGUSR1 finds the PID of each client, and SIGUSR2 uses it again, thus the heal of
|
|
2616
|
+
* a gap comes soon after its hold. When that PID fails, the client started again, and the lab finds
|
|
2617
|
+
* its PID. The stamp is the first act and the last end.
|
|
2618
|
+
*/
|
|
2619
|
+
const signalClients = async (ctx, signal) => {
|
|
2620
|
+
const clients = [...ctx.tracker.pods.values()].filter((pod) => pod.labels["app"] === "client" && !pod.del && pod.node !== null);
|
|
2621
|
+
if (clients.length === 0) throw new Error(`the signal ${signal} needs clients`);
|
|
2622
|
+
const stamps = await Promise.all(clients.map(async (pod) => {
|
|
2623
|
+
const node = pod.node;
|
|
2624
|
+
const known = signal === "SIGUSR2" ? ctx.pids.get(pod.name) : void 0;
|
|
2625
|
+
if (known !== void 0) try {
|
|
2626
|
+
return await ctx.lab.act(node, {
|
|
2627
|
+
op: "signal",
|
|
2628
|
+
pid: known,
|
|
2629
|
+
signal
|
|
2630
|
+
});
|
|
2631
|
+
} catch {}
|
|
2632
|
+
const pid = await ctx.lab.pidOf(node, ctx.namespace, pod.name, CLIENT);
|
|
2633
|
+
ctx.pids.set(pod.name, pid);
|
|
2634
|
+
return ctx.lab.act(node, {
|
|
2635
|
+
op: "signal",
|
|
2636
|
+
pid,
|
|
2637
|
+
signal
|
|
2638
|
+
});
|
|
2639
|
+
}));
|
|
2640
|
+
return {
|
|
2641
|
+
t0: Math.min(...stamps.map((stamp) => stamp.t0)),
|
|
2642
|
+
t1: Math.max(...stamps.map((stamp) => stamp.t1))
|
|
2643
|
+
};
|
|
2644
|
+
};
|
|
2435
2645
|
/** This function does one step of a fault plan. It gives the stamp of its act, or null for an act without a stamp. */
|
|
2436
2646
|
const doStep = async (ctx, step, replicas, background) => {
|
|
2437
2647
|
const { lab, cluster, namespace } = ctx;
|
|
2438
2648
|
const target = replicas[0];
|
|
2439
2649
|
const fill = (args) => args.map((arg) => arg.replaceAll("{namespace}", namespace).replaceAll(POD, target.pod).replaceAll(NODE, target.node).replaceAll(VERSION, String(ctx.version)));
|
|
2440
2650
|
const where = async (which) => {
|
|
2651
|
+
if (which === "servers") throw new Error("the target servers is a target of the injector only");
|
|
2652
|
+
if (which === "clients") throw new Error("the target clients is a target of a signal only");
|
|
2441
2653
|
if (which === "server") return {
|
|
2442
2654
|
node: target.node,
|
|
2443
2655
|
pid: await lab.pidOf(target.node, namespace, target.pod, "app"),
|
|
@@ -2489,6 +2701,7 @@ const doStep = async (ctx, step, replicas, background) => {
|
|
|
2489
2701
|
};
|
|
2490
2702
|
}
|
|
2491
2703
|
case "signal": {
|
|
2704
|
+
if (step.target === "clients") return signalClients(ctx, step.signal);
|
|
2492
2705
|
const at = await where(step.target);
|
|
2493
2706
|
return lab.act(at.node, {
|
|
2494
2707
|
op: "signal",
|
|
@@ -2511,6 +2724,7 @@ const doStep = async (ctx, step, replicas, background) => {
|
|
|
2511
2724
|
};
|
|
2512
2725
|
}
|
|
2513
2726
|
case "inject": {
|
|
2727
|
+
if (step.target === "servers") return injectServers(ctx, step.action, replicas);
|
|
2514
2728
|
const at = await where(step.target);
|
|
2515
2729
|
let first = null;
|
|
2516
2730
|
for (const replica of replicas) {
|
|
@@ -2777,14 +2991,19 @@ const runKindCells = async (cells, options) => {
|
|
|
2777
2991
|
kind: "shipped",
|
|
2778
2992
|
name: "grpc-go"
|
|
2779
2993
|
}) : void 0;
|
|
2994
|
+
const flavors = new Set(cells.flatMap((cell) => cell.mesh === void 0 ? [] : [cell.mesh.flavor]));
|
|
2995
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
2996
|
+
for (const name of new Set([...flavors].flatMap((flavor) => MESH_IMAGES[flavor]))) pinned.set(name, await localPin(ISTIO_IMAGES[name]));
|
|
2780
2997
|
const images = {
|
|
2781
2998
|
lab: image,
|
|
2782
2999
|
redis: redisImage,
|
|
2783
3000
|
grpc: grpcImage,
|
|
2784
|
-
mesh:
|
|
2785
|
-
pilot:
|
|
2786
|
-
proxy:
|
|
2787
|
-
|
|
3001
|
+
mesh: flavors.size === 0 ? void 0 : {
|
|
3002
|
+
pilot: pinned.get("pilot"),
|
|
3003
|
+
proxy: pinned.get("proxy"),
|
|
3004
|
+
cni: pinned.get("cni"),
|
|
3005
|
+
ztunnel: pinned.get("ztunnel")
|
|
3006
|
+
}
|
|
2788
3007
|
};
|
|
2789
3008
|
const groups = /* @__PURE__ */ new Map();
|
|
2790
3009
|
for (const cell of cells) groups.set(clusterKey(cell), [...groups.get(clusterKey(cell)) ?? [], cell]);
|
|
@@ -2805,7 +3024,11 @@ const runKindCells = async (cells, options) => {
|
|
|
2805
3024
|
if (grpcImage !== void 0 && group.some((cell) => stackOf(cell) === "grpc")) await cluster.load(grpcImage);
|
|
2806
3025
|
if (mesh !== void 0) {
|
|
2807
3026
|
if (images.mesh === void 0) throw new Error(`${group[0].name}: the run has no images of Istio`);
|
|
2808
|
-
for (const
|
|
3027
|
+
for (const name of MESH_IMAGES[mesh.flavor]) {
|
|
3028
|
+
const each = images.mesh[name];
|
|
3029
|
+
if (each === void 0) throw new Error(`${group[0].name}: the run has no image ${name} of Istio`);
|
|
3030
|
+
await cluster.load(each);
|
|
3031
|
+
}
|
|
2809
3032
|
const began = Date.now();
|
|
2810
3033
|
await installMesh(cluster, mesh.flavor, images.mesh);
|
|
2811
3034
|
options.log(` Istio ${mesh.flavor} installed in ${Math.round((Date.now() - began) / 100) / 10} s`);
|
|
@@ -2844,7 +3067,8 @@ const runKindCells = async (cells, options) => {
|
|
|
2844
3067
|
version: 1,
|
|
2845
3068
|
probed: "",
|
|
2846
3069
|
listened: "",
|
|
2847
|
-
expected: cell.pod.replicas
|
|
3070
|
+
expected: cell.pod.replicas,
|
|
3071
|
+
pids: /* @__PURE__ */ new Map()
|
|
2848
3072
|
};
|
|
2849
3073
|
const share = Math.ceil(cell.runs / environments) - (env < cell.runs % environments || cell.runs % environments === 0 ? 0 : 1);
|
|
2850
3074
|
const runs = [await runOnce(ctx, env, -1, true, ours)];
|
|
@@ -2863,10 +3087,7 @@ const runKindCells = async (cells, options) => {
|
|
|
2863
3087
|
lab: image,
|
|
2864
3088
|
redis: redisImage,
|
|
2865
3089
|
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
|
|
2866
|
-
...cell.mesh === void 0 ? {} : {
|
|
2867
|
-
"istio-pilot": ISTIO_IMAGES.pilot,
|
|
2868
|
-
"istio-proxy": ISTIO_IMAGES.proxy
|
|
2869
|
-
}
|
|
3090
|
+
...cell.mesh === void 0 ? {} : Object.fromEntries(MESH_IMAGES[cell.mesh.flavor].map((name) => [`istio-${name}`, ISTIO_IMAGES[name]]))
|
|
2870
3091
|
}
|
|
2871
3092
|
});
|
|
2872
3093
|
entry.loads.push(...loads);
|
|
@@ -2927,11 +3148,13 @@ const estimate = (cell, environments = 2) => {
|
|
|
2927
3148
|
"app-stall": hold + 40,
|
|
2928
3149
|
"cpu-throttle": hold + 40,
|
|
2929
3150
|
rollout: hold + 50,
|
|
2930
|
-
"authorization-policy": hold + 20
|
|
3151
|
+
"authorization-policy": hold + 20,
|
|
3152
|
+
idle: hold + 5
|
|
2931
3153
|
};
|
|
2932
3154
|
const after = observationAfter(cell);
|
|
2933
3155
|
const observed = after === null ? 0 : hold + valueIn(after, "s");
|
|
2934
|
-
const
|
|
3156
|
+
const closes = cell.prediction.kind === "ends" && cell.prediction.band !== null ? valueIn(cell.prediction.band.hi, "s") + 3 : 0;
|
|
3157
|
+
const each = 25 + Math.max(observed, closes, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
|
|
2935
3158
|
return (cell.mesh === void 0 ? 60 : 180) * environments + (cell.runs + environments) * each;
|
|
2936
3159
|
};
|
|
2937
3160
|
/**
|
|
@@ -2963,6 +3186,6 @@ const shardOf = (cells, index, count) => {
|
|
|
2963
3186
|
return out;
|
|
2964
3187
|
};
|
|
2965
3188
|
//#endregion
|
|
2966
|
-
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 };
|
|
3189
|
+
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, MESH_IMAGES, MESH_WORKLOADS, 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, followsZtunnel, 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, socketsOf, stackOf, stageLabImage, stallRemovalBand, stoppingOf, tarEntry, timeSpread, toolPath, transitionOf, unitOf, updatesOf, upgrade, upgradeCell, verdictsOf, writeResult };
|
|
2967
3190
|
|
|
2968
3191
|
//# sourceMappingURL=index.js.map
|