@mark1russell7/kind 0.13.2 → 0.13.3
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 +207 -10
- package/build/index.js +532 -82
- package/build/index.js.map +1 -1
- package/build/{judge-C7Zr4KoS.js → judge-DKrgfhAt.js} +17 -7
- package/build/judge-DKrgfhAt.js.map +1 -0
- package/build/{judge-DpdSgpjv.d.ts → judge-DhLShDUj.d.ts} +13 -3
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/package.json +3 -3
- package/build/judge-C7Zr4KoS.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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-DKrgfhAt.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";
|
|
@@ -642,6 +642,55 @@ const createCluster = async (scenario, dir) => {
|
|
|
642
642
|
return cluster;
|
|
643
643
|
};
|
|
644
644
|
//#endregion
|
|
645
|
+
//#region src/helm.ts
|
|
646
|
+
/** This function gives the path of a chart in the cache, after a download when the cache does not have it. It refuses a file whose sha256 is not the pinned value. */
|
|
647
|
+
const ensurePinnedChart = async (chart, cache = cacheFolder()) => {
|
|
648
|
+
const path = join(cache, "charts", `${chart.name}-${chart.version}.tgz`);
|
|
649
|
+
const bytes = existsSync(path) ? readFileSync(path) : await download(chart.url);
|
|
650
|
+
const found = sha256(bytes);
|
|
651
|
+
if (found !== chart.sha256) throw new Error(`the chart ${chart.name} ${chart.version} has the sha256 ${found}, not the pinned ${chart.sha256}`);
|
|
652
|
+
if (!existsSync(path)) {
|
|
653
|
+
mkdirSync(join(cache, "charts"), { recursive: true });
|
|
654
|
+
writeFileSync(path, bytes);
|
|
655
|
+
}
|
|
656
|
+
return path;
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* This function installs a pinned chart in a cluster with the values `values`. It writes the values
|
|
660
|
+
* to a file in the folder of the cluster. Helm waits for each workload of the chart, for up to 10 min.
|
|
661
|
+
*/
|
|
662
|
+
const helmInstall = async (cluster, release, chart, namespace, values, options = {}) => {
|
|
663
|
+
const helm = await ensureTool(HELM);
|
|
664
|
+
const path = await ensurePinnedChart(chart);
|
|
665
|
+
const file = join(cluster.dir, `${release}-values.json`);
|
|
666
|
+
writeFileSync(file, `${JSON.stringify(values, null, 2)}\n`);
|
|
667
|
+
const home = join(cluster.dir, "helm");
|
|
668
|
+
const env = {
|
|
669
|
+
HELM_CACHE_HOME: join(home, "cache"),
|
|
670
|
+
HELM_CONFIG_HOME: join(home, "config"),
|
|
671
|
+
HELM_DATA_HOME: join(home, "data")
|
|
672
|
+
};
|
|
673
|
+
const create = options.createNamespace === true ? ["--create-namespace"] : [];
|
|
674
|
+
await command(helm, [
|
|
675
|
+
"install",
|
|
676
|
+
release,
|
|
677
|
+
path,
|
|
678
|
+
"-n",
|
|
679
|
+
namespace,
|
|
680
|
+
...create,
|
|
681
|
+
"--kubeconfig",
|
|
682
|
+
cluster.kubeconfig,
|
|
683
|
+
"-f",
|
|
684
|
+
file,
|
|
685
|
+
"--wait",
|
|
686
|
+
"--timeout",
|
|
687
|
+
"10m"
|
|
688
|
+
], {
|
|
689
|
+
env,
|
|
690
|
+
timeout: 9e5
|
|
691
|
+
});
|
|
692
|
+
};
|
|
693
|
+
//#endregion
|
|
645
694
|
//#region src/cilium.ts
|
|
646
695
|
/**
|
|
647
696
|
* This module installs Cilium 1.20.4 in a kind cluster with Helm, from images pinned by digest.
|
|
@@ -652,6 +701,8 @@ const createCluster = async (scenario, dir) => {
|
|
|
652
701
|
const CILIUM_VERSION = "1.20.4";
|
|
653
702
|
/** This chart is the Helm chart of Cilium 1.20.4. The sha256 is of the file on `helm.cilium.io` (read on 2026-10-27). */
|
|
654
703
|
const CILIUM_CHART = {
|
|
704
|
+
name: "cilium",
|
|
705
|
+
version: CILIUM_VERSION,
|
|
655
706
|
url: "https://helm.cilium.io/cilium-1.20.4.tgz",
|
|
656
707
|
sha256: "505abff8385811df1a949776fc42e9f10697989a332acc86d6e8933efcb855ed"
|
|
657
708
|
};
|
|
@@ -712,17 +763,7 @@ const CILIUM_WORKLOADS = [
|
|
|
712
763
|
"daemonset/cilium-envoy"
|
|
713
764
|
];
|
|
714
765
|
/** This function gives the path of the chart in the cache, after a download when the cache does not have it. It refuses a file whose sha256 is not the pinned value. */
|
|
715
|
-
const ensureChart =
|
|
716
|
-
const path = join(cache, "charts", `cilium-${CILIUM_VERSION}.tgz`);
|
|
717
|
-
const bytes = existsSync(path) ? readFileSync(path) : await download(CILIUM_CHART.url);
|
|
718
|
-
const found = sha256(bytes);
|
|
719
|
-
if (found !== CILIUM_CHART.sha256) throw new Error(`the chart of Cilium has the sha256 ${found}, not the pinned ${CILIUM_CHART.sha256}`);
|
|
720
|
-
if (!existsSync(path)) {
|
|
721
|
-
mkdirSync(join(cache, "charts"), { recursive: true });
|
|
722
|
-
writeFileSync(path, bytes);
|
|
723
|
-
}
|
|
724
|
-
return path;
|
|
725
|
-
};
|
|
766
|
+
const ensureChart = (cache = cacheFolder()) => ensurePinnedChart(CILIUM_CHART, cache);
|
|
726
767
|
/**
|
|
727
768
|
* This function installs Cilium in a cluster with no CNI and no kube-proxy. Helm keeps its own
|
|
728
769
|
* files in the folder of the cluster. The function waits for each workload, for each node to be
|
|
@@ -730,34 +771,8 @@ const ensureChart = async (cache = cacheFolder()) => {
|
|
|
730
771
|
* lines of the log of the agent.
|
|
731
772
|
*/
|
|
732
773
|
const installCilium = async (cluster, images) => {
|
|
733
|
-
const helm = await ensureTool(HELM);
|
|
734
|
-
const chart = await ensureChart();
|
|
735
|
-
const file = join(cluster.dir, "cilium-values.json");
|
|
736
|
-
writeFileSync(file, `${JSON.stringify(ciliumValues(cluster.scenario.name, images), null, 2)}\n`);
|
|
737
|
-
const home = join(cluster.dir, "helm");
|
|
738
|
-
const env = {
|
|
739
|
-
HELM_CACHE_HOME: join(home, "cache"),
|
|
740
|
-
HELM_CONFIG_HOME: join(home, "config"),
|
|
741
|
-
HELM_DATA_HOME: join(home, "data")
|
|
742
|
-
};
|
|
743
774
|
try {
|
|
744
|
-
await
|
|
745
|
-
"install",
|
|
746
|
-
"cilium",
|
|
747
|
-
chart,
|
|
748
|
-
"-n",
|
|
749
|
-
"kube-system",
|
|
750
|
-
"--kubeconfig",
|
|
751
|
-
cluster.kubeconfig,
|
|
752
|
-
"-f",
|
|
753
|
-
file,
|
|
754
|
-
"--wait",
|
|
755
|
-
"--timeout",
|
|
756
|
-
"10m"
|
|
757
|
-
], {
|
|
758
|
-
env,
|
|
759
|
-
timeout: 9e5
|
|
760
|
-
});
|
|
775
|
+
await helmInstall(cluster, "cilium", CILIUM_CHART, "kube-system", ciliumValues(cluster.scenario.name, images));
|
|
761
776
|
for (const workload of CILIUM_WORKLOADS) await cluster.kubectl([
|
|
762
777
|
"-n",
|
|
763
778
|
"kube-system",
|
|
@@ -2496,7 +2511,7 @@ const vipOf = (subnet) => {
|
|
|
2496
2511
|
0
|
|
2497
2512
|
].map((shift) => Math.floor(address / 2 ** shift) % 256).join(".");
|
|
2498
2513
|
};
|
|
2499
|
-
const sleep$
|
|
2514
|
+
const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2500
2515
|
/** This function gives the pods of kube-vip that are not deleted. */
|
|
2501
2516
|
const kubeVipPods = async (cluster) => {
|
|
2502
2517
|
return JSON.parse(await cluster.kubectl([
|
|
@@ -2521,11 +2536,11 @@ const kubeVipPods = async (cluster) => {
|
|
|
2521
2536
|
/** This function gives the nodes with a ready pod of kube-vip that is not deleted. */
|
|
2522
2537
|
const kubeVipReady = async (cluster) => (await kubeVipPods(cluster)).filter((pod) => pod.ready && pod.node !== "").map((pod) => pod.node).sort();
|
|
2523
2538
|
/** This function waits until a condition gives null, and gives null, or the last reason after `within` ms. */
|
|
2524
|
-
const until = async (within, check) => {
|
|
2539
|
+
const until$1 = async (within, check) => {
|
|
2525
2540
|
const end = Date.now() + within;
|
|
2526
2541
|
let why = await check();
|
|
2527
2542
|
while (why !== null && Date.now() < end) {
|
|
2528
|
-
await sleep$
|
|
2543
|
+
await sleep$2(500);
|
|
2529
2544
|
why = await check();
|
|
2530
2545
|
}
|
|
2531
2546
|
return why;
|
|
@@ -2560,7 +2575,7 @@ const holdVip = async (cluster, holder, nodes, within = 18e4) => {
|
|
|
2560
2575
|
"--timeout=120s"
|
|
2561
2576
|
]);
|
|
2562
2577
|
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`);
|
|
2578
|
+
const own = await until$1(within, async () => (await kubeVipReady(cluster)).includes(holder) ? null : `kube-vip on ${holder} is not ready`);
|
|
2564
2579
|
if (own !== null) return own;
|
|
2565
2580
|
const held = async () => {
|
|
2566
2581
|
const lease = await vipLeaseOf(cluster);
|
|
@@ -2568,11 +2583,11 @@ const holdVip = async (cluster, holder, nodes, within = 18e4) => {
|
|
|
2568
2583
|
};
|
|
2569
2584
|
if (await held() !== null) {
|
|
2570
2585
|
for (const node of others) await label(cluster, node, false);
|
|
2571
|
-
const moved = await until(within, held);
|
|
2586
|
+
const moved = await until$1(within, held);
|
|
2572
2587
|
if (moved !== null) return moved;
|
|
2573
2588
|
}
|
|
2574
2589
|
for (const node of others) await label(cluster, node, true);
|
|
2575
|
-
return until(within, async () => {
|
|
2590
|
+
return until$1(within, async () => {
|
|
2576
2591
|
const ready = await kubeVipReady(cluster);
|
|
2577
2592
|
const missing = nodes.filter((node) => !ready.includes(node));
|
|
2578
2593
|
return missing.length === 0 ? null : `kube-vip is not ready on ${missing.join(", ")}`;
|
|
@@ -2617,7 +2632,7 @@ const installKubeVip = async (cluster, image, holder, nodes) => {
|
|
|
2617
2632
|
* This function waits until the Service of a VIP has the VIP in its status: kube-vip took it. It
|
|
2618
2633
|
* gives null, or why not in `within` ms.
|
|
2619
2634
|
*/
|
|
2620
|
-
const vipServed = (cluster, namespace, service, vip, within = 12e4) => until(within, async () => {
|
|
2635
|
+
const vipServed = (cluster, namespace, service, vip, within = 12e4) => until$1(within, async () => {
|
|
2621
2636
|
const ingress = (await cluster.kubectl([
|
|
2622
2637
|
"-n",
|
|
2623
2638
|
namespace,
|
|
@@ -2630,6 +2645,402 @@ const vipServed = (cluster, namespace, service, vip, within = 12e4) => until(wit
|
|
|
2630
2645
|
return ingress.split(/\s+/).includes(vip) ? null : `the Service ${service} has the ingress ${ingress || "none"}, not ${vip}`;
|
|
2631
2646
|
});
|
|
2632
2647
|
//#endregion
|
|
2648
|
+
//#region src/metallb.ts
|
|
2649
|
+
/**
|
|
2650
|
+
* This module installs MetalLB 0.16.1 in L2 mode in a kind cluster (SYS-53). The controller of
|
|
2651
|
+
* MetalLB gives a Service of type LoadBalancer an address from a pool: the VIP. A speaker on each
|
|
2652
|
+
* worker answers ARP for the VIP if its node owns the VIP. The VIP is on no interface of a node.
|
|
2653
|
+
* Thus a frozen speaker answers nothing.
|
|
2654
|
+
*
|
|
2655
|
+
* Each term of the owner comes from the source of the tag v0.16.1 (commit 791dfcc6):
|
|
2656
|
+
* - The owner of a VIP is the first node in the order of sha256(node + "#" + VIP), of the nodes with
|
|
2657
|
+
* a usable speaker (speaker/layer2_controller.go, lines 83–129). A usable speaker is a member of
|
|
2658
|
+
* memberlist that is not dead (internal/speakerlist/speakerlist.go, lines 303–320).
|
|
2659
|
+
* - The speakers start memberlist from `DefaultLANConfig` (speakerlist.go, line 72). A file
|
|
2660
|
+
* /etc/metallb/memberlist/config.json replaces that config (lines 77–84). The install makes no
|
|
2661
|
+
* such file, thus each value is the default of hashicorp/memberlist v0.5.1.
|
|
2662
|
+
* - Each event of memberlist (a join, a leave or an update) calls `ForceSync` (speakerlist.go, lines
|
|
2663
|
+
* 339–349). `ForceSync` reconciles each Service at once, with no delay (internal/k8s/k8s.go, lines
|
|
2664
|
+
* 181–184, and internal/k8s/controllers/service_controller_reload.go, lines 62–118).
|
|
2665
|
+
* - The owner sends a gratuitous ARP at each reconcile. It sends one at once, then one each 1100 ms
|
|
2666
|
+
* for 5 s (internal/layer2/announcer.go, lines 157–197 and 258–272).
|
|
2667
|
+
*/
|
|
2668
|
+
/** This version is the release of MetalLB of the lab: the tag v0.16.1, the commit 791dfcc63ced7a6b3e869131eae75e645150fff0. */
|
|
2669
|
+
const METALLB_VERSION = "0.16.1";
|
|
2670
|
+
/** This chart is the Helm chart of MetalLB 0.16.1. The sha256 is of the file of the release, and it is the digest in the index of the charts. */
|
|
2671
|
+
const METALLB_CHART = {
|
|
2672
|
+
name: "metallb",
|
|
2673
|
+
version: METALLB_VERSION,
|
|
2674
|
+
url: "https://github.com/metallb/metallb/releases/download/metallb-chart-0.16.1/metallb-0.16.1.tgz",
|
|
2675
|
+
sha256: "fb06bb584fcb7856f15733b2a6a2aff5b61b5c350687e341c163ae24a5938adc"
|
|
2676
|
+
};
|
|
2677
|
+
/**
|
|
2678
|
+
* These are the images of MetalLB 0.16.1 for linux/amd64, pinned by the digest of the manifest of the
|
|
2679
|
+
* platform. The lab pulls each one by its digest, tags it (`localPin`), and loads it into each node.
|
|
2680
|
+
*/
|
|
2681
|
+
const METALLB_IMAGES = {
|
|
2682
|
+
speaker: "quay.io/metallb/speaker:v0.16.1@sha256:37a98a9d1cd970051c5dededb6f922c1e6c3b90fdd0fc1350d1686e67675af0e",
|
|
2683
|
+
controller: "quay.io/metallb/controller:v0.16.1@sha256:5a3e101335f5ea2cfb0ddf51acdfa9538251e0623ffcd6cbfc1b274b7898790c"
|
|
2684
|
+
};
|
|
2685
|
+
/** This namespace holds MetalLB: its controller, its speakers and its objects. */
|
|
2686
|
+
const METALLB_NAMESPACE = "metallb-system";
|
|
2687
|
+
/** This annotation of a Service gives the controller of MetalLB the address of its VIP. */
|
|
2688
|
+
const METALLB_ANNOTATION = "metallb.io/loadBalancerIPs";
|
|
2689
|
+
/** This name is the name of the pool of the VIP and of its L2Advertisement. */
|
|
2690
|
+
const METALLB_POOL = "lab-vip";
|
|
2691
|
+
/** This annotation of the Service makes each speaker reconcile it. The owner then sends its gratuitous ARPs. */
|
|
2692
|
+
const ANNOUNCE_ANNOTATION = "lab.systems/announce";
|
|
2693
|
+
/**
|
|
2694
|
+
* These are the terms of memberlist v0.5.1 that the speakers use: the defaults of `DefaultLANConfig`
|
|
2695
|
+
* (config.go, lines 302–341).
|
|
2696
|
+
* - `probeInterval` is the interval of the probes (line 318). The awareness of a healthy member does not scale it.
|
|
2697
|
+
* - `probeTimeout` is the wait for the ack of a direct ping (line 317).
|
|
2698
|
+
* - `suspicionMult` and `suspicionMaxTimeoutMult` give the time from suspect to dead (lines 314–315).
|
|
2699
|
+
* - `pushPullInterval` is the interval of the full exchange of state (line 316).
|
|
2700
|
+
* - `gossipInterval` is the interval of the gossip (line 323).
|
|
2701
|
+
*/
|
|
2702
|
+
const MEMBERLIST = {
|
|
2703
|
+
probeInterval: {
|
|
2704
|
+
value: 1,
|
|
2705
|
+
unit: "s"
|
|
2706
|
+
},
|
|
2707
|
+
probeTimeout: {
|
|
2708
|
+
value: 500,
|
|
2709
|
+
unit: "ms"
|
|
2710
|
+
},
|
|
2711
|
+
suspicionMult: 4,
|
|
2712
|
+
suspicionMaxTimeoutMult: 6,
|
|
2713
|
+
pushPullInterval: {
|
|
2714
|
+
value: 30,
|
|
2715
|
+
unit: "s"
|
|
2716
|
+
},
|
|
2717
|
+
gossipInterval: {
|
|
2718
|
+
value: 200,
|
|
2719
|
+
unit: "ms"
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
/** These are the gratuitous ARPs of the owner at each reconcile: one at once, then one each `every` until `for` (announcer.go, lines 172–182). */
|
|
2723
|
+
const METALLB_SPAM = {
|
|
2724
|
+
every: {
|
|
2725
|
+
value: 1100,
|
|
2726
|
+
unit: "ms"
|
|
2727
|
+
},
|
|
2728
|
+
for: {
|
|
2729
|
+
value: 5,
|
|
2730
|
+
unit: "s"
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
/**
|
|
2734
|
+
* This function gives the time from suspect to dead in memberlist v0.5.1, for a cluster of `members`
|
|
2735
|
+
* members with no peer that can confirm. The terms:
|
|
2736
|
+
* - `suspicionTimeout` is SuspicionMult × max(1, log10(n)) × ProbeInterval (util.go, lines 71–76).
|
|
2737
|
+
* - `suspectNode` sets k = SuspicionMult − 2 confirmations, and k = 0 if n − 2 < k (state.go, lines 1216–1224).
|
|
2738
|
+
* - With k = 0, the timer fires at the minimum, not at the maximum (suspicion.go, lines 70–76).
|
|
2739
|
+
* With confirmations to wait for, the function gives the maximum: SuspicionMaxTimeoutMult times the minimum.
|
|
2740
|
+
*/
|
|
2741
|
+
const suspicionOf = (members, terms = MEMBERLIST) => {
|
|
2742
|
+
const min = Math.floor(Math.max(1, Math.log10(Math.max(1, members))) * 1e3) * valueIn(terms.probeInterval, "ms") * terms.suspicionMult / 1e3;
|
|
2743
|
+
return {
|
|
2744
|
+
value: (members - 2 < terms.suspicionMult - 2 ? 0 : terms.suspicionMult - 2) < 1 ? min : min * terms.suspicionMaxTimeoutMult,
|
|
2745
|
+
unit: "ms"
|
|
2746
|
+
};
|
|
2747
|
+
};
|
|
2748
|
+
/**
|
|
2749
|
+
* This function gives the Helm values of the install. The values that are not the defaults of the chart:
|
|
2750
|
+
* - No FRR and no frr-k8s: the lab uses L2 mode only, thus it needs no BGP.
|
|
2751
|
+
* - A speaker only on a node with `VIP_LABEL`, the workers. Thus memberlist has the workers as its
|
|
2752
|
+
* members, and the control plane does not answer ARP.
|
|
2753
|
+
* - The controller on the control plane. A zone loss of a worker then does not take it away.
|
|
2754
|
+
* - Each image from the node (`pullPolicy: Never`).
|
|
2755
|
+
*/
|
|
2756
|
+
const metallbValues = (images) => {
|
|
2757
|
+
const image = (local) => {
|
|
2758
|
+
const at = local.lastIndexOf(":");
|
|
2759
|
+
if (at <= 0) throw new Error(`the image ${local} has no tag`);
|
|
2760
|
+
return {
|
|
2761
|
+
repository: local.slice(0, at),
|
|
2762
|
+
tag: local.slice(at + 1),
|
|
2763
|
+
pullPolicy: "Never"
|
|
2764
|
+
};
|
|
2765
|
+
};
|
|
2766
|
+
return {
|
|
2767
|
+
frrk8s: { enabled: false },
|
|
2768
|
+
controller: {
|
|
2769
|
+
image: image(images.controller),
|
|
2770
|
+
nodeSelector: { "node-role.kubernetes.io/control-plane": "" },
|
|
2771
|
+
tolerations: [{
|
|
2772
|
+
key: "node-role.kubernetes.io/control-plane",
|
|
2773
|
+
operator: "Exists",
|
|
2774
|
+
effect: "NoSchedule"
|
|
2775
|
+
}]
|
|
2776
|
+
},
|
|
2777
|
+
speaker: {
|
|
2778
|
+
image: image(images.speaker),
|
|
2779
|
+
frr: { enabled: false },
|
|
2780
|
+
tolerateMaster: false,
|
|
2781
|
+
nodeSelector: { [VIP_LABEL]: "1" },
|
|
2782
|
+
memberlist: {
|
|
2783
|
+
enabled: true,
|
|
2784
|
+
mlBindPort: 7946
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
};
|
|
2789
|
+
/** This function gives the pool of the VIP and its L2Advertisement. The pool has only the VIP. */
|
|
2790
|
+
const metallbObjects = (vip) => [{
|
|
2791
|
+
apiVersion: "metallb.io/v1beta1",
|
|
2792
|
+
kind: "IPAddressPool",
|
|
2793
|
+
metadata: {
|
|
2794
|
+
name: METALLB_POOL,
|
|
2795
|
+
namespace: METALLB_NAMESPACE
|
|
2796
|
+
},
|
|
2797
|
+
spec: { addresses: [`${vip}/32`] }
|
|
2798
|
+
}, {
|
|
2799
|
+
apiVersion: "metallb.io/v1beta1",
|
|
2800
|
+
kind: "L2Advertisement",
|
|
2801
|
+
metadata: {
|
|
2802
|
+
name: METALLB_POOL,
|
|
2803
|
+
namespace: METALLB_NAMESPACE
|
|
2804
|
+
},
|
|
2805
|
+
spec: { ipAddressPools: [METALLB_POOL] }
|
|
2806
|
+
}];
|
|
2807
|
+
/** This function gives the nodes in the order of the election of the owner of `vip`: by sha256(node + "#" + vip), as bytes (layer2_controller.go, lines 115–120). */
|
|
2808
|
+
const ownerOrder = (nodes, vip) => {
|
|
2809
|
+
const hash = (node) => createHash("sha256").update(`${node}#${vip}`).digest();
|
|
2810
|
+
return [...nodes].sort((a, b) => Buffer.compare(hash(a), hash(b)));
|
|
2811
|
+
};
|
|
2812
|
+
const dotted = (address) => [
|
|
2813
|
+
24,
|
|
2814
|
+
16,
|
|
2815
|
+
8,
|
|
2816
|
+
0
|
|
2817
|
+
].map((shift) => Math.floor(address / 2 ** shift) % 256).join(".");
|
|
2818
|
+
/**
|
|
2819
|
+
* This function gives the VIP of a subnet for MetalLB. It looks down from the sixth address from the
|
|
2820
|
+
* end, and it gives the first address that `owner` owns, of the nodes `nodes`. Docker gives the address of each container
|
|
2821
|
+
* from the start of the subnet, thus no container takes it.
|
|
2822
|
+
*/
|
|
2823
|
+
const metallbVipOf = (subnet, owner, nodes) => {
|
|
2824
|
+
if (!nodes.includes(owner)) throw new Error(`the owner ${owner} is not one of the nodes ${nodes.join(", ")}`);
|
|
2825
|
+
const [first, last] = cidrRange(subnet);
|
|
2826
|
+
for (let address = last - 5; address > first + 1 && address > last - 260; address--) {
|
|
2827
|
+
const vip = dotted(address);
|
|
2828
|
+
if (ownerOrder(nodes, vip)[0] === owner) return vip;
|
|
2829
|
+
}
|
|
2830
|
+
throw new Error(`the subnet ${subnet} has no VIP that ${owner} owns`);
|
|
2831
|
+
};
|
|
2832
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2833
|
+
/** This function waits until a condition gives null, and gives null, or the last reason after `within` ms. */
|
|
2834
|
+
const until = async (within, check) => {
|
|
2835
|
+
const end = Date.now() + within;
|
|
2836
|
+
let why = await check();
|
|
2837
|
+
while (why !== null && Date.now() < end) {
|
|
2838
|
+
await sleep$1(500);
|
|
2839
|
+
why = await check();
|
|
2840
|
+
}
|
|
2841
|
+
return why;
|
|
2842
|
+
};
|
|
2843
|
+
/** This function gives the nodes that announce a Service, from the ServiceL2Status objects of the speakers. */
|
|
2844
|
+
const announcementsOf = async (cluster, namespace, service) => {
|
|
2845
|
+
return JSON.parse(await cluster.kubectl([
|
|
2846
|
+
"-n",
|
|
2847
|
+
METALLB_NAMESPACE,
|
|
2848
|
+
"get",
|
|
2849
|
+
"servicel2statuses.metallb.io",
|
|
2850
|
+
"-o",
|
|
2851
|
+
"json"
|
|
2852
|
+
])).items.filter((item) => item.status?.serviceName === service && item.status.serviceNamespace === namespace && typeof item.status.node === "string").map((item) => ({
|
|
2853
|
+
node: item.status.node,
|
|
2854
|
+
created: item.metadata?.creationTimestamp ?? null
|
|
2855
|
+
})).sort((a, b) => a.node.localeCompare(b.node));
|
|
2856
|
+
};
|
|
2857
|
+
/** This function gives the nodes with a ready speaker that is not deleted. */
|
|
2858
|
+
const speakersReady = async (cluster) => {
|
|
2859
|
+
return JSON.parse(await cluster.kubectl([
|
|
2860
|
+
"-n",
|
|
2861
|
+
METALLB_NAMESPACE,
|
|
2862
|
+
"get",
|
|
2863
|
+
"pods",
|
|
2864
|
+
"-l",
|
|
2865
|
+
"app.kubernetes.io/component=speaker",
|
|
2866
|
+
"-o",
|
|
2867
|
+
"json"
|
|
2868
|
+
])).items.filter((pod) => pod.metadata.deletionTimestamp === void 0 && pod.spec.nodeName !== void 0).filter((pod) => (pod.status?.containerStatuses ?? []).length > 0 && pod.status.containerStatuses.every((status) => status.ready)).map((pod) => pod.spec.nodeName).sort();
|
|
2869
|
+
};
|
|
2870
|
+
/**
|
|
2871
|
+
* This function makes `owner` the only node that announces the Service, with a ready speaker on each
|
|
2872
|
+
* node of `nodes`. It gives null, or why it could not in `within` ms. After a heal, the other node
|
|
2873
|
+
* stops when it sees that the owner is alive again. The function then annotates the Service. Each
|
|
2874
|
+
* speaker reconciles it, and the owner sends its gratuitous ARPs for 5 s. Thus each client learns
|
|
2875
|
+
* the MAC address of the owner.
|
|
2876
|
+
*/
|
|
2877
|
+
const holdMetallb = async (cluster, owner, nodes, namespace, service, within = 18e4) => {
|
|
2878
|
+
const ready = await until(within, async () => {
|
|
2879
|
+
const found = await speakersReady(cluster);
|
|
2880
|
+
const missing = nodes.filter((node) => !found.includes(node));
|
|
2881
|
+
return missing.length === 0 ? null : `the speaker of MetalLB is not ready on ${missing.join(", ")}`;
|
|
2882
|
+
});
|
|
2883
|
+
if (ready !== null) return ready;
|
|
2884
|
+
const sole = await until(within, async () => {
|
|
2885
|
+
const found = (await announcementsOf(cluster, namespace, service)).map((each) => each.node);
|
|
2886
|
+
return found.length === 1 && found[0] === owner ? null : `the Service ${service} is announced by ${found.join(", ") || "no node"}, not only by ${owner}`;
|
|
2887
|
+
});
|
|
2888
|
+
if (sole !== null) return sole;
|
|
2889
|
+
await cluster.kubectl([
|
|
2890
|
+
"-n",
|
|
2891
|
+
namespace,
|
|
2892
|
+
"annotate",
|
|
2893
|
+
"service",
|
|
2894
|
+
service,
|
|
2895
|
+
`${ANNOUNCE_ANNOTATION}=${Date.now()}`,
|
|
2896
|
+
"--overwrite"
|
|
2897
|
+
]);
|
|
2898
|
+
return null;
|
|
2899
|
+
};
|
|
2900
|
+
/**
|
|
2901
|
+
* This function installs MetalLB in a cluster, with its images on each node, the speakers on `nodes`,
|
|
2902
|
+
* and a pool of the VIP. The webhook of the controller checks the pool, thus the function tries the
|
|
2903
|
+
* pool again until the webhook answers. A failed install gives the pods of MetalLB and the tail of
|
|
2904
|
+
* their logs.
|
|
2905
|
+
*/
|
|
2906
|
+
const installMetallb = async (cluster, images, nodes, vip) => {
|
|
2907
|
+
try {
|
|
2908
|
+
for (const node of nodes) await cluster.kubectl([
|
|
2909
|
+
"label",
|
|
2910
|
+
"node",
|
|
2911
|
+
node,
|
|
2912
|
+
`${VIP_LABEL}=1`,
|
|
2913
|
+
"--overwrite"
|
|
2914
|
+
]);
|
|
2915
|
+
await helmInstall(cluster, "metallb", METALLB_CHART, METALLB_NAMESPACE, metallbValues(images), { createNamespace: true });
|
|
2916
|
+
let failure = null;
|
|
2917
|
+
const end = Date.now() + 18e4;
|
|
2918
|
+
do {
|
|
2919
|
+
failure = await cluster.apply(metallbObjects(vip)).then(() => null, (error) => error);
|
|
2920
|
+
if (failure !== null) await sleep$1(2e3);
|
|
2921
|
+
} while (failure !== null && Date.now() < end);
|
|
2922
|
+
if (failure !== null) throw failure;
|
|
2923
|
+
if ((await cluster.kubectl([
|
|
2924
|
+
"-n",
|
|
2925
|
+
"metallb-system",
|
|
2926
|
+
"get",
|
|
2927
|
+
"configmap",
|
|
2928
|
+
"metallb-memberlist-config",
|
|
2929
|
+
"--ignore-not-found",
|
|
2930
|
+
"-o",
|
|
2931
|
+
"name"
|
|
2932
|
+
])).trim() !== "") throw new Error("the ConfigMap metallb-memberlist-config is there: memberlist would not use its defaults");
|
|
2933
|
+
} catch (error) {
|
|
2934
|
+
const state = async (args) => (await cluster.kubectl([
|
|
2935
|
+
"-n",
|
|
2936
|
+
METALLB_NAMESPACE,
|
|
2937
|
+
...args
|
|
2938
|
+
]).catch((why) => String(why))).slice(-4e3);
|
|
2939
|
+
const pods = await state([
|
|
2940
|
+
"get",
|
|
2941
|
+
"pods",
|
|
2942
|
+
"-o",
|
|
2943
|
+
"wide"
|
|
2944
|
+
]);
|
|
2945
|
+
const logs = await state([
|
|
2946
|
+
"logs",
|
|
2947
|
+
"-l",
|
|
2948
|
+
"app.kubernetes.io/name=metallb",
|
|
2949
|
+
"--tail=40",
|
|
2950
|
+
"--prefix",
|
|
2951
|
+
"--all-containers"
|
|
2952
|
+
]);
|
|
2953
|
+
throw new Error(`the install of MetalLB failed: ${String(error)}\nthe pods of MetalLB:\n${pods}\nthe logs:\n${logs}`);
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
/** These words mark the lines of a speaker that the record keeps: the events of memberlist, the announces and the withdraws. */
|
|
2957
|
+
const KEPT = /node event|Marking|Suspect|Refuting|serviceAnnounced|serviceWithdrawn|memberlist/;
|
|
2958
|
+
/**
|
|
2959
|
+
* This function gives the record of MetalLB after a run: the nodes that announce the Service, and
|
|
2960
|
+
* some lines of each speaker. The lines are about memberlist and the announces, with the times of the kubelet.
|
|
2961
|
+
*/
|
|
2962
|
+
const metallbRecord = async (cluster, namespace, service) => {
|
|
2963
|
+
return {
|
|
2964
|
+
announcements: await announcementsOf(cluster, namespace, service),
|
|
2965
|
+
log: (await cluster.kubectl([
|
|
2966
|
+
"-n",
|
|
2967
|
+
METALLB_NAMESPACE,
|
|
2968
|
+
"logs",
|
|
2969
|
+
"-l",
|
|
2970
|
+
"app.kubernetes.io/component=speaker",
|
|
2971
|
+
"-c",
|
|
2972
|
+
"speaker",
|
|
2973
|
+
"--since=300s",
|
|
2974
|
+
"--tail=4000",
|
|
2975
|
+
"--prefix",
|
|
2976
|
+
"--timestamps"
|
|
2977
|
+
]).catch((error) => String(error))).split("\n").filter((line) => KEPT.test(line)).slice(-120).join("\n").slice(-12e3)
|
|
2978
|
+
};
|
|
2979
|
+
};
|
|
2980
|
+
//#endregion
|
|
2981
|
+
//#region src/announcer.ts
|
|
2982
|
+
const local = (images, name) => {
|
|
2983
|
+
const image = images[name];
|
|
2984
|
+
if (image === void 0) throw new Error(`the run has no local image of ${name}`);
|
|
2985
|
+
return image;
|
|
2986
|
+
};
|
|
2987
|
+
/** This announcer is kube-vip 1.3.1 in ARP mode, with one Lease for each VIP of a Service (`kubevip.ts`). */
|
|
2988
|
+
const KUBE_VIP_ANNOUNCER = {
|
|
2989
|
+
name: "kube-vip",
|
|
2990
|
+
images: { [KUBE_VIP]: KUBE_VIP_IMAGE },
|
|
2991
|
+
service: {
|
|
2992
|
+
loadBalancerClass: LB_CLASS,
|
|
2993
|
+
annotation: VIP_ANNOTATION
|
|
2994
|
+
},
|
|
2995
|
+
settle: KUBE_VIP_ARP_RATE,
|
|
2996
|
+
vipOf: (subnet) => vipOf(subnet),
|
|
2997
|
+
install: (cluster, images, owner, nodes) => installKubeVip(cluster, local(images, KUBE_VIP), owner, nodes),
|
|
2998
|
+
hold: (cluster, owner, nodes) => holdVip(cluster, owner, nodes),
|
|
2999
|
+
record: async (cluster) => {
|
|
3000
|
+
return {
|
|
3001
|
+
lease: await vipLeaseOf(cluster),
|
|
3002
|
+
log: (await cluster.kubectl([
|
|
3003
|
+
"-n",
|
|
3004
|
+
KUBE_VIP_LEASE.namespace,
|
|
3005
|
+
"logs",
|
|
3006
|
+
"-l",
|
|
3007
|
+
`app.kubernetes.io/name=${KUBE_VIP}`,
|
|
3008
|
+
"--tail=30",
|
|
3009
|
+
"--prefix",
|
|
3010
|
+
"--timestamps"
|
|
3011
|
+
]).catch((error) => String(error))).slice(-6e3)
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
/** This announcer is MetalLB 0.16.1 in L2 mode, with its speakers in memberlist (`metallb.ts`). */
|
|
3016
|
+
const METALLB_ANNOUNCER = {
|
|
3017
|
+
name: "metallb",
|
|
3018
|
+
images: {
|
|
3019
|
+
"metallb-speaker": METALLB_IMAGES.speaker,
|
|
3020
|
+
"metallb-controller": METALLB_IMAGES.controller
|
|
3021
|
+
},
|
|
3022
|
+
service: {
|
|
3023
|
+
loadBalancerClass: null,
|
|
3024
|
+
annotation: METALLB_ANNOTATION
|
|
3025
|
+
},
|
|
3026
|
+
settle: METALLB_SPAM.for,
|
|
3027
|
+
vipOf: (subnet, owner, nodes) => metallbVipOf(subnet, owner, nodes),
|
|
3028
|
+
install: (cluster, images, _owner, nodes, vip) => installMetallb(cluster, {
|
|
3029
|
+
speaker: local(images, "metallb-speaker"),
|
|
3030
|
+
controller: local(images, "metallb-controller")
|
|
3031
|
+
}, nodes, vip),
|
|
3032
|
+
hold: (cluster, owner, nodes, namespace, service) => holdMetallb(cluster, owner, nodes, namespace, service),
|
|
3033
|
+
record: (cluster, namespace, service) => metallbRecord(cluster, namespace, service)
|
|
3034
|
+
};
|
|
3035
|
+
/** This function gives the announcer of the VIP of a cell: kube-vip when the cell names none. */
|
|
3036
|
+
const announcerOf = (cell) => cell.vip?.announcer === "metallb" ? METALLB_ANNOUNCER : KUBE_VIP_ANNOUNCER;
|
|
3037
|
+
/** This function gives null if each client sends to `mac`, or the clients that do not. */
|
|
3038
|
+
const macsAt = (found, mac, count) => {
|
|
3039
|
+
const wrong = found.filter((each) => each.mac !== mac);
|
|
3040
|
+
if (found.length !== count) return `${found.length} clients outside answer, not ${count}`;
|
|
3041
|
+
return wrong.length === 0 ? null : wrong.map((each) => `${each.client} has ${each.mac ?? "no MAC"}`).join(", ");
|
|
3042
|
+
};
|
|
3043
|
+
//#endregion
|
|
2633
3044
|
//#region src/lds.ts
|
|
2634
3045
|
/**
|
|
2635
3046
|
* This filter of RE2 selects the two counters of the listener manager of Envoy that an update of LDS
|
|
@@ -2682,6 +3093,18 @@ const KEPT_KERNEL = /* @__PURE__ */ new Set([
|
|
|
2682
3093
|
]);
|
|
2683
3094
|
/** This function tells if the lab keeps a kernel record of the prober. */
|
|
2684
3095
|
const keptKernel = (record) => KEPT_KERNEL.has(String(record["ev"])) || isTunnelState(record);
|
|
3096
|
+
/**
|
|
3097
|
+
* This function gives the MAC address of `ip` in a table of /proc/net/arp, in lower case. It gives
|
|
3098
|
+
* null if the table has no row for `ip`, or if the row has the flags 0x0: no MAC yet. The ARP watch
|
|
3099
|
+
* of `client.mjs` reads the table the same way.
|
|
3100
|
+
*/
|
|
3101
|
+
const arpMacOf = (table, ip) => {
|
|
3102
|
+
for (const row of table.split("\n").slice(1)) {
|
|
3103
|
+
const [address, , flags, mac] = row.trim().split(/\s+/);
|
|
3104
|
+
if (address === ip) return flags === "0x0" || mac === void 0 ? null : mac.toLowerCase();
|
|
3105
|
+
}
|
|
3106
|
+
return null;
|
|
3107
|
+
};
|
|
2685
3108
|
/** This container class can share the process namespace of another container. */
|
|
2686
3109
|
const shared = async () => {
|
|
2687
3110
|
const { GenericContainer } = await import("testcontainers");
|
|
@@ -2876,6 +3299,17 @@ const openKindLab = async (cluster, image) => {
|
|
|
2876
3299
|
outsiders.push(client);
|
|
2877
3300
|
}
|
|
2878
3301
|
},
|
|
3302
|
+
async outsideMacs(ip) {
|
|
3303
|
+
const found = [];
|
|
3304
|
+
for (const [k, client] of outsiders.entries()) {
|
|
3305
|
+
const { output } = await client.exec(["cat", "/proc/net/arp"]);
|
|
3306
|
+
found.push({
|
|
3307
|
+
client: `${OUTSIDE}-${k}`,
|
|
3308
|
+
mac: arpMacOf(output, ip)
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
return found;
|
|
3312
|
+
},
|
|
2879
3313
|
async stopProbe() {
|
|
2880
3314
|
if (prober !== null) await prober.stop({ timeout: 0 }).catch(() => void 0);
|
|
2881
3315
|
prober = null;
|
|
@@ -3289,22 +3723,26 @@ const keptZone = { nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecutio
|
|
|
3289
3723
|
values: [ZONE_LOST]
|
|
3290
3724
|
}] }] } } };
|
|
3291
3725
|
/**
|
|
3292
|
-
* This function gives the Service of a VIP (`
|
|
3293
|
-
* class of
|
|
3726
|
+
* This function gives the Service of a VIP (`announcer.ts`). It is of type LoadBalancer, with the
|
|
3727
|
+
* class of its announcer, if the announcer has one. The annotation of the announcer has the address
|
|
3728
|
+
* of the VIP. Without `service`, the announcer is kube-vip. The policy `Cluster` lets each node
|
|
3294
3729
|
* send a request to a replica on another node. The holder writes the address to the status of the
|
|
3295
3730
|
* Service. Then kube-proxy has the rules of the address on each node.
|
|
3296
3731
|
*/
|
|
3297
|
-
const vipService = (namespace, vip, ports
|
|
3732
|
+
const vipService = (namespace, vip, ports, service = {
|
|
3733
|
+
loadBalancerClass: LB_CLASS,
|
|
3734
|
+
annotation: VIP_ANNOTATION
|
|
3735
|
+
}) => ({
|
|
3298
3736
|
apiVersion: "v1",
|
|
3299
3737
|
kind: "Service",
|
|
3300
3738
|
metadata: {
|
|
3301
3739
|
name: "lab",
|
|
3302
3740
|
namespace,
|
|
3303
|
-
annotations: { [
|
|
3741
|
+
annotations: { [service.annotation]: vip }
|
|
3304
3742
|
},
|
|
3305
3743
|
spec: {
|
|
3306
3744
|
type: "LoadBalancer",
|
|
3307
|
-
loadBalancerClass:
|
|
3745
|
+
...service.loadBalancerClass === null ? {} : { loadBalancerClass: service.loadBalancerClass },
|
|
3308
3746
|
externalTrafficPolicy: "Cluster",
|
|
3309
3747
|
selector: { app: "lab" },
|
|
3310
3748
|
ports
|
|
@@ -3315,7 +3753,7 @@ const vipService = (namespace, vip, ports) => ({
|
|
|
3315
3753
|
* settings of the cell. The image is the lab image for the service, or the image of grpc-go.
|
|
3316
3754
|
*
|
|
3317
3755
|
* With a VIP (`KindCell.vip`), the Service is of type LoadBalancer, with the address `vip` and the
|
|
3318
|
-
* class of
|
|
3756
|
+
* class of its announcer (`vipService`). No replica is in the lost zone. Thus a zone loss takes away
|
|
3319
3757
|
* only the holder of the VIP.
|
|
3320
3758
|
*/
|
|
3321
3759
|
const serverObjects = (cell, namespace, image, version = 1, vip = null) => {
|
|
@@ -3339,7 +3777,7 @@ const serverObjects = (cell, namespace, image, version = 1, vip = null) => {
|
|
|
3339
3777
|
selector: { app: "lab" },
|
|
3340
3778
|
ports
|
|
3341
3779
|
}
|
|
3342
|
-
} : vipService(namespace, vip, ports), {
|
|
3780
|
+
} : vipService(namespace, vip, ports, announcerOf(cell).service), {
|
|
3343
3781
|
apiVersion: "apps/v1",
|
|
3344
3782
|
kind: "Deployment",
|
|
3345
3783
|
metadata: {
|
|
@@ -3749,7 +4187,7 @@ const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !
|
|
|
3749
4187
|
* This function gives the key of the cluster of a cell: its kube-proxy mode, its node count, and the
|
|
3750
4188
|
* flavor of its mesh. A zone loss has clusters of its own, with a zone on each node.
|
|
3751
4189
|
*/
|
|
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");
|
|
4190
|
+
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 ? "" : cell.vip.announcer === "metallb" ? "-vip-mlb" : "-vip");
|
|
3753
4191
|
/** This function tells if a cell needs a cluster with zones: a zone loss. */
|
|
3754
4192
|
const zoned = (cell) => cell.trigger.kind === "zone-loss";
|
|
3755
4193
|
/**
|
|
@@ -4418,34 +4856,26 @@ const lostNodeOf = (cluster) => cluster.nodes.at(-1);
|
|
|
4418
4856
|
/** This function gives the workers of a cluster: each node but the control plane, the first (`nodeNames`). */
|
|
4419
4857
|
const workersOf = (cluster) => cluster.nodes.slice(1);
|
|
4420
4858
|
/**
|
|
4421
|
-
* This function adds a line `vip` to the lines. The line has the
|
|
4422
|
-
* of each node on the Docker network
|
|
4423
|
-
* Lease
|
|
4859
|
+
* This function adds a line `vip` to the lines. The line has the state of the announcer, the MAC
|
|
4860
|
+
* address of each node on the Docker network, and the name of the announcer. The state of kube-vip
|
|
4861
|
+
* is its Lease. The state of MetalLB is the nodes that announce the Service. It also has the wall clock of the runner with its CLOCK_MONOTONIC. The times of the
|
|
4862
|
+
* Lease and of the logs are on the wall clock, and each stamp of the lab is on CLOCK_MONOTONIC. The containers of
|
|
4424
4863
|
* the runner share one kernel, thus the pair converts one into the other.
|
|
4425
4864
|
*/
|
|
4426
4865
|
const recordVip = async (ctx) => {
|
|
4427
4866
|
const wall = Date.now();
|
|
4428
4867
|
const t = Number(process.hrtime.bigint()) / 1e6;
|
|
4429
|
-
const
|
|
4868
|
+
const announcer = announcerOf(ctx.cell);
|
|
4869
|
+
const state = await announcer.record(ctx.cluster, ctx.namespace, "lab");
|
|
4430
4870
|
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
4871
|
ctx.lab.record({
|
|
4442
4872
|
ev: "vip",
|
|
4443
4873
|
t,
|
|
4444
4874
|
wall,
|
|
4445
4875
|
vip: ctx.vip,
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4876
|
+
announcer: announcer.name,
|
|
4877
|
+
...state,
|
|
4878
|
+
macs
|
|
4449
4879
|
});
|
|
4450
4880
|
};
|
|
4451
4881
|
/** This function puts the replicas of a node first, and keeps the sequence of the others. */
|
|
@@ -4510,9 +4940,21 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
4510
4940
|
const lost = zoneLoss ? await nodeSteady(cluster, lostNodeOf(cluster), 3e5) : null;
|
|
4511
4941
|
if (lost !== null) return empty(`the node of the lost zone was not at rest: ${lost}`, await lab.now());
|
|
4512
4942
|
if (cell.vip !== void 0) {
|
|
4513
|
-
const
|
|
4514
|
-
|
|
4515
|
-
await
|
|
4943
|
+
const announcer = announcerOf(cell);
|
|
4944
|
+
const holder = lostNodeOf(cluster);
|
|
4945
|
+
const held = await announcer.hold(cluster, holder, workersOf(cluster), ctx.namespace, "lab");
|
|
4946
|
+
if (held !== null) return empty(`the VIP is not on ${holder}: ${held}`, await lab.now());
|
|
4947
|
+
if (ctx.vip === null) return empty(`${cell.name}: the cluster has no VIP`, await lab.now());
|
|
4948
|
+
const mac = (await vipNetworkOf(cluster.network)).macs[holder];
|
|
4949
|
+
if (mac === void 0) return empty(`the node ${holder} has no MAC address on the Docker network`, await lab.now());
|
|
4950
|
+
const end = Date.now() + valueIn(announcer.settle, "ms") + 1e4;
|
|
4951
|
+
let moved = macsAt(await lab.outsideMacs(ctx.vip), mac, cell.vip.clients);
|
|
4952
|
+
while (moved !== null && Date.now() < end) {
|
|
4953
|
+
await sleep(250);
|
|
4954
|
+
moved = macsAt(await lab.outsideMacs(ctx.vip), mac, cell.vip.clients);
|
|
4955
|
+
}
|
|
4956
|
+
if (moved !== null) return empty(`the clients outside do not send to ${holder} (${mac}): ${moved}`, await lab.now());
|
|
4957
|
+
await sleep(valueIn(announcer.settle, "ms") + valueIn(cell.workload.timeout, "ms") + 1e3);
|
|
4516
4958
|
}
|
|
4517
4959
|
if (await steady(ctx, 3e5) !== null) return empty("the cell was not at rest after the wait for a quiet VM", await lab.now());
|
|
4518
4960
|
const replicas = zoneLoss ? lostFirst(replicasOf(ctx), lostNodeOf(cluster)) : replicasOf(ctx);
|
|
@@ -4736,7 +5178,13 @@ const runKindCells = async (cells, options) => {
|
|
|
4736
5178
|
operator: await localPin(CILIUM_IMAGES.operator),
|
|
4737
5179
|
envoy: await localPin(CILIUM_IMAGES.envoy)
|
|
4738
5180
|
} : void 0;
|
|
4739
|
-
const
|
|
5181
|
+
const announcerImages = /* @__PURE__ */ new Map();
|
|
5182
|
+
for (const announcer of [KUBE_VIP_ANNOUNCER, METALLB_ANNOUNCER]) {
|
|
5183
|
+
if (!cells.some((cell) => cell.vip !== void 0 && announcerOf(cell).name === announcer.name)) continue;
|
|
5184
|
+
const pinned = {};
|
|
5185
|
+
for (const [name, ref] of Object.entries(announcer.images)) pinned[name] = await localPin(ref);
|
|
5186
|
+
announcerImages.set(announcer.name, pinned);
|
|
5187
|
+
}
|
|
4740
5188
|
const images = {
|
|
4741
5189
|
lab: image,
|
|
4742
5190
|
redis: redisImage,
|
|
@@ -4784,12 +5232,14 @@ const runKindCells = async (cells, options) => {
|
|
|
4784
5232
|
}
|
|
4785
5233
|
let vip = null;
|
|
4786
5234
|
if (group[0].vip !== void 0) {
|
|
4787
|
-
|
|
4788
|
-
|
|
5235
|
+
const announcer = announcerOf(group[0]);
|
|
5236
|
+
const pinned = announcerImages.get(announcer.name);
|
|
5237
|
+
if (pinned === void 0) throw new Error(`${group[0].name}: the run has no images of ${announcer.name}`);
|
|
5238
|
+
for (const each of Object.values(pinned)) await cluster.load(each);
|
|
4789
5239
|
const began = Date.now();
|
|
4790
|
-
await
|
|
4791
|
-
|
|
4792
|
-
options.log(`
|
|
5240
|
+
vip = announcer.vipOf((await vipNetworkOf(cluster.network)).subnet, lostNodeOf(cluster), workersOf(cluster));
|
|
5241
|
+
await announcer.install(cluster, pinned, lostNodeOf(cluster), workersOf(cluster), vip);
|
|
5242
|
+
options.log(` ${announcer.name} installed in ${Math.round((Date.now() - began) / 100) / 10} s, the VIP ${vip}`);
|
|
4793
5243
|
}
|
|
4794
5244
|
lab = await openKindLab(cluster, image);
|
|
4795
5245
|
const { GenericContainer } = await import("testcontainers");
|
|
@@ -4850,7 +5300,7 @@ const runKindCells = async (cells, options) => {
|
|
|
4850
5300
|
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
|
|
4851
5301
|
...cell.mesh === void 0 ? {} : Object.fromEntries(MESH_IMAGES[cell.mesh.flavor].map((name) => [`istio-${name}`, ISTIO_IMAGES[name]])),
|
|
4852
5302
|
...s.cni === "cilium" ? Object.fromEntries(Object.entries(CILIUM_IMAGES).map(([name, ref]) => [`cilium-${name}`, ref])) : {},
|
|
4853
|
-
...cell.vip === void 0 ? {} :
|
|
5303
|
+
...cell.vip === void 0 ? {} : announcerOf(cell).images
|
|
4854
5304
|
}
|
|
4855
5305
|
});
|
|
4856
5306
|
entry.loads.push(...loads);
|
|
@@ -4956,6 +5406,6 @@ const shardOf = (cells, index, count) => {
|
|
|
4956
5406
|
return out;
|
|
4957
5407
|
};
|
|
4958
5408
|
//#endregion
|
|
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 };
|
|
5409
|
+
export { ACCESS_LOG_FORMAT, ANNOUNCE_ANNOTATION, 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_ANNOUNCER, 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, MEMBERLIST, MESH_IMAGES, MESH_WORKLOADS, METALLB_ANNOTATION, METALLB_ANNOUNCER, METALLB_CHART, METALLB_IMAGES, METALLB_NAMESPACE, METALLB_POOL, METALLB_SPAM, METALLB_VERSION, 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, announcementsOf, announcerOf, arpMacOf, 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, ensurePinnedChart, ensureTool, estimatedTime, factsOf, failuresOf, faultSpan, followsZtunnel, goDuration, helmInstall, holdMetallb, holdVip, hostsOf, installCilium, installKubeVip, installMesh, installMetallb, 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, macsAt, medianInterval, medianText, meshKey, metallbObjects, metallbRecord, metallbValues, metallbVipOf, middle, middleboxCommands, namesService, namespaceObject, namespaceOf, nodeLossBand, nodeNames, nodeReturnBand, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, overshootBand, ownerOrder, 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, speakersReady, stackOf, stageLabImage, stallRemovalBand, stopOf, stoppingOf, suspicionOf, sysctlCommand, tarEntry, timeSpread, toolPath, transitionOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, verdictsOf, vipLeaseOf, vipNetworkOf, vipOf, vipServed, vipService, writeResult, zoneOf, ztunnelCertificates, ztunnelEnvArgs, ztunnelPodsOf, ztunnelReplaced };
|
|
4960
5410
|
|
|
4961
5411
|
//# sourceMappingURL=index.js.map
|