@mark1russell7/kind 0.11.6 → 0.11.7
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 +35 -30
- package/image/client.mjs +29 -2
- package/image/watch.mjs +11 -11
- package/package.json +1 -1
package/image/agent.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The agent of a kind node. It
|
|
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
|
|
49
|
-
* those of a handshake. A kernel without CONFIG_NF_CONNTRACK_PROCFS has no
|
|
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
|
|
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
|
|
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
|
|
102
|
-
* of the softirqs (the kernel, all CPUs), and the drops of each veth.
|
|
103
|
-
* /proc/<pid>/net
|
|
104
|
-
*
|
|
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
|
|
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
|
|
153
|
-
* three quarters of the CPUs of a runner (
|
|
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`.
|
|
270
|
-
* interval and each pod of the node with a PID in it
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
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,
|
|
316
|
-
* (envoy_bootstrap.json of Istio 1.31.3)
|
|
317
|
-
*
|
|
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
|
|
363
|
-
* v1.39.4). A line `eds` gives the healthy hosts (`b`), and each
|
|
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
|
|
373
|
-
* manager. A line `lds` gives each update of a listener (`mod`) and each update in
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
4
|
+
"version": "0.11.7",
|
|
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",
|