@mark1russell7/kind 0.13.1 → 0.13.2
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/build/index.d.ts +146 -3
- package/build/index.js +557 -28
- package/build/index.js.map +1 -1
- package/build/{judge-DA754h1o.js → judge-C7Zr4KoS.js} +39 -10
- package/build/judge-C7Zr4KoS.js.map +1 -0
- package/build/{judge-CSWVew2-.d.ts → judge-DpdSgpjv.d.ts} +41 -3
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/client.mjs +25 -0
- package/package.json +1 -1
- package/build/judge-DA754h1o.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as signalsOf, A as HBONE_PORT, B as isTunnelState, C as observationEnd, Ct as
|
|
1
|
+
import { $ as signalsOf, A as HBONE_PORT, At as secondsOf, B as isTunnelState, C as observationEnd, Ct as VIP_ANNOTATION, D as reusesReset, Dt as overshootBand, E as referenceOf, Et as drainBand, F as deletionOf, G as podsOf, H as outcomesOf, I as dropOf, J as restartOf, K as recordsOf, L as emptyOf, M as backendsAt, N as byClients, O as unitOf, Ot as requestBand, P as byNode, Q as signalOf, R as failuresOf, S as observationAfter, St as SERVER, T as propagationSource, Tt as bandOf, U as periodOf, V as kernelCounts, W as podsAt, X as rulesOf, Y as restartsAt, Z as sessionsAt, _ as checkRun, _t as LB_CLASS, a as calibrationOf, at as socketsOf, b as factsOf, bt as PORT, c as predictionText, ct as tunnelsOf, d as FORMAT_V1, dt as RELATIVE, et as sliceDropOf, f as judge, ft as medianInterval, g as LAST_SLACK, gt as HEALTH_PORT, h as KIND_NOISE, ht as CLIENT, i as readRecord, it as slicesOf, j as TCP_STATE, jt as showBand, k as updatesOf, kt as seconds, l as timeSpread, lt as ztunnelPodsOf, m as upgradeCell, mt as quantileOf, n as kindCellRecordSchema, nt as sliceReadyAt, o as cellsTable, ot as stoppingOf, p as upgrade, pt as middle, q as requestsOf, r as kindCellRecordV1Schema, rt as sliceReadyOf, s as medianText, st as transitionOf, t as STAGES, tt as sliceEmptyOf, u as FORMAT, ut as ztunnelReplaced, v as cyclesOf, vt as LOG_LABEL, w as propagationOf, wt as backlogEnd, x as judgeKind, xt as REDIS, y as deletionsOf, yt as OUTSIDE, z as intakeOf } from "./judge-C7Zr4KoS.js";
|
|
2
2
|
import { valueIn } from "@mark1russell7/quantity";
|
|
3
3
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -2223,6 +2223,413 @@ const ztunnelCertificates = async (cluster) => {
|
|
|
2223
2223
|
return out;
|
|
2224
2224
|
};
|
|
2225
2225
|
//#endregion
|
|
2226
|
+
//#region src/kubevip.ts
|
|
2227
|
+
/**
|
|
2228
|
+
* This module installs kube-vip 1.3.1 in a kind cluster (SYS-51). The install gives a Service of type
|
|
2229
|
+
* LoadBalancer an address on the Docker network of the nodes: the VIP. In ARP mode, one node holds
|
|
2230
|
+
* the VIP on its link, and it sends a gratuitous ARP for it each `vip_arpRate`. A Lease elects that
|
|
2231
|
+
* node. Each value of the install is in `kubeVipObjects`, with its reason.
|
|
2232
|
+
*
|
|
2233
|
+
* The lab uses a VIP of a Service, not a VIP of the control plane. A VIP of the control plane needs
|
|
2234
|
+
* three control-plane nodes and TLS on port 6443. A VIP of a Service needs one DaemonSet on the
|
|
2235
|
+
* workers, and it serves the server of the cell.
|
|
2236
|
+
*
|
|
2237
|
+
* Each term of the election comes from the source of v1.3.1:
|
|
2238
|
+
* - With `svc_election` off, one election of the lease `plndr-svcs-lock` gives each VIP to one
|
|
2239
|
+
* node (pkg/manager/worker/common.go, `GlobalLeader`). The lease is in the namespace of the pod.
|
|
2240
|
+
* - The election is `leaderelection.RunOrDie` of client-go v0.37.1, with a Lease lock and
|
|
2241
|
+
* `ReleaseOnCancel` (pkg/election/election.go). The identity is the name of the node.
|
|
2242
|
+
* - A leader that loses its lease stops the process (`OnStoppedLeading`), and the kubelet starts it again.
|
|
2243
|
+
* - The ARP manager sends a gratuitous ARP for each VIP at each tick of one ticker of
|
|
2244
|
+
* `vip_arpRate`. A new VIP gets its first one at the next tick, not at once (pkg/arp/arp.go).
|
|
2245
|
+
*/
|
|
2246
|
+
/** This version is the release of kube-vip of the lab, from 2026-10-19: the tag of its image. */
|
|
2247
|
+
const KUBE_VIP_VERSION = "1.3.1";
|
|
2248
|
+
/**
|
|
2249
|
+
* This image is kube-vip 1.3.1 for linux/amd64, pinned by the digest of its manifest. The digest of
|
|
2250
|
+
* the index of the tag is sha256:1962f9daf226503f7e6b107f595cb28b7917fd2d0113957075e19a3e5a6c1e33.
|
|
2251
|
+
* The manifest of one platform loads into kind with no index.
|
|
2252
|
+
*/
|
|
2253
|
+
const KUBE_VIP_IMAGE = "ghcr.io/kube-vip/kube-vip:v1.3.1@sha256:47353c7bde7fec21642cf621bc3fda2b999c051342912efda468999a399be667";
|
|
2254
|
+
/** This is the name of kube-vip: of its ServiceAccount, its DaemonSet and its container. */
|
|
2255
|
+
const KUBE_VIP = "kube-vip";
|
|
2256
|
+
/** This is the Lease of the election of the VIPs of the Services (`svc_leasename`, cmd/kube-vip.go of v1.3.1). */
|
|
2257
|
+
const KUBE_VIP_LEASE = {
|
|
2258
|
+
namespace: "kube-system",
|
|
2259
|
+
name: "plndr-svcs-lock"
|
|
2260
|
+
};
|
|
2261
|
+
/**
|
|
2262
|
+
* These are the times of the election, as the install sets them. Each one is the default of the
|
|
2263
|
+
* flags of v1.3.1 (cmd/kube-vip.go, lines 91–94). They are a lease of 15 s, a renew deadline of 10 s
|
|
2264
|
+
* and a retry period of 2 s. The install sets each one in the environment, thus a new default cannot
|
|
2265
|
+
* change a cell.
|
|
2266
|
+
*/
|
|
2267
|
+
const KUBE_VIP_ELECTION = {
|
|
2268
|
+
lease: {
|
|
2269
|
+
value: 15,
|
|
2270
|
+
unit: "s"
|
|
2271
|
+
},
|
|
2272
|
+
renew: {
|
|
2273
|
+
value: 10,
|
|
2274
|
+
unit: "s"
|
|
2275
|
+
},
|
|
2276
|
+
retry: {
|
|
2277
|
+
value: 2,
|
|
2278
|
+
unit: "s"
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
/** This time is the interval of the gratuitous ARPs, the default of the flag `arpRate` of v1.3.1 (cmd/kube-vip.go, line 61). */
|
|
2282
|
+
const KUBE_VIP_ARP_RATE = {
|
|
2283
|
+
value: 3e3,
|
|
2284
|
+
unit: "ms"
|
|
2285
|
+
};
|
|
2286
|
+
/** This label of a node selects the nodes of kube-vip: the DaemonSet has a pod only on a node with it. */
|
|
2287
|
+
const VIP_LABEL = "lab.systems/vip";
|
|
2288
|
+
/** This label of a pod of kube-vip selects them. */
|
|
2289
|
+
const APP = { "app.kubernetes.io/name": KUBE_VIP };
|
|
2290
|
+
const whole$1 = (amount) => String(Math.round(valueIn(amount, "s")));
|
|
2291
|
+
/**
|
|
2292
|
+
* This function gives the objects of kube-vip in kube-system. They are its ServiceAccount, its
|
|
2293
|
+
* ClusterRole and binding, and its DaemonSet. The rules of the role are those of the manifest that
|
|
2294
|
+
* v1.3.1 generates (pkg/kubevip/config_generator.go, lines 95–134). The DaemonSet:
|
|
2295
|
+
* - is on the host network, with NET_ADMIN and NET_RAW. It adds the VIP to `eth0` and sends ARP.
|
|
2296
|
+
* - has a pod only on a node with `VIP_LABEL`. Thus the lab chooses the holder of the VIP.
|
|
2297
|
+
* - sets ARP mode, the Services, one election for all VIPs, and each time of the election.
|
|
2298
|
+
* - takes the image from the node (`imagePullPolicy: Never`).
|
|
2299
|
+
*/
|
|
2300
|
+
const kubeVipObjects = (image) => {
|
|
2301
|
+
const env = {
|
|
2302
|
+
vip_arp: "true",
|
|
2303
|
+
vip_interface: "eth0",
|
|
2304
|
+
port: "6443",
|
|
2305
|
+
cp_enable: "false",
|
|
2306
|
+
svc_enable: "true",
|
|
2307
|
+
svc_election: "false",
|
|
2308
|
+
svc_leasename: KUBE_VIP_LEASE.name,
|
|
2309
|
+
vip_leaderelection: "true",
|
|
2310
|
+
vip_leaseduration: whole$1(KUBE_VIP_ELECTION.lease),
|
|
2311
|
+
vip_renewdeadline: whole$1(KUBE_VIP_ELECTION.renew),
|
|
2312
|
+
vip_retryperiod: whole$1(KUBE_VIP_ELECTION.retry),
|
|
2313
|
+
vip_arpRate: String(Math.round(valueIn(KUBE_VIP_ARP_RATE, "ms")))
|
|
2314
|
+
};
|
|
2315
|
+
const namespace = KUBE_VIP_LEASE.namespace;
|
|
2316
|
+
return [
|
|
2317
|
+
{
|
|
2318
|
+
apiVersion: "v1",
|
|
2319
|
+
kind: "ServiceAccount",
|
|
2320
|
+
metadata: {
|
|
2321
|
+
name: KUBE_VIP,
|
|
2322
|
+
namespace
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
2327
|
+
kind: "ClusterRole",
|
|
2328
|
+
metadata: { name: "system:kube-vip-role" },
|
|
2329
|
+
rules: [
|
|
2330
|
+
{
|
|
2331
|
+
apiGroups: [""],
|
|
2332
|
+
resources: ["services/status"],
|
|
2333
|
+
verbs: ["update"]
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
apiGroups: [""],
|
|
2337
|
+
resources: ["services", "endpoints"],
|
|
2338
|
+
verbs: [
|
|
2339
|
+
"list",
|
|
2340
|
+
"get",
|
|
2341
|
+
"watch",
|
|
2342
|
+
"update"
|
|
2343
|
+
]
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
apiGroups: [""],
|
|
2347
|
+
resources: ["nodes"],
|
|
2348
|
+
verbs: [
|
|
2349
|
+
"list",
|
|
2350
|
+
"get",
|
|
2351
|
+
"watch",
|
|
2352
|
+
"update",
|
|
2353
|
+
"patch"
|
|
2354
|
+
]
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
apiGroups: ["coordination.k8s.io"],
|
|
2358
|
+
resources: ["leases"],
|
|
2359
|
+
verbs: [
|
|
2360
|
+
"list",
|
|
2361
|
+
"get",
|
|
2362
|
+
"watch",
|
|
2363
|
+
"update",
|
|
2364
|
+
"create"
|
|
2365
|
+
]
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
apiGroups: ["discovery.k8s.io"],
|
|
2369
|
+
resources: ["endpointslices"],
|
|
2370
|
+
verbs: [
|
|
2371
|
+
"list",
|
|
2372
|
+
"get",
|
|
2373
|
+
"watch",
|
|
2374
|
+
"update"
|
|
2375
|
+
]
|
|
2376
|
+
},
|
|
2377
|
+
{
|
|
2378
|
+
apiGroups: [""],
|
|
2379
|
+
resources: ["pods"],
|
|
2380
|
+
verbs: ["list"]
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
apiGroups: ["networking.k8s.io"],
|
|
2384
|
+
resources: ["servicecidrs"],
|
|
2385
|
+
verbs: [
|
|
2386
|
+
"list",
|
|
2387
|
+
"get",
|
|
2388
|
+
"watch"
|
|
2389
|
+
]
|
|
2390
|
+
}
|
|
2391
|
+
]
|
|
2392
|
+
},
|
|
2393
|
+
{
|
|
2394
|
+
apiVersion: "rbac.authorization.k8s.io/v1",
|
|
2395
|
+
kind: "ClusterRoleBinding",
|
|
2396
|
+
metadata: { name: "system:kube-vip-binding" },
|
|
2397
|
+
roleRef: {
|
|
2398
|
+
apiGroup: "rbac.authorization.k8s.io",
|
|
2399
|
+
kind: "ClusterRole",
|
|
2400
|
+
name: "system:kube-vip-role"
|
|
2401
|
+
},
|
|
2402
|
+
subjects: [{
|
|
2403
|
+
kind: "ServiceAccount",
|
|
2404
|
+
name: KUBE_VIP,
|
|
2405
|
+
namespace
|
|
2406
|
+
}]
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
apiVersion: "apps/v1",
|
|
2410
|
+
kind: "DaemonSet",
|
|
2411
|
+
metadata: {
|
|
2412
|
+
name: KUBE_VIP,
|
|
2413
|
+
namespace,
|
|
2414
|
+
labels: APP
|
|
2415
|
+
},
|
|
2416
|
+
spec: {
|
|
2417
|
+
selector: { matchLabels: APP },
|
|
2418
|
+
template: {
|
|
2419
|
+
metadata: { labels: APP },
|
|
2420
|
+
spec: {
|
|
2421
|
+
serviceAccountName: KUBE_VIP,
|
|
2422
|
+
hostNetwork: true,
|
|
2423
|
+
nodeSelector: { [VIP_LABEL]: "1" },
|
|
2424
|
+
containers: [{
|
|
2425
|
+
name: KUBE_VIP,
|
|
2426
|
+
image,
|
|
2427
|
+
imagePullPolicy: "Never",
|
|
2428
|
+
args: ["manager"],
|
|
2429
|
+
env: [...Object.entries(env).map(([name, value]) => ({
|
|
2430
|
+
name,
|
|
2431
|
+
value
|
|
2432
|
+
})), {
|
|
2433
|
+
name: "vip_nodename",
|
|
2434
|
+
valueFrom: { fieldRef: { fieldPath: "spec.nodeName" } }
|
|
2435
|
+
}],
|
|
2436
|
+
securityContext: { capabilities: { add: ["NET_ADMIN", "NET_RAW"] } }
|
|
2437
|
+
}]
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
];
|
|
2443
|
+
};
|
|
2444
|
+
/** This function gives the Lease of the election, or null when it is not there yet. */
|
|
2445
|
+
const vipLeaseOf = async (cluster) => {
|
|
2446
|
+
const text = await cluster.kubectl([
|
|
2447
|
+
"-n",
|
|
2448
|
+
KUBE_VIP_LEASE.namespace,
|
|
2449
|
+
"get",
|
|
2450
|
+
"lease",
|
|
2451
|
+
KUBE_VIP_LEASE.name,
|
|
2452
|
+
"-o",
|
|
2453
|
+
"json",
|
|
2454
|
+
"--ignore-not-found"
|
|
2455
|
+
]);
|
|
2456
|
+
if (text.trim() === "") return null;
|
|
2457
|
+
const spec = JSON.parse(text).spec ?? {};
|
|
2458
|
+
const text0 = (key) => typeof spec[key] === "string" && spec[key] !== "" ? spec[key] : null;
|
|
2459
|
+
const number0 = (key) => typeof spec[key] === "number" ? spec[key] : null;
|
|
2460
|
+
return {
|
|
2461
|
+
holder: text0("holderIdentity"),
|
|
2462
|
+
acquireTime: text0("acquireTime"),
|
|
2463
|
+
renewTime: text0("renewTime"),
|
|
2464
|
+
leaseDurationSeconds: number0("leaseDurationSeconds"),
|
|
2465
|
+
leaseTransitions: number0("leaseTransitions")
|
|
2466
|
+
};
|
|
2467
|
+
};
|
|
2468
|
+
/** This function reads the Docker network of a cluster: its IPv4 subnet, and the MAC address of each container. */
|
|
2469
|
+
const vipNetworkOf = async (network) => {
|
|
2470
|
+
const [info] = JSON.parse(await command("docker", [
|
|
2471
|
+
"network",
|
|
2472
|
+
"inspect",
|
|
2473
|
+
network
|
|
2474
|
+
]));
|
|
2475
|
+
const subnet = (info?.IPAM?.Config ?? []).map((config) => config.Subnet ?? "").find((each) => /^\d+\.\d+\.\d+\.\d+\/\d+$/.test(each));
|
|
2476
|
+
if (subnet === void 0) throw new Error(`the Docker network ${network} has no IPv4 subnet`);
|
|
2477
|
+
const macs = {};
|
|
2478
|
+
for (const container of Object.values(info?.Containers ?? {})) if (container.Name !== void 0 && container.MacAddress !== void 0) macs[container.Name] = container.MacAddress.toLowerCase();
|
|
2479
|
+
return {
|
|
2480
|
+
subnet,
|
|
2481
|
+
macs
|
|
2482
|
+
};
|
|
2483
|
+
};
|
|
2484
|
+
/**
|
|
2485
|
+
* This function gives the VIP of a subnet: the sixth address from its end. Docker gives the address
|
|
2486
|
+
* of each container from the start of the subnet, thus no container takes it.
|
|
2487
|
+
*/
|
|
2488
|
+
const vipOf = (subnet) => {
|
|
2489
|
+
const [first, last] = cidrRange(subnet);
|
|
2490
|
+
const address = last - 5;
|
|
2491
|
+
if (address <= first + 1) throw new Error(`the subnet ${subnet} is too small for a VIP`);
|
|
2492
|
+
return [
|
|
2493
|
+
24,
|
|
2494
|
+
16,
|
|
2495
|
+
8,
|
|
2496
|
+
0
|
|
2497
|
+
].map((shift) => Math.floor(address / 2 ** shift) % 256).join(".");
|
|
2498
|
+
};
|
|
2499
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2500
|
+
/** This function gives the pods of kube-vip that are not deleted. */
|
|
2501
|
+
const kubeVipPods = async (cluster) => {
|
|
2502
|
+
return JSON.parse(await cluster.kubectl([
|
|
2503
|
+
"-n",
|
|
2504
|
+
KUBE_VIP_LEASE.namespace,
|
|
2505
|
+
"get",
|
|
2506
|
+
"pods",
|
|
2507
|
+
"-l",
|
|
2508
|
+
`app.kubernetes.io/name=${KUBE_VIP}`,
|
|
2509
|
+
"-o",
|
|
2510
|
+
"json"
|
|
2511
|
+
])).items.filter((pod) => pod.metadata.deletionTimestamp === void 0).map((pod) => {
|
|
2512
|
+
const statuses = pod.status?.containerStatuses ?? [];
|
|
2513
|
+
return {
|
|
2514
|
+
name: pod.metadata.name,
|
|
2515
|
+
node: pod.spec.nodeName ?? "",
|
|
2516
|
+
ready: statuses.length > 0 && statuses.every((status) => status.ready),
|
|
2517
|
+
restarts: statuses.reduce((sum, status) => sum + (status.restartCount ?? 0), 0)
|
|
2518
|
+
};
|
|
2519
|
+
});
|
|
2520
|
+
};
|
|
2521
|
+
/** This function gives the nodes with a ready pod of kube-vip that is not deleted. */
|
|
2522
|
+
const kubeVipReady = async (cluster) => (await kubeVipPods(cluster)).filter((pod) => pod.ready && pod.node !== "").map((pod) => pod.node).sort();
|
|
2523
|
+
/** This function waits until a condition gives null, and gives null, or the last reason after `within` ms. */
|
|
2524
|
+
const until = async (within, check) => {
|
|
2525
|
+
const end = Date.now() + within;
|
|
2526
|
+
let why = await check();
|
|
2527
|
+
while (why !== null && Date.now() < end) {
|
|
2528
|
+
await sleep$1(500);
|
|
2529
|
+
why = await check();
|
|
2530
|
+
}
|
|
2531
|
+
return why;
|
|
2532
|
+
};
|
|
2533
|
+
const label = (cluster, node, on) => cluster.kubectl([
|
|
2534
|
+
"label",
|
|
2535
|
+
"node",
|
|
2536
|
+
node,
|
|
2537
|
+
on ? `${VIP_LABEL}=1` : `${VIP_LABEL}-`,
|
|
2538
|
+
...on ? ["--overwrite"] : []
|
|
2539
|
+
]);
|
|
2540
|
+
/**
|
|
2541
|
+
* This function makes `holder` the holder of the VIP, with a ready pod of kube-vip on each node of
|
|
2542
|
+
* `nodes`. It gives null, or why it could not in `within` ms. If another node holds the lease, the
|
|
2543
|
+
* function takes the label from each other node. The DaemonSet then deletes their pods. The leader
|
|
2544
|
+
* gives back its lease at its stop (`ReleaseOnCancel`). The function gives each label back after
|
|
2545
|
+
* `holder` takes the lease.
|
|
2546
|
+
*
|
|
2547
|
+
* A leader that lost its lease restarted its container. The kubelet gives a container a longer
|
|
2548
|
+
* back-off at each restart, thus the function first deletes each pod of kube-vip with a restart.
|
|
2549
|
+
* The DaemonSet then makes a new pod with no back-off.
|
|
2550
|
+
*/
|
|
2551
|
+
const holdVip = async (cluster, holder, nodes, within = 18e4) => {
|
|
2552
|
+
const others = nodes.filter((node) => node !== holder);
|
|
2553
|
+
for (const pod of await kubeVipPods(cluster)) if (pod.restarts > 0) await cluster.kubectl([
|
|
2554
|
+
"-n",
|
|
2555
|
+
KUBE_VIP_LEASE.namespace,
|
|
2556
|
+
"delete",
|
|
2557
|
+
"pod",
|
|
2558
|
+
pod.name,
|
|
2559
|
+
"--wait=true",
|
|
2560
|
+
"--timeout=120s"
|
|
2561
|
+
]);
|
|
2562
|
+
await label(cluster, holder, true);
|
|
2563
|
+
const own = await until(within, async () => (await kubeVipReady(cluster)).includes(holder) ? null : `kube-vip on ${holder} is not ready`);
|
|
2564
|
+
if (own !== null) return own;
|
|
2565
|
+
const held = async () => {
|
|
2566
|
+
const lease = await vipLeaseOf(cluster);
|
|
2567
|
+
return lease?.holder === holder ? null : `the lease ${KUBE_VIP_LEASE.name} is held by ${lease?.holder ?? "no node"}, not ${holder}`;
|
|
2568
|
+
};
|
|
2569
|
+
if (await held() !== null) {
|
|
2570
|
+
for (const node of others) await label(cluster, node, false);
|
|
2571
|
+
const moved = await until(within, held);
|
|
2572
|
+
if (moved !== null) return moved;
|
|
2573
|
+
}
|
|
2574
|
+
for (const node of others) await label(cluster, node, true);
|
|
2575
|
+
return until(within, async () => {
|
|
2576
|
+
const ready = await kubeVipReady(cluster);
|
|
2577
|
+
const missing = nodes.filter((node) => !ready.includes(node));
|
|
2578
|
+
return missing.length === 0 ? null : `kube-vip is not ready on ${missing.join(", ")}`;
|
|
2579
|
+
});
|
|
2580
|
+
};
|
|
2581
|
+
/**
|
|
2582
|
+
* This function installs kube-vip in a cluster, with its image on each node (`cluster.load`), and
|
|
2583
|
+
* makes `holder` the holder of the VIP. A failed install gives the pods of kube-vip and the tail of
|
|
2584
|
+
* their logs.
|
|
2585
|
+
*/
|
|
2586
|
+
const installKubeVip = async (cluster, image, holder, nodes) => {
|
|
2587
|
+
try {
|
|
2588
|
+
await label(cluster, holder, true);
|
|
2589
|
+
await cluster.apply(kubeVipObjects(image));
|
|
2590
|
+
const why = await holdVip(cluster, holder, nodes);
|
|
2591
|
+
if (why !== null) throw new Error(why);
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
const state = async (args) => (await cluster.kubectl([
|
|
2594
|
+
"-n",
|
|
2595
|
+
KUBE_VIP_LEASE.namespace,
|
|
2596
|
+
...args
|
|
2597
|
+
]).catch((failure) => String(failure))).slice(-4e3);
|
|
2598
|
+
const pods = await state([
|
|
2599
|
+
"get",
|
|
2600
|
+
"pods",
|
|
2601
|
+
"-l",
|
|
2602
|
+
`app.kubernetes.io/name=${KUBE_VIP}`,
|
|
2603
|
+
"-o",
|
|
2604
|
+
"wide"
|
|
2605
|
+
]);
|
|
2606
|
+
const logs = await state([
|
|
2607
|
+
"logs",
|
|
2608
|
+
"-l",
|
|
2609
|
+
`app.kubernetes.io/name=${KUBE_VIP}`,
|
|
2610
|
+
"--tail=40",
|
|
2611
|
+
"--prefix"
|
|
2612
|
+
]);
|
|
2613
|
+
throw new Error(`the install of kube-vip failed: ${String(error)}\nthe pods of kube-vip:\n${pods}\nthe logs:\n${logs}`);
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
/**
|
|
2617
|
+
* This function waits until the Service of a VIP has the VIP in its status: kube-vip took it. It
|
|
2618
|
+
* gives null, or why not in `within` ms.
|
|
2619
|
+
*/
|
|
2620
|
+
const vipServed = (cluster, namespace, service, vip, within = 12e4) => until(within, async () => {
|
|
2621
|
+
const ingress = (await cluster.kubectl([
|
|
2622
|
+
"-n",
|
|
2623
|
+
namespace,
|
|
2624
|
+
"get",
|
|
2625
|
+
"service",
|
|
2626
|
+
service,
|
|
2627
|
+
"-o",
|
|
2628
|
+
"jsonpath={.status.loadBalancer.ingress[*].ip}"
|
|
2629
|
+
])).trim();
|
|
2630
|
+
return ingress.split(/\s+/).includes(vip) ? null : `the Service ${service} has the ingress ${ingress || "none"}, not ${vip}`;
|
|
2631
|
+
});
|
|
2632
|
+
//#endregion
|
|
2226
2633
|
//#region src/lds.ts
|
|
2227
2634
|
/**
|
|
2228
2635
|
* This filter of RE2 selects the two counters of the listener manager of Envoy that an update of LDS
|
|
@@ -2335,6 +2742,11 @@ const openKindLab = async (cluster, image) => {
|
|
|
2335
2742
|
const opened = [];
|
|
2336
2743
|
let watcher = null;
|
|
2337
2744
|
let prober = null;
|
|
2745
|
+
let outsiders = [];
|
|
2746
|
+
const stopOutside = async () => {
|
|
2747
|
+
for (const container of outsiders) await container.stop({ timeout: 0 }).catch(() => void 0);
|
|
2748
|
+
outsiders = [];
|
|
2749
|
+
};
|
|
2338
2750
|
try {
|
|
2339
2751
|
for (const node of cluster.nodes) {
|
|
2340
2752
|
const agent = await new Shared(image).withPidMode(`container:${node}`).withPrivilegedMode().withNetworkMode("none").withEnvironment({ NODE_NAME: node }).withCommand([
|
|
@@ -2401,7 +2813,8 @@ const openKindLab = async (cluster, image) => {
|
|
|
2401
2813
|
return [
|
|
2402
2814
|
...opened,
|
|
2403
2815
|
...watcher === null ? [] : [watcher],
|
|
2404
|
-
...prober === null ? [] : [prober]
|
|
2816
|
+
...prober === null ? [] : [prober],
|
|
2817
|
+
...outsiders
|
|
2405
2818
|
].map((container) => container.getName().replace(/^\//, ""));
|
|
2406
2819
|
},
|
|
2407
2820
|
take() {
|
|
@@ -2448,6 +2861,21 @@ const openKindLab = async (cluster, image) => {
|
|
|
2448
2861
|
})).start();
|
|
2449
2862
|
await Promise.race([ready, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("no begin line from the prober in 60 s")), 6e4))]);
|
|
2450
2863
|
},
|
|
2864
|
+
async outside(spec) {
|
|
2865
|
+
await stopOutside();
|
|
2866
|
+
if (spec === null) return;
|
|
2867
|
+
for (let k = 0; k < spec.count; k += 1) {
|
|
2868
|
+
const client = await new GenericContainer(image).withNetworkMode(cluster.network).withEnvironment({
|
|
2869
|
+
TARGET: spec.target,
|
|
2870
|
+
EVERY_MS: String(valueIn(spec.every, "ms")),
|
|
2871
|
+
TIMEOUT_MS: String(valueIn(spec.timeout, "ms")),
|
|
2872
|
+
POD_NAME: `${OUTSIDE}-${k}`,
|
|
2873
|
+
NODE_NAME: OUTSIDE,
|
|
2874
|
+
ARP_WATCH: spec.arp
|
|
2875
|
+
}).withCommand(["node", "client.mjs"]).withLogConsumer(consume((line) => push(line))).start();
|
|
2876
|
+
outsiders.push(client);
|
|
2877
|
+
}
|
|
2878
|
+
},
|
|
2451
2879
|
async stopProbe() {
|
|
2452
2880
|
if (prober !== null) await prober.stop({ timeout: 0 }).catch(() => void 0);
|
|
2453
2881
|
prober = null;
|
|
@@ -2634,6 +3062,7 @@ const openKindLab = async (cluster, image) => {
|
|
|
2634
3062
|
},
|
|
2635
3063
|
async close() {
|
|
2636
3064
|
await lab.stopProbe();
|
|
3065
|
+
await stopOutside();
|
|
2637
3066
|
if (watcher !== null) await watcher.stop({ timeout: 0 }).catch(() => void 0);
|
|
2638
3067
|
watcher = null;
|
|
2639
3068
|
for (const container of opened.reverse()) await container.stop({ timeout: 0 }).catch(() => void 0);
|
|
@@ -2853,14 +3282,53 @@ const preStopOf = (preStop, grace) => {
|
|
|
2853
3282
|
`sleep ${preStop}`
|
|
2854
3283
|
] } } } };
|
|
2855
3284
|
};
|
|
3285
|
+
/** This affinity keeps a pod in the zone that a zone loss keeps. */
|
|
3286
|
+
const keptZone = { nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchExpressions: [{
|
|
3287
|
+
key: ZONE_LABEL,
|
|
3288
|
+
operator: "NotIn",
|
|
3289
|
+
values: [ZONE_LOST]
|
|
3290
|
+
}] }] } } };
|
|
3291
|
+
/**
|
|
3292
|
+
* This function gives the Service of a VIP (`kubevip.ts`). It is of type LoadBalancer, with the
|
|
3293
|
+
* class of kube-vip and the address of the VIP in its annotation. The policy `Cluster` lets each node
|
|
3294
|
+
* send a request to a replica on another node. The holder writes the address to the status of the
|
|
3295
|
+
* Service. Then kube-proxy has the rules of the address on each node.
|
|
3296
|
+
*/
|
|
3297
|
+
const vipService = (namespace, vip, ports) => ({
|
|
3298
|
+
apiVersion: "v1",
|
|
3299
|
+
kind: "Service",
|
|
3300
|
+
metadata: {
|
|
3301
|
+
name: "lab",
|
|
3302
|
+
namespace,
|
|
3303
|
+
annotations: { [VIP_ANNOTATION]: vip }
|
|
3304
|
+
},
|
|
3305
|
+
spec: {
|
|
3306
|
+
type: "LoadBalancer",
|
|
3307
|
+
loadBalancerClass: LB_CLASS,
|
|
3308
|
+
externalTrafficPolicy: "Cluster",
|
|
3309
|
+
selector: { app: "lab" },
|
|
3310
|
+
ports
|
|
3311
|
+
}
|
|
3312
|
+
});
|
|
2856
3313
|
/**
|
|
2857
3314
|
* These are the objects of the server: a Service of type ClusterIP, and the Deployment with the
|
|
2858
3315
|
* settings of the cell. The image is the lab image for the service, or the image of grpc-go.
|
|
3316
|
+
*
|
|
3317
|
+
* With a VIP (`KindCell.vip`), the Service is of type LoadBalancer, with the address `vip` and the
|
|
3318
|
+
* class of kube-vip (`vipService`). No replica is in the lost zone. Thus a zone loss takes away
|
|
3319
|
+
* only the holder of the VIP.
|
|
2859
3320
|
*/
|
|
2860
|
-
const serverObjects = (cell, namespace, image, version = 1) => {
|
|
3321
|
+
const serverObjects = (cell, namespace, image, version = 1, vip = null) => {
|
|
2861
3322
|
const preStop = whole(cell.pod.preStop, "the preStop sleep");
|
|
2862
3323
|
const grace = whole(cell.pod.grace, "the grace period");
|
|
2863
|
-
|
|
3324
|
+
if (cell.vip !== void 0 && vip === null) throw new Error(`${cell.name}: a cell with a VIP needs the address of the VIP`);
|
|
3325
|
+
const ports = [{
|
|
3326
|
+
name: stackOf(cell) === "grpc" ? "grpc" : "http",
|
|
3327
|
+
port: 80,
|
|
3328
|
+
targetPort: PORT
|
|
3329
|
+
}];
|
|
3330
|
+
const zoneLoss = cell.trigger.kind === "zone-loss";
|
|
3331
|
+
return [vip === null ? {
|
|
2864
3332
|
apiVersion: "v1",
|
|
2865
3333
|
kind: "Service",
|
|
2866
3334
|
metadata: {
|
|
@@ -2869,13 +3337,9 @@ const serverObjects = (cell, namespace, image, version = 1) => {
|
|
|
2869
3337
|
},
|
|
2870
3338
|
spec: {
|
|
2871
3339
|
selector: { app: "lab" },
|
|
2872
|
-
ports
|
|
2873
|
-
name: stackOf(cell) === "grpc" ? "grpc" : "http",
|
|
2874
|
-
port: 80,
|
|
2875
|
-
targetPort: PORT
|
|
2876
|
-
}]
|
|
3340
|
+
ports
|
|
2877
3341
|
}
|
|
2878
|
-
}, {
|
|
3342
|
+
} : vipService(namespace, vip, ports), {
|
|
2879
3343
|
apiVersion: "apps/v1",
|
|
2880
3344
|
kind: "Deployment",
|
|
2881
3345
|
metadata: {
|
|
@@ -2908,12 +3372,13 @@ const serverObjects = (cell, namespace, image, version = 1) => {
|
|
|
2908
3372
|
topologyKey: "kubernetes.io/hostname",
|
|
2909
3373
|
whenUnsatisfiable: "ScheduleAnyway",
|
|
2910
3374
|
labelSelector: { matchLabels: { app: "lab" } }
|
|
2911
|
-
}, ...cell.
|
|
3375
|
+
}, ...zoneLoss && cell.vip === void 0 ? [{
|
|
2912
3376
|
maxSkew: 1,
|
|
2913
3377
|
topologyKey: ZONE_LABEL,
|
|
2914
3378
|
whenUnsatisfiable: "DoNotSchedule",
|
|
2915
3379
|
labelSelector: { matchLabels: { app: "lab" } }
|
|
2916
3380
|
}] : []],
|
|
3381
|
+
...zoneLoss && cell.vip !== void 0 ? { affinity: keptZone } : {},
|
|
2917
3382
|
containers: [{
|
|
2918
3383
|
name: "app",
|
|
2919
3384
|
image,
|
|
@@ -2955,11 +3420,7 @@ const clientObjects = (cell, namespace, image, target) => [{
|
|
|
2955
3420
|
operator: "Exists",
|
|
2956
3421
|
effect: "NoSchedule"
|
|
2957
3422
|
}],
|
|
2958
|
-
...cell.trigger.kind === "zone-loss" ? { affinity:
|
|
2959
|
-
key: ZONE_LABEL,
|
|
2960
|
-
operator: "NotIn",
|
|
2961
|
-
values: [ZONE_LOST]
|
|
2962
|
-
}] }] } } } } : {},
|
|
3423
|
+
...cell.trigger.kind === "zone-loss" ? { affinity: keptZone } : {},
|
|
2963
3424
|
containers: [{
|
|
2964
3425
|
name: CLIENT,
|
|
2965
3426
|
image,
|
|
@@ -3288,7 +3749,7 @@ const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !
|
|
|
3288
3749
|
* This function gives the key of the cluster of a cell: its kube-proxy mode, its node count, and the
|
|
3289
3750
|
* flavor of its mesh. A zone loss has clusters of its own, with a zone on each node.
|
|
3290
3751
|
*/
|
|
3291
|
-
const clusterKey = (cell) => cell.mesh !== void 0 ? `${cell.mode}-${cell.workers}-${cell.mesh.flavor}` : zoned(cell) ? `${cell.mode}-${cell.workers}-zones` : `${cell.mode}-${cell.workers}
|
|
3752
|
+
const clusterKey = (cell) => (cell.mesh !== void 0 ? `${cell.mode}-${cell.workers}-${cell.mesh.flavor}` : zoned(cell) ? `${cell.mode}-${cell.workers}-zones` : `${cell.mode}-${cell.workers}`) + (cell.vip === void 0 ? "" : "-vip");
|
|
3292
3753
|
/** This function tells if a cell needs a cluster with zones: a zone loss. */
|
|
3293
3754
|
const zoned = (cell) => cell.trigger.kind === "zone-loss";
|
|
3294
3755
|
/**
|
|
@@ -3353,8 +3814,12 @@ const unsteady = (ctx) => {
|
|
|
3353
3814
|
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"}`;
|
|
3354
3815
|
}
|
|
3355
3816
|
const nodes = new Set([...tracker.clients.values()].map((client) => client.node));
|
|
3356
|
-
const clientNodes = ctx.cluster.nodes.length - (cell.trigger.kind === "zone-loss" ? 1 : 0);
|
|
3817
|
+
const clientNodes = ctx.cluster.nodes.length - (cell.trigger.kind === "zone-loss" ? 1 : 0) + (cell.vip === void 0 ? 0 : 1);
|
|
3357
3818
|
if (nodes.size !== clientNodes) return `clients answer on ${nodes.size} nodes, not ${clientNodes}`;
|
|
3819
|
+
if (cell.vip !== void 0) {
|
|
3820
|
+
const outside = [...tracker.clients.values()].filter((client) => client.node === OUTSIDE).length;
|
|
3821
|
+
if (outside !== cell.vip.clients) return `${outside} clients outside answer, not ${cell.vip.clients}`;
|
|
3822
|
+
}
|
|
3358
3823
|
for (const [pod, client] of tracker.clients) {
|
|
3359
3824
|
if (tracker.last - client.ok > 1e3) return `client ${pod} has no answer in the last second`;
|
|
3360
3825
|
if (tracker.last - client.failed < 2e3) return `client ${pod} failed in the last 2 s`;
|
|
@@ -3457,7 +3922,7 @@ const quiet = async (ctx, ours, within = QUIET_WITHIN) => {
|
|
|
3457
3922
|
* clients. A client of gRPC starts after the server is ready: each failed connect of a channel makes
|
|
3458
3923
|
* the backoff of grpc-js longer, up to 120 s.
|
|
3459
3924
|
*/
|
|
3460
|
-
const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
3925
|
+
const deploy = async (lab, cluster, cell, namespace, images, vip) => {
|
|
3461
3926
|
await cluster.apply([namespaceObject(cell, namespace)]);
|
|
3462
3927
|
await lab.watch([namespace], { ztunnel: followsZtunnel(cell) });
|
|
3463
3928
|
if (hasRedis(cell)) {
|
|
@@ -3473,7 +3938,7 @@ const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
|
3473
3938
|
}
|
|
3474
3939
|
const grpc = stackOf(cell) === "grpc";
|
|
3475
3940
|
if (grpc && images.grpc === void 0) throw new Error(`${cell.name}: the cluster has no image of grpc-go`);
|
|
3476
|
-
await cluster.apply(serverObjects(cell, namespace, grpc ? images.grpc : lab.image));
|
|
3941
|
+
await cluster.apply(serverObjects(cell, namespace, grpc ? images.grpc : lab.image, 1, cell.vip === void 0 ? null : vip));
|
|
3477
3942
|
const clusterIp = (await cluster.kubectl([
|
|
3478
3943
|
"-n",
|
|
3479
3944
|
namespace,
|
|
@@ -3502,6 +3967,18 @@ const deploy = async (lab, cluster, cell, namespace, images) => {
|
|
|
3502
3967
|
`daemonset/${CLIENT}`,
|
|
3503
3968
|
"--timeout=300s"
|
|
3504
3969
|
]);
|
|
3970
|
+
if (cell.vip !== void 0) {
|
|
3971
|
+
if (vip === null) throw new Error(`${cell.name}: the cluster has no VIP`);
|
|
3972
|
+
const served = await vipServed(cluster, namespace, "lab", vip);
|
|
3973
|
+
if (served !== null) throw new Error(`${cell.name}: ${served}`);
|
|
3974
|
+
await lab.outside({
|
|
3975
|
+
target: `${vip}:80`,
|
|
3976
|
+
every: cell.workload.every,
|
|
3977
|
+
timeout: cell.workload.timeout,
|
|
3978
|
+
count: cell.vip.clients,
|
|
3979
|
+
arp: vip
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3505
3982
|
await lab.rules({
|
|
3506
3983
|
mode: cluster.scenario.dataplane,
|
|
3507
3984
|
ns: namespace,
|
|
@@ -3938,6 +4415,39 @@ const doStep = async (ctx, step, replicas, background) => {
|
|
|
3938
4415
|
};
|
|
3939
4416
|
/** This function gives the node of the lost zone: the last worker (`zoneOf`). */
|
|
3940
4417
|
const lostNodeOf = (cluster) => cluster.nodes.at(-1);
|
|
4418
|
+
/** This function gives the workers of a cluster: each node but the control plane, the first (`nodeNames`). */
|
|
4419
|
+
const workersOf = (cluster) => cluster.nodes.slice(1);
|
|
4420
|
+
/**
|
|
4421
|
+
* This function adds a line `vip` to the lines. The line has the Lease of the VIP and the MAC address
|
|
4422
|
+
* of each node on the Docker network. It also has the wall clock of the runner with its CLOCK_MONOTONIC. The times of the
|
|
4423
|
+
* Lease are on the wall clock, and each stamp of the lab is on CLOCK_MONOTONIC. The containers of
|
|
4424
|
+
* the runner share one kernel, thus the pair converts one into the other.
|
|
4425
|
+
*/
|
|
4426
|
+
const recordVip = async (ctx) => {
|
|
4427
|
+
const wall = Date.now();
|
|
4428
|
+
const t = Number(process.hrtime.bigint()) / 1e6;
|
|
4429
|
+
const lease = await vipLeaseOf(ctx.cluster);
|
|
4430
|
+
const { macs } = await vipNetworkOf(ctx.cluster.network);
|
|
4431
|
+
const log = await ctx.cluster.kubectl([
|
|
4432
|
+
"-n",
|
|
4433
|
+
KUBE_VIP_LEASE.namespace,
|
|
4434
|
+
"logs",
|
|
4435
|
+
"-l",
|
|
4436
|
+
"app.kubernetes.io/name=kube-vip",
|
|
4437
|
+
"--tail=30",
|
|
4438
|
+
"--prefix",
|
|
4439
|
+
"--timestamps"
|
|
4440
|
+
]).catch((error) => String(error));
|
|
4441
|
+
ctx.lab.record({
|
|
4442
|
+
ev: "vip",
|
|
4443
|
+
t,
|
|
4444
|
+
wall,
|
|
4445
|
+
vip: ctx.vip,
|
|
4446
|
+
lease,
|
|
4447
|
+
macs,
|
|
4448
|
+
log: log.slice(-6e3)
|
|
4449
|
+
});
|
|
4450
|
+
};
|
|
3941
4451
|
/** This function puts the replicas of a node first, and keeps the sequence of the others. */
|
|
3942
4452
|
const lostFirst = (replicas, node) => [...replicas.filter((replica) => replica.node === node), ...replicas.filter((replica) => replica.node !== node)];
|
|
3943
4453
|
/** This function tells if a run is over: the trigger is done, and the cell is at rest again. */
|
|
@@ -3969,7 +4479,6 @@ const stoppedOf = (cell, replicas, target, lines) => deletes(cell) ? replicas.fi
|
|
|
3969
4479
|
const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
3970
4480
|
const { lab, cell, cluster, namespace } = ctx;
|
|
3971
4481
|
const plan = planOf(cell);
|
|
3972
|
-
const atRest = await steady(ctx, 3e5);
|
|
3973
4482
|
const empty = (why, start) => ({
|
|
3974
4483
|
env,
|
|
3975
4484
|
index,
|
|
@@ -3988,6 +4497,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
3988
4497
|
lines: lab.take(),
|
|
3989
4498
|
dropped: {}
|
|
3990
4499
|
});
|
|
4500
|
+
const atRest = await steady(ctx, 3e5);
|
|
3991
4501
|
if (atRest !== null) return empty(`the cell was not at rest: ${atRest}`, await lab.now());
|
|
3992
4502
|
await probeThePods(ctx);
|
|
3993
4503
|
await pollListeners(ctx);
|
|
@@ -3999,11 +4509,16 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
3999
4509
|
const zoneLoss = cell.trigger.kind === "zone-loss";
|
|
4000
4510
|
const lost = zoneLoss ? await nodeSteady(cluster, lostNodeOf(cluster), 3e5) : null;
|
|
4001
4511
|
if (lost !== null) return empty(`the node of the lost zone was not at rest: ${lost}`, await lab.now());
|
|
4512
|
+
if (cell.vip !== void 0) {
|
|
4513
|
+
const held = await holdVip(cluster, lostNodeOf(cluster), workersOf(cluster));
|
|
4514
|
+
if (held !== null) return empty(`the VIP is not on ${lostNodeOf(cluster)}: ${held}`, await lab.now());
|
|
4515
|
+
await sleep(valueIn(KUBE_VIP_ARP_RATE, "ms") + valueIn(cell.workload.timeout, "ms") + 1e3);
|
|
4516
|
+
}
|
|
4002
4517
|
if (await steady(ctx, 3e5) !== null) return empty("the cell was not at rest after the wait for a quiet VM", await lab.now());
|
|
4003
4518
|
const replicas = zoneLoss ? lostFirst(replicasOf(ctx), lostNodeOf(cluster)) : replicasOf(ctx);
|
|
4004
|
-
if (zoneLoss && replicas[0]?.node !== lostNodeOf(cluster)) return empty(`no replica is on the node of the lost zone, ${lostNodeOf(cluster)}`, await lab.now());
|
|
4519
|
+
if (zoneLoss && cell.vip === void 0 && replicas[0]?.node !== lostNodeOf(cluster)) return empty(`no replica is on the node of the lost zone, ${lostNodeOf(cluster)}`, await lab.now());
|
|
4005
4520
|
const restarts = new Map(ctx.tracker.servers().map((pod) => [pod.name, pod.cs.reduce((sum, status) => sum + status.rc, 0)]));
|
|
4006
|
-
const target = hasRedis(cell) && [
|
|
4521
|
+
const target = cell.vip !== void 0 || hasRedis(cell) && [
|
|
4007
4522
|
"rst",
|
|
4008
4523
|
"blackhole",
|
|
4009
4524
|
"freeze-process",
|
|
@@ -4103,6 +4618,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
4103
4618
|
}
|
|
4104
4619
|
const end = await lab.now();
|
|
4105
4620
|
const timedOut = Date.now() >= limit;
|
|
4621
|
+
if (cell.vip !== void 0) await recordVip(ctx).catch((error) => notes.push(`the record of the VIP failed: ${String(error)}`));
|
|
4106
4622
|
for (const step of plan.restore) try {
|
|
4107
4623
|
await doStep(ctx, step, replicas, background);
|
|
4108
4624
|
} catch (error) {
|
|
@@ -4220,6 +4736,7 @@ const runKindCells = async (cells, options) => {
|
|
|
4220
4736
|
operator: await localPin(CILIUM_IMAGES.operator),
|
|
4221
4737
|
envoy: await localPin(CILIUM_IMAGES.envoy)
|
|
4222
4738
|
} : void 0;
|
|
4739
|
+
const kubeVip = cells.some((cell) => cell.vip !== void 0) ? await localPin(KUBE_VIP_IMAGE) : void 0;
|
|
4223
4740
|
const images = {
|
|
4224
4741
|
lab: image,
|
|
4225
4742
|
redis: redisImage,
|
|
@@ -4265,6 +4782,15 @@ const runKindCells = async (cells, options) => {
|
|
|
4265
4782
|
await installMesh(cluster, mesh.flavor, images.mesh);
|
|
4266
4783
|
options.log(` Istio ${mesh.flavor} installed in ${Math.round((Date.now() - began) / 100) / 10} s`);
|
|
4267
4784
|
}
|
|
4785
|
+
let vip = null;
|
|
4786
|
+
if (group[0].vip !== void 0) {
|
|
4787
|
+
if (kubeVip === void 0) throw new Error(`${group[0].name}: the run has no image of kube-vip`);
|
|
4788
|
+
await cluster.load(kubeVip);
|
|
4789
|
+
const began = Date.now();
|
|
4790
|
+
await installKubeVip(cluster, kubeVip, lostNodeOf(cluster), workersOf(cluster));
|
|
4791
|
+
vip = vipOf((await vipNetworkOf(cluster.network)).subnet);
|
|
4792
|
+
options.log(` kube-vip installed in ${Math.round((Date.now() - began) / 100) / 10} s, the VIP ${vip}`);
|
|
4793
|
+
}
|
|
4268
4794
|
lab = await openKindLab(cluster, image);
|
|
4269
4795
|
const { GenericContainer } = await import("testcontainers");
|
|
4270
4796
|
const Shared = class extends GenericContainer {
|
|
@@ -4285,7 +4811,7 @@ const runKindCells = async (cells, options) => {
|
|
|
4285
4811
|
const ztunnelEnv = cell.mesh?.ztunnel;
|
|
4286
4812
|
try {
|
|
4287
4813
|
if (ztunnelEnv !== void 0) await setZtunnelEnv(cluster, ztunnelEnv, false);
|
|
4288
|
-
const clusterIp = await deploy(lab, cluster, cell, namespace, images);
|
|
4814
|
+
const clusterIp = await deploy(lab, cluster, cell, namespace, images, vip);
|
|
4289
4815
|
for (const name of [...lab.own(), loader.getName().replace(/^\//, "")]) ours.add(name);
|
|
4290
4816
|
const ctx = {
|
|
4291
4817
|
lab,
|
|
@@ -4294,6 +4820,7 @@ const runKindCells = async (cells, options) => {
|
|
|
4294
4820
|
namespace,
|
|
4295
4821
|
tracker,
|
|
4296
4822
|
clusterIp,
|
|
4823
|
+
vip: cell.vip === void 0 ? null : vip,
|
|
4297
4824
|
loads,
|
|
4298
4825
|
loader,
|
|
4299
4826
|
idle: options.idle ?? .6,
|
|
@@ -4322,7 +4849,8 @@ const runKindCells = async (cells, options) => {
|
|
|
4322
4849
|
redis: redisImage,
|
|
4323
4850
|
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
|
|
4324
4851
|
...cell.mesh === void 0 ? {} : Object.fromEntries(MESH_IMAGES[cell.mesh.flavor].map((name) => [`istio-${name}`, ISTIO_IMAGES[name]])),
|
|
4325
|
-
...s.cni === "cilium" ? Object.fromEntries(Object.entries(CILIUM_IMAGES).map(([name, ref]) => [`cilium-${name}`, ref])) : {}
|
|
4852
|
+
...s.cni === "cilium" ? Object.fromEntries(Object.entries(CILIUM_IMAGES).map(([name, ref]) => [`cilium-${name}`, ref])) : {},
|
|
4853
|
+
...cell.vip === void 0 ? {} : { "kube-vip": KUBE_VIP_IMAGE }
|
|
4326
4854
|
}
|
|
4327
4855
|
});
|
|
4328
4856
|
entry.loads.push(...loads);
|
|
@@ -4336,6 +4864,7 @@ const runKindCells = async (cells, options) => {
|
|
|
4336
4864
|
}));
|
|
4337
4865
|
} finally {
|
|
4338
4866
|
off();
|
|
4867
|
+
await lab.outside(null).catch(() => void 0);
|
|
4339
4868
|
await lab.rules(null).catch(() => void 0);
|
|
4340
4869
|
await lab.eds(null).catch(() => void 0);
|
|
4341
4870
|
await lab.lds(null).catch(() => void 0);
|
|
@@ -4395,8 +4924,8 @@ const estimate = (cell, environments = 2) => {
|
|
|
4395
4924
|
const observed = after === null ? 0 : hold + valueIn(after, "s");
|
|
4396
4925
|
const closes = cell.prediction.kind === "ends" && cell.prediction.band !== null ? valueIn(cell.prediction.band.hi, "s") + 3 : 0;
|
|
4397
4926
|
const quiet = cell.trigger.quiet === true ? hold + 10 : 0;
|
|
4398
|
-
const each = 25 + Math.max(observed, closes, quiet, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
|
|
4399
|
-
return (cell.mesh === void 0 ? 60 : 180) * environments + (cell.runs + environments) * each;
|
|
4927
|
+
const each = 25 + (cell.vip === void 0 ? 0 : 30) + Math.max(observed, closes, quiet, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
|
|
4928
|
+
return ((cell.mesh === void 0 ? 60 : 180) + (cell.vip === void 0 ? 0 : 60)) * environments + (cell.runs + environments) * each;
|
|
4400
4929
|
};
|
|
4401
4930
|
/**
|
|
4402
4931
|
* This function gives an estimate of the time of a cell: its setup, then each run and each
|
|
@@ -4427,6 +4956,6 @@ const shardOf = (cells, index, count) => {
|
|
|
4427
4956
|
return out;
|
|
4428
4957
|
};
|
|
4429
4958
|
//#endregion
|
|
4430
|
-
export { ACCESS_LOG_FORMAT, CHANNEL_USE, CILIUM_CHART, CILIUM_CONFIG_KEYS, CILIUM_IMAGES, CILIUM_VERSION, CILIUM_WORKLOADS, CLIENT, CLIENT_ACCOUNT, CLOSE_LEAD, CONTROLLER, DATAPLANES, DNS_PORT, DOCUMENTED, DRAIN_RULES, EDS_EVERY, FLAG_CHECK, FORMAT, FORMAT_V1, HBONE_PORT, HEALTH_PORT, HELM, 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, MIDDLEBOX, NETEM_DRAIN, NETNS_DEFAULTS, NODE_IMAGES, NODE_LIFECYCLE, O04, O09, O10, O16, POD_LINK, POLICY, PORT, QUIET_LEAD, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, SPREAD_LEAD, STAGES, SYSCTL, TCP_STATE, WATCHDOG, ZONE_KEPT, ZONE_LABEL, ZONE_LOST, ZTUNNEL_PODS, backendsAt, backendsOf, backlogEnd, bandOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, certificateArgs, checkRun, cidrOverlap, cidrRange, ciliumBackendsOf, ciliumMode, ciliumValues, clientObjects, clusterFilter, compose, createCluster, cyclesOf, deletes, deletionOf, deletionWindow, deletionsOf, describeCell, dnsCommands, drainBand, dropOf, emptyOf, ensureChart, ensureTool, estimatedTime, factsOf, failuresOf, faultSpan, followsZtunnel, goDuration, hostsOf, installCilium, installMesh, intakeAfter, intakeOf, isKubeProxyMode, isTunnelState, istioOperator, judge, judgeKind, keptKernel, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeletBand, labImage, labSource, latchBand, leafCertificates, learnWindow, lintCell, listenerCountsOf, localPin, medianInterval, medianText, meshKey, middle, middleboxCommands, namesService, namespaceObject, namespaceOf, nodeLossBand, nodeNames, nodeReturnBand, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, overshootBand, pausedWindow, periodOf, placementOf, planOf, platformKey, podsAt, podsOf, policyObject, pollsListeners, predictionText, probeOf, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestBand, requestsOf, restartBand, restartOf, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverCluster, serverObjects, sessionsAt, setZtunnelEnv, sha256, shardOf, sharedGate, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, sliceReadyAt, sliceReadyOf, slicesOf, socketsOf, stackOf, stageLabImage, stallRemovalBand, stopOf, stoppingOf, sysctlCommand, tarEntry, timeSpread, toolPath, transitionOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, verdictsOf, writeResult, zoneOf, ztunnelCertificates, ztunnelEnvArgs, ztunnelPodsOf, ztunnelReplaced };
|
|
4959
|
+
export { ACCESS_LOG_FORMAT, CHANNEL_USE, CILIUM_CHART, CILIUM_CONFIG_KEYS, CILIUM_IMAGES, CILIUM_VERSION, CILIUM_WORKLOADS, CLIENT, CLIENT_ACCOUNT, CLOSE_LEAD, CONTROLLER, DATAPLANES, DNS_PORT, DOCUMENTED, DRAIN_RULES, EDS_EVERY, FLAG_CHECK, FORMAT, FORMAT_V1, HBONE_PORT, HEALTH_PORT, HELM, ISTIOCTL, ISTIO_IMAGES, ISTIO_VERSION, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, KUBE_VIP, KUBE_VIP_ARP_RATE, KUBE_VIP_ELECTION, KUBE_VIP_IMAGE, KUBE_VIP_LEASE, KUBE_VIP_VERSION, LAST_SLACK, LB_CLASS, LDS_EVERY, LDS_FILTER, LOG_LABEL, MESH_IMAGES, MESH_WORKLOADS, MIDDLEBOX, NETEM_DRAIN, NETNS_DEFAULTS, NODE_IMAGES, NODE_LIFECYCLE, O04, O09, O10, O16, OUTSIDE, POD_LINK, POLICY, PORT, QUIET_LEAD, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, SPREAD_LEAD, STAGES, SYSCTL, TCP_STATE, VIP_ANNOTATION, VIP_LABEL, WATCHDOG, ZONE_KEPT, ZONE_LABEL, ZONE_LOST, ZTUNNEL_PODS, backendsAt, backendsOf, backlogEnd, bandOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, certificateArgs, checkRun, cidrOverlap, cidrRange, ciliumBackendsOf, ciliumMode, ciliumValues, clientObjects, clusterFilter, compose, createCluster, cyclesOf, deletes, deletionOf, deletionWindow, deletionsOf, describeCell, dnsCommands, drainBand, dropOf, emptyOf, ensureChart, ensureTool, estimatedTime, factsOf, failuresOf, faultSpan, followsZtunnel, goDuration, holdVip, hostsOf, installCilium, installKubeVip, installMesh, intakeAfter, intakeOf, isKubeProxyMode, isTunnelState, istioOperator, judge, judgeKind, keptKernel, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeVipObjects, kubeVipPods, kubeVipReady, kubeletBand, labImage, labSource, latchBand, leafCertificates, learnWindow, lintCell, listenerCountsOf, localPin, medianInterval, medianText, meshKey, middle, middleboxCommands, namesService, namespaceObject, namespaceOf, nodeLossBand, nodeNames, nodeReturnBand, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, overshootBand, pausedWindow, periodOf, placementOf, planOf, platformKey, podsAt, podsOf, policyObject, pollsListeners, predictionText, probeOf, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestBand, requestsOf, restartBand, restartOf, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverCluster, serverObjects, sessionsAt, setZtunnelEnv, sha256, shardOf, sharedGate, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, sliceReadyAt, sliceReadyOf, slicesOf, socketsOf, stackOf, stageLabImage, stallRemovalBand, stopOf, stoppingOf, sysctlCommand, tarEntry, timeSpread, toolPath, transitionOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, verdictsOf, vipLeaseOf, vipNetworkOf, vipOf, vipServed, vipService, writeResult, zoneOf, ztunnelCertificates, ztunnelEnvArgs, ztunnelPodsOf, ztunnelReplaced };
|
|
4431
4960
|
|
|
4432
4961
|
//# sourceMappingURL=index.js.map
|