@mark1russell7/kind 0.13.2 → 0.13.4

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.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-C7Zr4KoS.js";
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";
@@ -10,7 +10,7 @@ import { gunzipSync } from "node:zlib";
10
10
  import { failSpan, lint, lintStop, recoverSpan, service } from "@mark1russell7/service";
11
11
  import { createRequire } from "node:module";
12
12
  import { tmpdir } from "node:os";
13
- import { harnessImage, imageOf } from "@mark1russell7/harness";
13
+ import { harnessImage, imageOf, substrateKey, substrateOf, substrateOn } from "@mark1russell7/harness";
14
14
  import { redisClient } from "@mark1russell7/service/redis";
15
15
  import { DRAIN_RULES, DRAIN_RULES as DRAIN_RULES$1, O04, O09, O10, O16 } from "@mark1russell7/service/rules";
16
16
  //#region src/backends.ts
@@ -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 = async (cache = cacheFolder()) => {
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 command(helm, [
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$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
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$1(500);
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 (`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
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: { [VIP_ANNOTATION]: vip }
3741
+ annotations: { [service.annotation]: vip }
3304
3742
  },
3305
3743
  spec: {
3306
3744
  type: "LoadBalancer",
3307
- loadBalancerClass: LB_CLASS,
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 kube-vip (`vipService`). No replica is in the lost zone. Thus a zone loss takes away
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 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
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 lease = await vipLeaseOf(ctx.cluster);
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
- lease,
4447
- macs,
4448
- log: log.slice(-6e3)
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 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);
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);
@@ -4705,7 +5147,8 @@ const writeResult = (out, result) => {
4705
5147
  voided: verdict.voided,
4706
5148
  failed: verdict.failed,
4707
5149
  median: verdict.median,
4708
- why: verdict.why
5150
+ why: verdict.why,
5151
+ substrate: substrateKey(substrateOf(result.manifests[0] ?? {}, result.at, "E2"))
4709
5152
  })}\n`, { flag: "a" });
4710
5153
  };
4711
5154
  /**
@@ -4736,7 +5179,13 @@ const runKindCells = async (cells, options) => {
4736
5179
  operator: await localPin(CILIUM_IMAGES.operator),
4737
5180
  envoy: await localPin(CILIUM_IMAGES.envoy)
4738
5181
  } : void 0;
4739
- const kubeVip = cells.some((cell) => cell.vip !== void 0) ? await localPin(KUBE_VIP_IMAGE) : void 0;
5182
+ const announcerImages = /* @__PURE__ */ new Map();
5183
+ for (const announcer of [KUBE_VIP_ANNOUNCER, METALLB_ANNOUNCER]) {
5184
+ if (!cells.some((cell) => cell.vip !== void 0 && announcerOf(cell).name === announcer.name)) continue;
5185
+ const pinned = {};
5186
+ for (const [name, ref] of Object.entries(announcer.images)) pinned[name] = await localPin(ref);
5187
+ announcerImages.set(announcer.name, pinned);
5188
+ }
4740
5189
  const images = {
4741
5190
  lab: image,
4742
5191
  redis: redisImage,
@@ -4784,12 +5233,14 @@ const runKindCells = async (cells, options) => {
4784
5233
  }
4785
5234
  let vip = null;
4786
5235
  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);
5236
+ const announcer = announcerOf(group[0]);
5237
+ const pinned = announcerImages.get(announcer.name);
5238
+ if (pinned === void 0) throw new Error(`${group[0].name}: the run has no images of ${announcer.name}`);
5239
+ for (const each of Object.values(pinned)) await cluster.load(each);
4789
5240
  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}`);
5241
+ vip = announcer.vipOf((await vipNetworkOf(cluster.network)).subnet, lostNodeOf(cluster), workersOf(cluster));
5242
+ await announcer.install(cluster, pinned, lostNodeOf(cluster), workersOf(cluster), vip);
5243
+ options.log(` ${announcer.name} installed in ${Math.round((Date.now() - began) / 100) / 10} s, the VIP ${vip}`);
4793
5244
  }
4794
5245
  lab = await openKindLab(cluster, image);
4795
5246
  const { GenericContainer } = await import("testcontainers");
@@ -4844,13 +5295,14 @@ const runKindCells = async (cells, options) => {
4844
5295
  env,
4845
5296
  scenario: s,
4846
5297
  cluster: manifest,
5298
+ substrate: substrateOn("E2"),
4847
5299
  images: {
4848
5300
  lab: image,
4849
5301
  redis: redisImage,
4850
5302
  ...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
4851
5303
  ...cell.mesh === void 0 ? {} : Object.fromEntries(MESH_IMAGES[cell.mesh.flavor].map((name) => [`istio-${name}`, ISTIO_IMAGES[name]])),
4852
5304
  ...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 }
5305
+ ...cell.vip === void 0 ? {} : announcerOf(cell).images
4854
5306
  }
4855
5307
  });
4856
5308
  entry.loads.push(...loads);
@@ -4956,6 +5408,6 @@ const shardOf = (cells, index, count) => {
4956
5408
  return out;
4957
5409
  };
4958
5410
  //#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 };
5411
+ 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
5412
 
4961
5413
  //# sourceMappingURL=index.js.map