@mark1russell7/kind 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # @mark1russell7/kind
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Kubernetes in Docker (kind) as a lab for connection timers. A scenario gives the cluster, and the lab puts the faults of the harness in the namespaces of its pods. Each observation is on the one monotonic clock of the VM, and the ordering law gives the predictions. The package also reads the raw record of a cell and judges it again, without Docker.
4
+
5
+ ```sh
6
+ npm install @mark1russell7/kind zod
7
+ ```
8
+
9
+ ## The two entries
10
+
11
+ | Entry | What it holds | What it loads |
12
+ | --- | --- | --- |
13
+ | `@mark1russell7/kind` | Each part: the cluster, the lab, the image, the faults, the predictions, the runner, and all of `./judge` | Testcontainers, Docker, `kind` and `kubectl` when a lab opens |
14
+ | `@mark1russell7/kind/judge` | The types and the zod schemas of a record, `upgrade`, `judge`, the verdict, the readers of the lines and the report | `@mark1russell7/quantity` and zod only |
15
+
16
+ The entry `./judge` loads no Docker, no Testcontainers, no child process and no `node:` module. Thus a program that only reads records can use it, in a browser too. Two tests hold it to that. One follows the imports of its source, and one imports it in a new Node process. The smoke test of a release does the same on the built tarball.
17
+
18
+ ```ts
19
+ import { judge, readRecord } from "@mark1russell7/kind/judge";
20
+
21
+ const record = readRecord(JSON.parse(text)); // a record of /1 or /2, as /2
22
+ const verdict = judge(record); // the verdict of this version, from the cell and the runs
23
+ ```
24
+
25
+ - `readRecord(data)` examines a record of either format with its zod schema, and gives it in the format `/2`.
26
+ - `upgrade(record)` gives a record of `/1` in the format `/2`. It does not examine the record, and it changes nothing in it.
27
+ - `judge(record)` judges a record of either format again, from its cell and its runs. For each of the 31 records of the kind workflow of 2026-10-16, it gives the verdict of the record.
28
+ - `kindCellRecordSchema` and `kindCellRecordV1Schema` are the zod schemas of the two formats.
29
+ - `judgeKind`, `checkRun`, `factsOf` and `unitOf` are the verdict. `cellsTable`, `predictionText`, `medianText`, `calibrationOf` and `timeSpread` are the report.
30
+
31
+ The judge does not load the harness, because the harness loads its Docker lab. It has its own copy of the median interval, the quantile and δ_rel of the harness, and a test holds the copy to the harness.
32
+
33
+ ## The record formats
34
+
35
+ The lab writes the raw record of each cell as JSON: `{ format, cell, runs, manifests, loads, verdict, at }`. A cell is all that the measurement needs, as data. A run keeps each line that its sources wrote, thus a holder of the record can judge it again.
36
+
37
+ - **`systems.kind-cell-record/2`** is the format that the lab writes. Each amount in it is a quantity, `{ "value": 34.244, "unit": "s" }`, from `@mark1russell7/quantity`.
38
+ - **`systems.kind-cell-record/1`** is the first format. Its bands, its medians and the times of its facts are numbers. The lab does not write it again, and a reader reads it through `upgrade`.
39
+
40
+ These are the changes from `/1` to `/2`. Each value stays the same. Only its form changes.
41
+
42
+ | Field | `/1` | `/2` |
43
+ | --- | --- | --- |
44
+ | `cell.prediction` bands (`band`, `start.band`, `end.band`) | `{ "lo": 28, "hi": 49 }`, in seconds | `{ "lo": { "value": 28, "unit": "s" }, "hi": { "value": 49, "unit": "s" } }` |
45
+ | `cell.trigger.cpuMax` | `{ "quota": 1000, "period": 1000000 }`, in microseconds | Each part a quantity in `us` |
46
+ | `verdict.median` and `verdict.checks[].value` | A number: seconds or a count, by the prediction | A quantity in the unit of the prediction (`unitOf`) |
47
+ | The times of `verdict.checks[].facts` | Numbers of seconds | Quantities in `s` |
48
+
49
+ The unit of the main value of a run comes from the prediction of its cell:
50
+
51
+ - `s` for a time: `removal`, `leaves`, `stalls`, and `restarts` with a band.
52
+ - `{request}` for the failed requests: `none`, `window`, `stays` and `measured`.
53
+ - `{replica}` for the replicas that received SIGTERM: `restarts` without a band.
54
+
55
+ These values stay numbers in the two formats:
56
+
57
+ - **A stamp** is a reading of CLOCK_MONOTONIC in milliseconds, a point on a clock, not an amount. The stamps are `start`, `end` and `acts` of a run, `t` of each line, and `first`, `last` and `deletion` of the facts.
58
+ - **A line** stays as its source wrote it. The image writes it, and the record keeps it.
59
+ - **A count** or **a share**: the replicas, the thresholds, the runs and the counts of a verdict. The jitter of a poll and the idle share of a load sample are shares.
60
+
61
+ The tests read three real records of `/1` (`test/records/`): a cell that holds, a cell that fails and a calibration. Each fixture is a whole record, without its load samples and without the kernel release of the runner. `KIND_RECORDS=<folder> pnpm test` judges each record under a folder again, and holds each verdict to the verdict of its record.
62
+
63
+ ## The cluster
64
+
65
+ - **The scenario** (`scenario.ts`) is the cluster as data: its nodes, its version, the mode and the sync periods of kube-proxy, its CNI and its subnets. `kindConfig(scenario)` generates the configuration of kind from it.
66
+ - **The node image** is `kindest/node:v1.37.0`, pinned by digest, the default of kind v0.33.0. The research names v1.37.1 for some defaults. The patch release is the mismatch.
67
+ - **The tools** (`tools.ts`) are kind v0.33.0 and kubectl v1.37.0. The lab downloads each one into `.cache/` in this package, and it compares the sha256 with the pinned value at each use. `KIND_CACHE` names another folder. The lab installs nothing on the system, and the same code works on Linux, macOS and Windows.
68
+ - **The isolation.** Each cluster gets its own Docker network, its own kubeconfig and its own cache for kubectl, in its run folder. The lab refuses a pod or service subnet that overlaps a Docker network.
69
+
70
+ ## The lab
71
+
72
+ `openKindLab(cluster, image)` (`lab.ts`) starts the instruments of a cluster:
73
+
74
+ - **The agent of each node** is a privileged container in the PID namespace of the node container. It signals a process of a pod by its PID on the node. It writes `cpu.max` in the cgroup of a container, and a flag file in the root of a container.
75
+ - **The rules of each node.** The agent reads the rules of kube-proxy in the namespaces of the node each 50 ms: `iptables-save`, `nft list table ip kube-proxy`, or `/proc/net/ip_vs`. It writes a line when the backends of the Service change (`backends.ts`). A backend of IPVS with the weight zero takes no new connection, thus it is not a backend.
76
+ - **The injector of each node** is the image of the harness, in the PID namespace of the node. `crictl` on the node gives the PID of a container, and `nsenter -n` enters the network namespace of its pod. The injector of the harness then applies its rules there.
77
+ - **The watcher** is a container on the network of the cluster. It watches the pods, the EndpointSlices and the events through the API, and it follows the log of each pod of the cell.
78
+ - **The prober** of the harness watches the network namespace of each pod, by its inode. The lab keeps its resets, errors and retransmissions, and it counts the other records.
79
+
80
+ Each pod of a cluster shares the kernel of the Docker VM. Thus each stamp of the agent, the injector, the watcher, the clients, the service and the prober is on the same CLOCK_MONOTONIC.
81
+
82
+ ## The image
83
+
84
+ The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`, its clients, the agent and the watcher, on Node 24.21.0. Its base is pinned by digest, and `npm-lock.json` pins the packages from npm. `stageLabImage()` stages its context, and `labImage()` tags the image by its content.
85
+
86
+ - **In the workspace** of this repository, the stage builds `@mark1russell7/quantity`, `@mark1russell7/algebra` and `@mark1russell7/service`, and packs each one as npm gets it.
87
+ - **From npm**, the stage packs the installed versions of the same packages. The package holds the files of the image, and `src/compose.ts` and `src/backends.ts`, for that.
88
+ - **The service** (`app.mjs`) is a Node HTTP server. Its one controller needs Redis when the cell has Redis. Its health server gives the probes, and it declares each stage of the shutdown. `compose.ts` composes the service from the declaration of the cell, and `lint()` reads the same composition.
89
+ - **The seams** of the service are a flag file that fails its liveness check, and SIGUSR2. SIGUSR2 blocks its event loop for the time in a file.
90
+ - **The clients** (`client.mjs`) send requests on new connections to the ClusterIP. Each request gives one line with its outcome: ok, 503, refused, reset, timeout or unreachable.
91
+
92
+ ## The triggers
93
+
94
+ `planOf(cell)` (`faults.ts`) maps each trigger to the acts of the lab:
95
+
96
+ | Trigger | Acts |
97
+ | --- | --- |
98
+ | `pod-deletion` | `kubectl delete pod` of the first replica |
99
+ | `rolling-update` | a new value of an environment variable of the Deployment |
100
+ | `node-drain` | `kubectl drain` of the node of the first replica, then `uncordon` |
101
+ | `scale-in` | one replica less, then back |
102
+ | `crash` | SIGKILL of the process of the service, from the node |
103
+ | `liveness-restart` | the flag file that fails the liveness check of the first replica |
104
+ | `freeze-process` | SIGSTOP and SIGCONT: of kube-proxy on one node across a deletion (T07), or of Redis (T18) |
105
+ | `rst`, `blackhole` | the injector of the harness in the namespace of Redis, with each replica as a peer |
106
+ | `peer-gone-without-close` | a black hole on Redis, then no Redis pod: its namespace goes away with its rules |
107
+ | `app-stall` | the event loop of the first replica blocked |
108
+ | `rollout` | no Redis pod, then a rolling update |
109
+ | `cpu-throttle` | `cpu.max` in the cgroup of the container of the first replica |
110
+
111
+ A cgroup path that is not the scope of one container is refused. A write to the root of the node throttles or freezes each pod of the node.
112
+
113
+ The plan's `event:clock-jump` has no trigger. A step of CLOCK_REALTIME in the VM steps the clock of each container: the kubelet, the API server and the cells of other agents. A time namespace offsets only the monotonic clocks, and the runtime of the service reads only the monotonic clock.
114
+
115
+ ## The predictions
116
+
117
+ `predict.ts` gives each band from the declaration, the pod settings and the calibration of the propagation:
118
+
119
+ - **A deletion.** The listener of a leaving pod stays open for its preStop sleep and the hold of `fail readiness`. A connection fails when the rules of its node select the pod after that. `rules.ts` gives O04 with the hold as data, and `lint()` decides it.
120
+ - **A pause of kube-proxy** keeps the rules of one node for its hold. The window on that node ends one propagation after the pause.
121
+ - **A fault of a shared dependency** trips the readiness latch of each replica (`failSpan`). Then the kubelet needs its threshold of probes, and the rules need one propagation.
122
+ - **A restart through a coupling** adds the liveness probes of the kubelet and the preStop sleep to the latch.
123
+
124
+ `verdict.ts` judges each run, and each cell on its counted runs. Each time lands in its band ± 100 ms: the poll of the rules and the delay of the watcher. A median in a band gets the test of the harness: its order-statistic interval in the band, made 5 % wider.
125
+
126
+ ## Tests
127
+
128
+ `pnpm --filter @mark1russell7/kind test` starts the tests of the configuration, the tools, the parser of the rules and the fault mapping. It also starts the tests of the predictions, the verdict and the judge of the real records. They do not need Docker.
129
+
130
+ The catalogs are in `@systems/cells` (`t07.ts`, `t18.ts`). `KIND_LAB=1` starts them in kind.
131
+
132
+ ## CI
133
+
134
+ The cells operate on the runners of GitHub, and not on a machine that does other work (`.github/workflows/kind.yml`). Each shard gets a runner, and each job makes its own clusters and deletes them.
135
+
136
+ - **The modes of the kernel.** kube-proxy in a node container uses the modules of the host kernel. Thus the job loads `ip_vs` and `nf_tables` first. A cell of a mode that the kernel cannot do waits, and `summary.ndjson` tells why.
137
+ - **The shards.** `shardOf` gives each shard an equal share of the estimated time, and the cells of one mode stay together. Thus a shard makes few clusters.
138
+ - **The output.** Each job uploads its raw records, `summary.ndjson`, and what the runner is. The run folder of each cluster stays out of the output, because a kubeconfig holds credentials.
139
+ - **A pull request** that changes the workflow starts a smoke run: the calibration of each mode, with 2 runs on one cluster.
140
+
141
+ ## Releases
142
+
143
+ CI publishes each new version of this package (`.github/workflows/release.yml`). npm trusts the workflow only for a package that exists. Thus the first version of a new package goes out by hand, and then the workflow becomes its trusted publisher. For a package that npm does not have, the workflow gives a warning and publishes nothing.
@@ -0,0 +1,393 @@
1
+ import { $ as SliceLine, $t as goDuration, A as TriggerV1, At as showBand, B as RunFacts, Bt as Replica, C as KindCellResult, Ct as LOG_LABEL, D as PredictionV1, Dt as bandOf, E as LoadSample, Et as SERVER, F as KindDecision, Ft as KubeletProbe, G as referenceOf, Gt as KUBE_PROXY_MODES, H as factsOf, Ht as Trigger, I as KindVerdict, It as PodSettings, J as Line, Jt as KubeProxyMode, K as unitOf, Kt as KindConfig, L as LAST_SLACK, Lt as Prediction, M as upgrade, Mt as Declaration, N as upgradeCell, Nt as KindCell, O as RunCheckV1, Ot as seconds, P as KIND_NOISE, Pt as KindRun, Q as RulesLine, Qt as cidrRange, R as Propagated, Rt as RedisDeclaration, S as KindCellRecordV1, St as HEALTH_PORT, T as KindVerdictV1, Tt as REDIS, U as judgeKind, Ut as TriggerKind, V as checkRun, Vt as Template, W as propagationOf, Wt as Workload, X as RecordLine, Xt as NodeImage, Y as PodLine, Yt as NODE_IMAGES, Z as Request, Zt as cidrOverlap, _ as ClusterManifest, _t as sliceDropOf, a as STAGES, at as failuresOf, b as FORMAT_V1, bt as transitionOf, c as readRecord, ct as outcomesOf, d as calibrationOf, dt as recordsOf, en as kindConfig, et as backendsAt, f as cellsTable, ft as requestsOf, g as BandV1, gt as signalsOf, h as timeSpread, ht as signalOf, i as quantileOf, in as scenarioProblems, it as emptyOf, j as judge, jt as Band, k as RunFactsV1, kt as secondsOf, l as Judged, lt as podsAt, m as predictionText, mt as rulesOf, n as medianInterval, nn as nodeNames, nt as deletionOf, o as kindCellRecordSchema, ot as intakeOf, p as medianText, pt as restartsAt, q as Endpoint, qt as KindScenario, r as middle, rn as scenario, rt as dropOf, s as kindCellRecordV1Schema, st as kernelCounts, t as RELATIVE, tn as kubeProxyPatch, tt as byNode, u as TimeSpread, ut as podsOf, v as EnvironmentManifest, vt as sliceEmptyOf, w as KindCellV1, wt as PORT, x as KindCellRecord, xt as CLIENT, y as FORMAT, yt as slicesOf, z as RunCheck, zt as Reference } from "./judge-CDDCr1Z8.js";
2
+ import { Quantity } from "@mark1russell7/quantity";
3
+ import { CheckFn, Finding, HealthServer, Poll, Rule, Scenario, ServiceBuilder, ServiceDescription, Stage, Timing, TransportImpl, Verdict } from "@mark1russell7/service";
4
+ import { RedisClient } from "@mark1russell7/service/redis";
5
+ //#region src/backends.d.ts
6
+ /** A service rule names the Service whose backends the agent reads: its namespace, its name, and its ClusterIP and port for IPVS. */
7
+ interface ServiceRule {
8
+ readonly mode: "iptables" | "ipvs" | "nftables";
9
+ readonly ns: string;
10
+ readonly svc: string;
11
+ readonly ip: string;
12
+ readonly port: number;
13
+ }
14
+ /**
15
+ * This function gives the backends of a Service in a dump of the rules of kube-proxy, as
16
+ * `ip:port`, sorted. A backend of IPVS with the weight zero takes no new connection, thus it is
17
+ * not a backend.
18
+ * - iptables: the `KUBE-SVC` chain of the Service jumps to a `KUBE-SEP` chain for each backend, and that chain does the DNAT.
19
+ * - nftables: the `service-` chain of the Service does the DNAT to each backend, or it names an `endpoint-` chain for each one.
20
+ * - IPVS: `/proc/net/ip_vs` has the line of the virtual service, then a line for each real server.
21
+ */
22
+ export declare const backendsOf: (mode: ServiceRule["mode"], text: string, rule: Pick<ServiceRule, "ns" | "svc" | "ip" | "port">) => string[];
23
+ //#endregion
24
+ //#region src/cluster.d.ts
25
+ /** The credentials of the API, as a container on the network of the cluster reaches it. */
26
+ interface Credentials {
27
+ readonly server: string;
28
+ readonly ca: string;
29
+ readonly cert: string;
30
+ readonly key: string;
31
+ }
32
+ /** A cluster is one kind cluster that the lab made, with its own kubeconfig and Docker network. */
33
+ interface Cluster {
34
+ readonly scenario: KindScenario;
35
+ readonly dir: string;
36
+ readonly kubeconfig: string;
37
+ readonly network: string;
38
+ readonly nodes: readonly string[];
39
+ kubectl(args: readonly string[], input?: string): Promise<string>;
40
+ /** This method applies Kubernetes objects, as JSON on standard input. */
41
+ apply(objects: readonly unknown[]): Promise<void>;
42
+ /** This method starts a command in a node container. */
43
+ exec(node: string, argv: readonly string[]): Promise<string>;
44
+ /** This method copies a local Docker image into each node. */
45
+ load(image: string): Promise<void>;
46
+ credentials(): Promise<Credentials>;
47
+ manifest(): Promise<ClusterManifest>;
48
+ delete(): Promise<void>;
49
+ }
50
+ /**
51
+ * This function makes a kind cluster from a scenario, in a folder that holds its config, its
52
+ * kubeconfig and the cache of kubectl. The cluster gets a Docker network of its own. The function
53
+ * refuses a pod or service subnet that overlaps a Docker network, because that breaks the traffic
54
+ * between the nodes.
55
+ */
56
+ export declare const createCluster: (scenario: KindScenario, dir: string) => Promise<Cluster>;
57
+ //#endregion
58
+ //#region src/compose.d.ts
59
+ /** These are the parts of a running service. A composition for `lint()` gives none of them. */
60
+ interface Parts {
61
+ readonly redis?: RedisClient | undefined;
62
+ readonly flag?: CheckFn | undefined;
63
+ readonly http?: TransportImpl | undefined;
64
+ readonly health?: HealthServer | undefined;
65
+ }
66
+ /** This name is the controller of the service, and its one route reads Redis when the service has Redis. */
67
+ export declare const CONTROLLER = "work";
68
+ export declare const ROUTE = "GET /work";
69
+ /** This name is the check that a flag file fails: the seam of the liveness restart. */
70
+ export declare const FLAG_CHECK = "self.flag";
71
+ /** This name is the watchdog of the event loop. */
72
+ export declare const WATCHDOG = "self.event-loop";
73
+ /**
74
+ * This function composes the service of a declaration: its policies, its couplings, its checks,
75
+ * one server, one controller and each stage of the shutdown. A declaration with Redis needs the
76
+ * client, because the client declares the dependency and its checks.
77
+ */
78
+ export declare const compose: (name: string, declaration: Declaration, parts?: Parts) => ServiceBuilder;
79
+ //#endregion
80
+ //#region src/faults.d.ts
81
+ /**
82
+ * This module maps each trigger of a cell to the acts of the lab, as data. The runner finds each
83
+ * target at the time of the run. A target is a pod, the PID of a container, the network namespace
84
+ * of a pod, or a cgroup.
85
+ */
86
+ /**
87
+ * A target is a process or a pod of the cell:
88
+ * - `server` is the first replica of the service, in the sequence of its name.
89
+ * - `redis` is the Redis pod.
90
+ * - `kube-proxy` is kube-proxy on the first worker.
91
+ */
92
+ type Target = "server" | "redis" | "kube-proxy";
93
+ /** A step is one act of a fault, in the sequence of the plan. */
94
+ type Step = {
95
+ readonly do: "kubectl";
96
+ readonly args: readonly string[];
97
+ readonly background?: boolean;
98
+ } | {
99
+ readonly do: "signal";
100
+ readonly target: Target;
101
+ readonly signal: "SIGKILL" | "SIGSTOP" | "SIGCONT";
102
+ } | {
103
+ readonly do: "pause";
104
+ readonly target: Target;
105
+ readonly time: Quantity;
106
+ } | {
107
+ readonly do: "inject";
108
+ readonly target: Target;
109
+ readonly action: "blackhole" | "clear" | "rst" | "clear-rst";
110
+ } | {
111
+ readonly do: "flag";
112
+ readonly target: Target;
113
+ readonly on: boolean;
114
+ } | {
115
+ readonly do: "cpu";
116
+ readonly target: Target;
117
+ readonly max: string;
118
+ } | {
119
+ readonly do: "stall";
120
+ readonly target: Target;
121
+ readonly time: Quantity;
122
+ } | {
123
+ readonly do: "wait";
124
+ readonly time: Quantity;
125
+ };
126
+ /**
127
+ * A fault plan is the steps of a run:
128
+ * - `inject` starts the trigger. The stamp of its first act is the `fault` reference.
129
+ * - `heal` ends the fault, `hold` after the fault. A trigger that ends by itself has no heal.
130
+ * - `restore` brings the cell back to its steady state after the run.
131
+ */
132
+ interface FaultPlan {
133
+ readonly inject: readonly Step[];
134
+ readonly hold: Quantity | null;
135
+ readonly heal: readonly Step[];
136
+ readonly restore: readonly Step[];
137
+ }
138
+ /** This function gives the fault plan of a cell. */
139
+ export declare const planOf: (cell: KindCell) => FaultPlan;
140
+ /** This function tells if a trigger stops a pod of the service with its grace: a deletion, an eviction, a scale-in or a rollout. */
141
+ export declare const deletes: (cell: KindCell) => boolean;
142
+ //#endregion
143
+ //#region src/image.d.ts
144
+ /**
145
+ * This function stages the build context of the lab image. In the workspace, it builds the packages
146
+ * of the repository and packs each one as npm gets it. From npm, it packs the installed packages.
147
+ * Then it copies the files of the image.
148
+ */
149
+ export declare const stageLabImage: () => string;
150
+ /** This function builds the lab image one time for each process, and gives its tag. Docker keeps its layers between builds. */
151
+ export declare const labImage: () => Promise<string>;
152
+ //#endregion
153
+ //#region src/lab.d.ts
154
+ /** A stamp brackets an act on CLOCK_MONOTONIC, in milliseconds. */
155
+ interface Stamp {
156
+ readonly t0: number;
157
+ readonly t1: number;
158
+ }
159
+ /** These kinds of kernel record stay in the raw record of a run. The lab counts the other kinds and drops them. */
160
+ export declare const KEPT_KERNEL: ReadonlySet<string>;
161
+ /** A kind lab is the instruments of one cluster: a sidecar pair on each node, the watcher of the API and the prober. */
162
+ interface KindLab {
163
+ readonly cluster: Cluster;
164
+ readonly image: string;
165
+ /** These are the lines since the last `take()`. */
166
+ readonly lines: Line[];
167
+ /** This record counts the kernel records that the lab dropped, by kind. */
168
+ readonly dropped: Record<string, number>;
169
+ /** This method gives the names of the containers that the lab started: they are not the lab containers of another agent. */
170
+ own(): string[];
171
+ /** This method gives the lines since the last call, and starts a new buffer. */
172
+ take(): Line[];
173
+ /** This method adds a listener of each line, and gives the function that removes it. */
174
+ listen(listener: (line: Line) => void): () => void;
175
+ watch(namespaces: readonly string[]): Promise<void>;
176
+ probe(netns: readonly number[]): Promise<void>;
177
+ stopProbe(): Promise<void>;
178
+ /** This method asks each agent to poll the backends of a Service in the rules of its node, `every` apart, or to stop with null. */
179
+ rules(rule: (ServiceRule & {
180
+ readonly every: Quantity;
181
+ }) | null): Promise<void>;
182
+ act(node: string, spec: Readonly<Record<string, unknown>>): Promise<Stamp>;
183
+ inject(node: string, pid: number, args: readonly string[]): Promise<Stamp>;
184
+ now(): Promise<number>;
185
+ pidOf(node: string, namespace: string, pod: string, container: string): Promise<number>;
186
+ netnsOf(node: string, pid: number): Promise<number>;
187
+ /** This method gives the path of the cgroup of a process, as the agent reaches it through the root of the node. */
188
+ cgroupOf(node: string, pid: number): Promise<string>;
189
+ close(): Promise<void>;
190
+ }
191
+ /**
192
+ * This function opens the instruments of a cluster. Each node gets the agent of the lab and the
193
+ * injector of the harness, privileged and in the PID namespace of the node. Thus the agent signals
194
+ * a process of a pod by its PID on the node. The injector enters the network namespace of a pod
195
+ * with `nsenter`.
196
+ */
197
+ export declare const openKindLab: (cluster: Cluster, image: string) => Promise<KindLab>;
198
+ /** This function pins a Docker image by digest under a local tag that holds the digest, for `kind load` and `imagePullPolicy: Never`. */
199
+ export declare const localPin: (image: string) => Promise<string>;
200
+ //#endregion
201
+ //#region src/manifests.d.ts
202
+ /** This image is Redis 7.4, the stock target of the harness, pinned by digest. */
203
+ export declare const REDIS_IMAGE = "redis:7.4@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f";
204
+ /** This function gives the namespace of a cell: `lab-` and 10 digits of the sha256 of its name. */
205
+ export declare const namespaceOf: (cell: Pick<KindCell, "name">) => string;
206
+ /** This function gives a probe of the pod: an HTTP GET of the health server, with each field of the cell. */
207
+ export declare const probeOf: (path: string, probe: KubeletProbe) => Record<string, unknown>;
208
+ /** These are the objects of Redis: one replica behind a Service, with no persistence. */
209
+ export declare const redisObjects: (namespace: string, image: string) => unknown[];
210
+ /** These are the objects of the service: a Service of type ClusterIP, and the Deployment with the settings of the cell. */
211
+ export declare const serverObjects: (cell: KindCell, namespace: string, image: string, version?: number) => unknown[];
212
+ /** These are the objects of the clients: a DaemonSet with one client on each node, the control plane too. */
213
+ export declare const clientObjects: (cell: KindCell, namespace: string, image: string, target: string) => unknown[];
214
+ //#endregion
215
+ //#region src/predict.d.ts
216
+ /**
217
+ * A propagation is the time from a change of an endpoint to the rules of each node. A
218
+ * calibration measures it for each kube-proxy mode and node count. Without a calibration, its upper
219
+ * bound is `minSyncPeriod`: an idle kube-proxy syncs a change at once, or one `minSyncPeriod`
220
+ * after its last sync.
221
+ */
222
+ interface Propagation extends Band {
223
+ /** This text tells where the bound comes from: a calibration, or the documented `minSyncPeriod`. */
224
+ readonly from: string;
225
+ }
226
+ /** This propagation is the documented bound, before a calibration: from zero to one `minSyncPeriod` of 1 s. */
227
+ export declare const DOCUMENTED: Propagation;
228
+ /**
229
+ * This function gives the time from a flip of the probed state to the decision of the kubelet. The
230
+ * next probe comes within one period, then `threshold − 1` more periods follow. A probe of an
231
+ * endpoint that does not answer fails at its timeout.
232
+ */
233
+ export declare const kubeletBand: (probe: KubeletProbe, hangs: boolean) => Band;
234
+ /**
235
+ * This function gives the time from a fault of a dependency to the trip of a readiness or liveness
236
+ * latch. A dependency that refuses fails each poll at once. A silent dependency fails each poll at
237
+ * the request timeout of its client, or at the timeout of the poll when that is shorter. The
238
+ * watchdog of the client can close the connection sooner, and then each poll fails at once.
239
+ */
240
+ export declare const latchBand: (timing: Timing, poll: Poll, failure: "refuses" | "silent", requestTimeout?: Quantity) => Band;
241
+ /** This function gives the time from a fault to the removal of the endpoints from the rules: the latch, then the kubelet, then the propagation. */
242
+ export declare const removalBand: (latch: Band, probe: KubeletProbe, propagation: Propagation) => Band;
243
+ /**
244
+ * This function gives the time from a fault to the SIGTERM of a replica whose liveness fails with
245
+ * its readiness. The time is the latch, then the liveness probes of the kubelet, then the preStop sleep.
246
+ */
247
+ export declare const restartBand: (latch: Band, liveness: KubeletProbe, preStop: Quantity) => Band;
248
+ /**
249
+ * This function gives the time from the start of a stall of the event loop to the removal of the
250
+ * replica from the rules. Each probe in the stall fails at its timeout, because the loop does not answer it.
251
+ */
252
+ export declare const stallRemovalBand: (readiness: KubeletProbe, propagation: Propagation) => Band;
253
+ /** This function gives the cap of a stage in a declaration, or zero when the declaration does not name the stage. */
254
+ export declare const capOf: (declaration: Declaration, stage: Stage) => Quantity;
255
+ /**
256
+ * This function gives the shortest time from the deletion of a pod to the stop of its intake. It
257
+ * is the preStop sleep, then the hold of `fail readiness`. The kubelet adds its own delay before
258
+ * the preStop, thus the real time is longer.
259
+ */
260
+ export declare const intakeAfter: (declaration: Declaration, preStop: Quantity) => Quantity;
261
+ /**
262
+ * A deletion window is what a deletion predicts on one node. The listener of the pod closes at the
263
+ * stop of its intake. The rules of the node stop the new connections to the pod at the
264
+ * propagation. A connection between the two is refused. Thus the window is empty when the intake
265
+ * stops after the latest propagation.
266
+ */
267
+ interface DeletionWindow {
268
+ /** This flag tells that no connection can meet a closed listener. */
269
+ readonly empty: boolean;
270
+ /** This band is the end of the window after the deletion: the propagation. */
271
+ readonly end: Band;
272
+ /** This time is the earliest stop of the intake after the deletion. */
273
+ readonly intake: Quantity;
274
+ }
275
+ /** This function gives the deletion window of a declaration and a preStop sleep on a node with a propagation. */
276
+ export declare const deletionWindow: (declaration: Declaration, preStop: Quantity, propagation: Propagation) => DeletionWindow;
277
+ /**
278
+ * This function gives the window on a node whose kube-proxy pauses across a deletion. The pause
279
+ * starts `lead` before the deletion and holds for `hold`. The node then syncs in one propagation
280
+ * after the end of the pause, at `resume` after the deletion. The window is empty when the intake
281
+ * stops after that.
282
+ */
283
+ export declare const pausedWindow: (declaration: Declaration, preStop: Quantity, lead: Quantity, hold: Quantity, propagation: Propagation) => DeletionWindow & {
284
+ readonly resume: Quantity;
285
+ };
286
+ //#endregion
287
+ //#region src/rules.d.ts
288
+ /**
289
+ * These are the rules of the ordering law that a drain in Kubernetes meets, as data. The caller of
290
+ * `lint()` gives the rules, thus they are here and not in the service.
291
+ */
292
+ /**
293
+ * O04 with the hold of the service. The listener of a pod stays open through the preStop sleep and
294
+ * then through the hold of `fail readiness`. Thus the sum of the two must cover the propagation of
295
+ * the endpoints. The rule of the research is the case without a hold.
296
+ */
297
+ export declare const O04: Rule;
298
+ /** O09 is W8: readiness detection plus propagation is no longer than liveness detection plus the preStop sleep. */
299
+ export declare const O09: Rule;
300
+ /** O10: the preStop sleep plus the drain of the process fits in the grace period. */
301
+ export declare const O10: Rule;
302
+ /** O16: the cooperative window is shorter than the drain, which is shorter than the grace less the preStop sleep. */
303
+ export declare const O16: Rule;
304
+ /** These are the rules of the drain that each kind cell gets. */
305
+ export declare const DRAIN_RULES: readonly Rule[];
306
+ /**
307
+ * This function gives the scenario of a cell for `lint()`: the preStop sleep, the grace, the probe
308
+ * period and the propagation. The probe windows are the period times the threshold of each probe.
309
+ */
310
+ export declare const scenarioOf: (cell: KindCell, propagation: Propagation) => Scenario;
311
+ /** A verdict set is the verdict of each drain rule for one declaration, by the identifier of the rule. */
312
+ type Verdicts = Readonly<Record<string, Verdict>>;
313
+ /** This function applies the drain rules to the description of a cell, and gives each finding. */
314
+ export declare const lintCell: (description: ServiceDescription, cell: KindCell, propagation: Propagation) => Finding[];
315
+ /** This function gives the verdict of each drain rule, by its identifier. */
316
+ export declare const verdictsOf: (findings: readonly Finding[]) => Verdicts;
317
+ /** This function gives the description of the service of a cell, as the app of the image composes it. Nothing connects. */
318
+ export declare const describeCell: (cell: KindCell) => ServiceDescription;
319
+ //#endregion
320
+ //#region src/run.d.ts
321
+ /** This time is the interval of the poll of the rules on each node. */
322
+ export declare const RULES_EVERY: Quantity;
323
+ /** These options run a catalog of kind cells. */
324
+ interface RunOptions {
325
+ /** This folder gets the raw record of each cell and `summary.ndjson`. */
326
+ readonly out: string;
327
+ readonly log: (line: string) => void;
328
+ /** This count is the number of environments, each a new cluster. */
329
+ readonly environments?: number;
330
+ /** This share of the CPUs of the VM must be idle before a run starts. */
331
+ readonly idle?: number;
332
+ /**
333
+ * This folder holds the run folder of each cluster: its config, its kubeconfig and the cache of
334
+ * kubectl. It is not the output, because a kubeconfig holds credentials.
335
+ */
336
+ readonly work?: string;
337
+ }
338
+ /** This function writes the result of a cell: its raw record, and one line of its verdict in `summary.ndjson`. */
339
+ export declare const writeResult: (out: string, result: KindCellResult) => void;
340
+ /**
341
+ * This function does the runs of a catalog of kind cells. The cells of one kube-proxy mode and node
342
+ * count share a cluster. Each group gets new clusters in sequence, one for each environment. Each
343
+ * environment does its share of the runs of each cell, after one warm-up. The function deletes each
344
+ * cluster after its cells.
345
+ */
346
+ export declare const runKindCells: (cells: readonly KindCell[], options: RunOptions) => Promise<KindCellResult[]>;
347
+ /**
348
+ * This function gives an estimate of the time of a cell: its setup, then each run and each
349
+ * warm-up. A run waits for the cell at rest, samples the load, does the trigger and waits until the
350
+ * cell is at rest again.
351
+ */
352
+ export declare const estimatedTime: (cell: KindCell, environments?: number) => Quantity;
353
+ /**
354
+ * This function gives the share of a shard of a catalog: cells in sequence. Thus the cells of one
355
+ * mode stay together and share clusters. Each shard gets an equal share of the estimated time.
356
+ */
357
+ export declare const shardOf: <T extends KindCell>(cells: readonly T[], index: number, count: number) => T[];
358
+ //#endregion
359
+ //#region src/tools.d.ts
360
+ /**
361
+ * A tool is a binary that the lab downloads, at one pinned release, for each platform. The lab
362
+ * keeps it in the cache folder of this package and does not install it on the system. Each use
363
+ * compares its sha256 with the pinned value.
364
+ */
365
+ interface Tool {
366
+ readonly name: "kind" | "kubectl";
367
+ readonly version: string;
368
+ /** These are the release assets, by the platform and the architecture of Node: `win32-x64`, `linux-x64`. */
369
+ readonly assets: Readonly<Record<string, {
370
+ readonly url: string;
371
+ readonly sha256: string;
372
+ }>>;
373
+ }
374
+ /** This release of kind is v0.33.0, from 2026-08-26. Its default node image is Kubernetes 1.37.0. The sums are from its `.sha256sum` assets. */
375
+ export declare const KIND: Tool;
376
+ /** This release of kubectl is v1.37.0, the version of the node image. The sums are from the `.sha256` files of `dl.k8s.io`. */
377
+ export declare const KUBECTL: Tool;
378
+ /** This folder is the cache of the tools: `.cache/` in this package, which git ignores. `KIND_CACHE` names another folder. */
379
+ export declare const cacheFolder: () => string;
380
+ /** This function gives the platform key of this process, as the assets name it. */
381
+ export declare const platformKey: (platform?: string, arch?: string) => string;
382
+ /** This function gives the sha256 of some bytes, in hexadecimal. */
383
+ export declare const sha256: (bytes: Uint8Array) => string;
384
+ /** This function gives the path of a tool in the cache, for a platform. */
385
+ export declare const toolPath: (tool: Tool, key?: string, cache?: string) => string;
386
+ /**
387
+ * This function gives the path of a tool, after a download when the cache does not have it. It
388
+ * refuses bytes whose sha256 is not the pinned value, from the cache or from the download.
389
+ */
390
+ export declare const ensureTool: (tool: Tool, key?: string, cache?: string) => Promise<string>;
391
+ //#endregion
392
+ export { type Band, type BandV1, CLIENT, type Cluster, type ClusterManifest, type Credentials, type Declaration, type DeletionWindow, Endpoint, type EnvironmentManifest, FORMAT, FORMAT_V1, type FaultPlan, HEALTH_PORT, type Judged, KIND_NOISE, KUBE_PROXY_MODES, type KindCell, type KindCellRecord, type KindCellRecordV1, type KindCellResult, type KindCellV1, type KindConfig, type KindDecision, type KindLab, type KindRun, type KindScenario, type KindVerdict, type KindVerdictV1, type KubeProxyMode, type KubeletProbe, LAST_SLACK, LOG_LABEL, Line, type LoadSample, NODE_IMAGES, type NodeImage, PORT, type Parts, PodLine, type PodSettings, type Prediction, type PredictionV1, type Propagated, type Propagation, REDIS, RELATIVE, RecordLine, type RedisDeclaration, type Reference, type Replica, Request, RulesLine, type RunCheck, type RunCheckV1, type RunFacts, type RunFactsV1, type RunOptions, SERVER, STAGES, type ServiceRule, SliceLine, type Stamp, type Step, type Target, type Template, type TimeSpread, type Tool, type Trigger, type TriggerKind, type TriggerV1, type Verdicts, type Workload, backendsAt, bandOf, byNode, calibrationOf, cellsTable, checkRun, cidrOverlap, cidrRange, deletionOf, dropOf, emptyOf, factsOf, failuresOf, goDuration, intakeOf, judge, judgeKind, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, medianInterval, medianText, middle, nodeNames, outcomesOf, podsAt, podsOf, predictionText, propagationOf, quantileOf, readRecord, recordsOf, referenceOf, requestsOf, restartsAt, rulesOf, scenario, scenarioProblems, seconds, secondsOf, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, slicesOf, timeSpread, transitionOf, unitOf, upgrade, upgradeCell };
393
+ //# sourceMappingURL=index.d.ts.map