@mark1russell7/kind 0.11.6 → 0.11.8

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/image/agent.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // The agent of a kind node. It runs in a privileged container in the PID namespace of the node
1
+ // The agent of a kind node. It operates in a privileged container in the PID namespace of the node
2
2
  // container, thus it sees the processes of each pod of the node, and PID 1 is the init of the node.
3
3
  //
4
4
  // node agent.mjs serve polls the rules of kube-proxy in the namespaces of the node, as
@@ -37,7 +37,7 @@ const node = process.env.NODE_NAME ?? "";
37
37
  const say = (line) => process.stdout.write(`${JSON.stringify({ ...line, node })}\n`);
38
38
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
39
39
 
40
- /** the backends of a Service in each mode of kube-proxy, as "ip:port" (with the weight for IPVS) */
40
+ /** These commands give the backends of a Service in each mode of kube-proxy, as "ip:port" (with the weight for IPVS). */
41
41
  const DUMP = {
42
42
  iptables: ["nsenter", "-t", "1", "-m", "-n", "--", "iptables-save", "-t", "nat"],
43
43
  nftables: ["nsenter", "-t", "1", "-m", "-n", "--", "nft", "list", "table", "ip", "kube-proxy"],
@@ -45,15 +45,15 @@ const DUMP = {
45
45
  };
46
46
 
47
47
  /**
48
- * the TCP lines of the conntrack table of the node's network namespace: those of the ClusterIP and
49
- * those of a handshake. A kernel without CONFIG_NF_CONNTRACK_PROCFS has no /proc/net/nf_conntrack
50
- * (the kernel of GitHub's runner, 6.17), thus the agent then asks netlink with the conntrack tool of
48
+ * This function reads the TCP lines of the conntrack table in the node's network namespace: those
49
+ * of the ClusterIP and those of a handshake. A kernel without CONFIG_NF_CONNTRACK_PROCFS has no
50
+ * /proc/net/nf_conntrack (the kernel of GitHub's runner, 6.17), thus the agent then asks netlink with the conntrack tool of
51
51
  * the node. Both give the same form of line. The first read decides the source. `text` is null when
52
52
  * no source answers, and `why` tells why.
53
53
  *
54
54
  * A dump through netlink walks each bucket of the table of the whole kernel with the softirqs off,
55
55
  * whatever its filter. A segment that comes in that walk waits for its end. Thus each read is one
56
- * dump of TCP, and the agent filters it. The workflow makes the table small (kind.yml)
56
+ * dump of TCP, and the agent filters it. The workflow makes the table small (kind.yml).
57
57
  */
58
58
  const SOURCES = {
59
59
  procfs: (ip) => [["nsenter", "-t", "1", "-n", "--", "grep", "-F", "-e", ` dst=${ip} `, "-e", "[UNREPLIED]", "-e", " SYN_RECV ", "/proc/net/nf_conntrack"]],
@@ -67,7 +67,7 @@ const conntrackOf = (ip) => {
67
67
  let ok = true;
68
68
  for (const [command, ...args] of SOURCES[name](ip)) {
69
69
  const run = spawnSync(command, args, { encoding: "utf8", maxBuffer: 64 << 20 });
70
- // grep exits 1 when no line matches; conntrack exits 0 with no entry
70
+ // grep exits 1 when no line matches, and conntrack exits 0 with no entry
71
71
  if (run.status === 0 || (name === "procfs" && run.status === 1)) {
72
72
  for (const line of (run.stdout ?? "").split("\n")) if (line.trim() !== "") lines.add(line);
73
73
  } else {
@@ -83,7 +83,7 @@ const conntrackOf = (ip) => {
83
83
  }
84
84
  return { text: null, why: why.join("; ") };
85
85
  };
86
- /** the PIDs of the clients of the node: the agent is in the PID namespace of the node, thus /proc holds each pod */
86
+ /** This function gives the PIDs of the clients of the node. The agent is in the PID namespace of the node, thus /proc holds each pod. */
87
87
  const clientsOf = () => {
88
88
  const out = [];
89
89
  for (const name of readdirSync("/proc")) {
@@ -98,10 +98,11 @@ const clientsOf = () => {
98
98
  };
99
99
 
100
100
  /**
101
- * the counters of dropped packets, by name. For the node: the drops of the backlogs and the squeezes
102
- * of the softirqs (the kernel, all CPUs), and the drops of each veth. For each client pod, through
103
- * /proc/<pid>/net: the IP discards and the sends with no route, the failed connects, the ICMP
104
- * errors of an unreachable destination, the drops of each interface, and the neighbors with no address yet
101
+ * This function gives the counters of dropped packets, by name. For the node, it gives the drops of
102
+ * the backlogs and the squeezes of the softirqs (the kernel, all CPUs), and the drops of each veth.
103
+ * For each client pod, it reads /proc/<pid>/net. There it gives the IP discards and the sends with
104
+ * no route, and the failed connects. It also gives the ICMP errors of an unreachable destination,
105
+ * the drops of each interface, and the neighbors with no address yet.
105
106
  */
106
107
  const countersOf = () => {
107
108
  const values = {};
@@ -136,7 +137,7 @@ const countersOf = () => {
136
137
  values[`client.${pid}.ip-out-discards`] = ip["OutDiscards"] ?? 0;
137
138
  values[`client.${pid}.ip-out-no-routes`] = ip["OutNoRoutes"] ?? 0;
138
139
  values[`client.${pid}.tcp-attempt-fails`] = tcp["AttemptFails"] ?? 0;
139
- // a refused connect is a reset, or an ICMP error such as the port unreachable of a reject rule
140
+ // a refused connect is a reset, or an ICMP error, for example the port unreachable of a reject rule
140
141
  values[`client.${pid}.icmp-in-unreachable`] = snmpOf(snmp, "Icmp")["InDestUnreachs"] ?? 0;
141
142
  for (const [name, drops] of Object.entries(devOf(own))) {
142
143
  if (name === "lo") continue;
@@ -149,9 +150,9 @@ const countersOf = () => {
149
150
  };
150
151
 
151
152
  /**
152
- * how often the agent reads the conntrack table, in ms. Three dumps each 250 ms on each node took
153
- * three quarters of the CPUs of a runner (run 39066066532), and the lab waited for an idle VM until
154
- * the end. An entry of a SYN with no reply lives 120 s, thus a read each second sees it
153
+ * This value tells how often the agent reads the conntrack table, in ms. Three dumps each 250 ms on
154
+ * each node took three quarters of the CPUs of a runner (CI 39066066532). Then the lab waited for an
155
+ * idle VM until the end. An entry of a SYN with no reply lives 120 s, thus a read each second sees it.
155
156
  */
156
157
  const CONNTRACK_EVERY = 1000;
157
158
 
@@ -266,12 +267,14 @@ const serve = async () => {
266
267
  };
267
268
 
268
269
  /**
269
- * the pollers of the sidecars of the node, of one kind: `eds` or `lds`. /tmp/<kind>.json gives the
270
- * interval and each pod of the node with a PID in it, and for `eds` the server cluster. The agent
271
- * starts one poller for each pod in the network namespace of that PID, and stops each one when the
272
- * file changes or goes. nsenter -n keeps the mount namespace of the agent, thus the poller is this
273
- * file, and its clock is the one of the VM. The EDS pollers read the client pods, and the LDS
274
- * pollers read the server pods
270
+ * This function controls the pollers of the sidecars of the node, of one kind: `eds` or `lds`.
271
+ * The file /tmp/<kind>.json gives the interval and each pod of the node with a PID in it. For
272
+ * `eds`, it also gives the server cluster. The agent starts one poller for each pod in the network
273
+ * namespace of that PID. It stops each poller when the file changes or goes.
274
+ *
275
+ * The command nsenter -n keeps the mount namespace of the agent. Thus the poller is this file, and
276
+ * its clock is the one of the VM. The EDS pollers read the client pods, and the LDS pollers read the
277
+ * server pods.
275
278
  */
276
279
  const pollers = async (kind) => {
277
280
  const file = `/tmp/${kind}.json`;
@@ -312,10 +315,10 @@ const pollers = async (kind) => {
312
315
  };
313
316
 
314
317
  /**
315
- * one poll loop of the admin endpoint of a sidecar, 127.0.0.1:15000 in the namespace of the pod
316
- * (envoy_bootstrap.json of Istio 1.31.3), each `every` ms. `read` gives the state of one answer, and
317
- * a line comes at each change of that state, with the start of the poll before it (`prev`), as a rules
318
- * line. Each second, a line `<kind>-alive` counts the polls
318
+ * This function is one poll loop of the admin endpoint of a sidecar, each `every` ms. The endpoint
319
+ * is 127.0.0.1:15000 in the namespace of the pod (envoy_bootstrap.json of Istio 1.31.3). `read`
320
+ * gives the state of one answer. A line comes at each change of that state, with the start of the
321
+ * poll before it (`prev`), as a rules line. Each second, a line `<kind>-alive` counts the polls.
319
322
  */
320
323
  const poll = async (kind, url, every, pod, read) => {
321
324
  let last = null;
@@ -359,8 +362,9 @@ const poll = async (kind, url, every, pod, read) => {
359
362
  };
360
363
 
361
364
  /**
362
- * one EDS poller. The filter of /clusters keeps the server cluster only (clusters_handler.cc of Envoy
363
- * v1.39.4). A line `eds` gives the healthy hosts (`b`), and each other host with its flags (`u`)
365
+ * This function is one EDS poller. The filter of /clusters keeps the server cluster only
366
+ * (clusters_handler.cc of Envoy v1.39.4). A line `eds` gives the healthy hosts (`b`), and each
367
+ * other host with its flags (`u`).
364
368
  */
365
369
  const eds = ({ cluster, every, pod }) =>
366
370
  poll("eds", `http://127.0.0.1:15000/clusters?filter=${encodeURIComponent(clusterFilter(cluster))}`, every, pod, (text) => {
@@ -369,8 +373,9 @@ const eds = ({ cluster, every, pod }) =>
369
373
  });
370
374
 
371
375
  /**
372
- * one LDS poller, in a server pod. The filter of /stats keeps the two counters of the listener
373
- * manager. A line `lds` gives each update of a listener (`mod`) and each update in place (`inPlace`)
376
+ * This function is one LDS poller, in a server pod. The filter of /stats keeps the two counters of
377
+ * the listener manager. A line `lds` gives each update of a listener (`mod`) and each update in
378
+ * place (`inPlace`).
374
379
  */
375
380
  const lds = ({ every, pod }) =>
376
381
  poll("lds", `http://127.0.0.1:15000/stats?filter=${encodeURIComponent(LDS_FILTER)}`, every, pod, (text) => {
package/image/client.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  // unreachable, or another error) and its local port (`lp`). With PROTOCOL h2c, each connection is a
7
7
  // session of HTTP/2, and each request is a stream on the next session in turn. With SESSION_CHECK,
8
8
  // each session has the session check of the service: a stream timeout or a PING with no ACK closes it.
9
+ // Each second, one line counts the PINGs of the checks and their ACKs (`pings`).
9
10
  import http from "node:http";
10
11
  import http2 from "node:http2";
11
12
  import net from "node:net";
@@ -32,7 +33,8 @@ let turn = 0;
32
33
  // each slot of HTTP/2 holds one session on its own socket, thus the client knows its local port. A
33
34
  // GOAWAY, a close or an error of the session makes the slot open a new session at its next stream.
34
35
  // kube-proxy picks a backend for that new connection. The old session ends the streams that it has.
35
- // The check of a session hears each frame of the peer: its SETTINGS, its PINGs, and each response
36
+ // The check of a session hears each frame of the peer: its SETTINGS, its PINGs, and each response.
37
+ // It sends each PING through `counted`, thus the client counts each PING and each ACK
36
38
  const sessionOf = () => {
37
39
  const socket = net.connect({ host, port: Number(port) });
38
40
  const session = http2.connect(`http://${host}:${port}`, { createConnection: () => socket });
@@ -42,7 +44,23 @@ const sessionOf = () => {
42
44
  held.check?.stop();
43
45
  };
44
46
  if (checkKnobs !== null) {
45
- held.check = sessionCheck(session, {
47
+ const counted = {
48
+ get connecting() {
49
+ return session.connecting;
50
+ },
51
+ ping(callback) {
52
+ const sent = session.ping((error) => {
53
+ if (error === null) pings.acked += 1;
54
+ callback(error);
55
+ });
56
+ if (sent) pings.sent += 1;
57
+ return sent;
58
+ },
59
+ destroy(error) {
60
+ session.destroy(error);
61
+ },
62
+ };
63
+ held.check = sessionCheck(counted, {
46
64
  knobs: checkKnobs,
47
65
  onClose: (cause) => {
48
66
  held.usable = false;
@@ -77,6 +95,15 @@ process.on("SIGHUP", () => {
77
95
  }, timeout + 100);
78
96
  });
79
97
 
98
+ // the PINGs of the session checks of this client. Each second, one line gives the PINGs that the client
99
+ // sent and the ACKs that it got in that second. A client with no check writes zeros, thus each record
100
+ // shows that the client counted
101
+ let pings = { sent: 0, acked: 0 };
102
+ setInterval(() => {
103
+ say({ ev: "pings", t: now(), sent: pings.sent, acked: pings.acked });
104
+ pings = { sent: 0, acked: 0 };
105
+ }, 1000);
106
+
80
107
  let pending = [];
81
108
  const flush = () => {
82
109
  if (pending.length === 0) return;
package/image/watch.mjs CHANGED
@@ -1,11 +1,11 @@
1
- // The watcher of a kind cell. It runs in a container on the Docker network of the cluster, thus in
1
+ // The watcher of a kind cell. It operates in a container on the Docker network of the cluster, thus in
2
2
  // the VM, and it stamps each event on CLOCK_MONOTONIC when it arrives. It watches the pods, the
3
3
  // EndpointSlices and the events of one namespace through the API, and it follows the log of each
4
4
  // pod with the label lab.systems/log=1. A log line is JSON with its own stamp, and the watcher
5
5
  // passes it on as it is.
6
6
  //
7
7
  // The sidecar of Istio (the container istio-proxy) is a native sidecar: an init container that
8
- // runs beside the app. The watcher follows it too. Its lines are the access log of Envoy (JSON, a
8
+ // operates beside the app. The watcher follows it too. Its lines are the access log of Envoy (JSON, a
9
9
  // stamp of the wall clock) and the text of pilot-agent. Each one becomes a line `proxy` with the
10
10
  // stamp of its arrival: evidence, not a time that a band reads. An access line of a call that
11
11
  // succeeded at its first attempt is only counted (`proxy-ok`, each second), thus the record keeps
@@ -43,10 +43,10 @@ const say = (line) => pending.push(`${JSON.stringify(line)}\n`);
43
43
  * opens again with no resource version, and the API gives each object again as ADDED.
44
44
  */
45
45
  const GONE = Symbol("gone");
46
- /** the result of a handler for an ERROR event: GONE for an expired resource version, else null */
46
+ /** This function gives the result of a handler for an ERROR event: GONE for an expired resource version, else null. */
47
47
  const errorOf = (status) => (status?.code === 410 ? GONE : null);
48
48
 
49
- /** a stream of lines from the API, again after each end, from the last resource version it saw */
49
+ /** This function opens a stream of lines from the API. After each end, it opens it again from the last resource version that it saw. */
50
50
  const stream = (path, query, onLine, again = true) => {
51
51
  let version = null;
52
52
  const open = () => {
@@ -104,9 +104,9 @@ const stateOf = (state) => {
104
104
  return "none";
105
105
  };
106
106
 
107
- /** the container of the sidecar of Istio */
107
+ /** This is the container of the sidecar of Istio. */
108
108
  const PROXY = "istio-proxy";
109
- /** the access lines of each sidecar that the watcher only counted, by pod, since the last count */
109
+ /** This map holds the access lines of each sidecar that the watcher only counted, by pod, since the last count. */
110
110
  const counted = new Map();
111
111
  setInterval(() => {
112
112
  const t = now();
@@ -114,11 +114,11 @@ setInterval(() => {
114
114
  counted.clear();
115
115
  }, 1000);
116
116
 
117
- /** an access line of Envoy that tells nothing more than its call: status 200, no flag, gRPC status 0, one attempt */
117
+ /** This function tells if an access line of Envoy tells nothing more than its call: status 200, no flag, gRPC status 0, one try. */
118
118
  const plain = (log) =>
119
119
  String(log.code) === "200" && (log.flags === "-" || log.flags === "" || log.flags === undefined) && String(log.grpc) === "0" && Number(log.attempts ?? 1) <= 1;
120
120
 
121
- /** a line of the sidecar: an access line of Envoy as JSON, or a text line of pilot-agent */
121
+ /** This function takes a line of the sidecar: an access line of Envoy as JSON, or a text line of pilot-agent. */
122
122
  const proxyLine = (pod, text) => {
123
123
  if (text.startsWith("{")) {
124
124
  try {
@@ -136,7 +136,7 @@ const proxyLine = (pod, text) => {
136
136
  say({ ev: "proxy", t: now(), p: pod, text: text.slice(0, 500) });
137
137
  };
138
138
 
139
- /** the namespace of the mesh, with the ztunnel pods */
139
+ /** This is the namespace of the mesh, with the ztunnel pods. */
140
140
  const MESH_NS = "istio-system";
141
141
 
142
142
  const following = new Set();
@@ -146,7 +146,7 @@ const follow = (ns, pod, container, id) => {
146
146
  following.add(key);
147
147
  stream(
148
148
  `/api/v1/namespaces/${ns}/pods/${pod}/log`,
149
- // a ztunnel ran before the cell: only its lines from the start of the watcher
149
+ // a ztunnel started before the cell: only its lines from the start of the watcher
150
150
  ns === MESH_NS ? { container, follow: "true", sinceSeconds: "5" } : { container, follow: "true" },
151
151
  (text) => {
152
152
  if (ns === MESH_NS) say({ ev: "ztunnel", t: now(), p: pod, text: text.slice(0, 500) });
@@ -176,7 +176,7 @@ for (const ns of namespaces) {
176
176
  last: stateOf(status.lastState),
177
177
  id: status.containerID ?? null,
178
178
  }));
179
- // the native sidecar of Istio is a running init container: its state, apart from the containers
179
+ // the native sidecar of Istio is an init container that operates: its state, apart from the containers
180
180
  const sidecars = (pod.status?.initContainerStatuses ?? [])
181
181
  .filter((status) => status.name === PROXY)
182
182
  .map((status) => ({ n: status.name, r: status.ready, rc: status.restartCount, st: stateOf(status.state), id: status.containerID ?? null }));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/kind",
4
- "version": "0.11.6",
4
+ "version": "0.11.8",
5
5
  "description": "Kubernetes in Docker as a lab for connection timers: a kind cluster from a scenario, faults in the namespaces of its pods, observations on the one monotonic clock of the VM, and predictions from the ordering law. Its judge entry reads and judges a raw record without Docker.",
6
6
  "keywords": [
7
7
  "kubernetes",
@@ -67,7 +67,7 @@
67
67
  "dependencies": {
68
68
  "@mark1russell7/harness": "^0.12.4",
69
69
  "@mark1russell7/quantity": "^0.2.0",
70
- "@mark1russell7/service": "^0.5.20",
70
+ "@mark1russell7/service": "^0.6.1",
71
71
  "testcontainers": "12.2.0"
72
72
  },
73
73
  "peerDependencies": {