@mark1russell7/kind 0.13.1 → 0.13.2

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