@mark1russell7/harness 0.11.14 → 0.11.16

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/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { J as Subject, _ as ImageSource, i as ClientImage, r as Client } from "./cell-COlAmUZ-.js";
1
+ import { Y as Subject, i as ClientImage, r as Client, v as ImageSource } from "./cell-B6YHtKFb.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.d.ts
4
4
  /**
package/build/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- import { S as subjectProblems, a as SHIPPED_NAMES, d as imageProblems, t as CLIENT_NAMES } from "./image-DIJ1AFg_.js";
1
+ import { S as subjectProblems, a as SHIPPED_NAMES, d as imageProblems, t as CLIENT_NAMES } from "./image-RM60Paqe.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.ts
4
4
  /** This function adds each problem that a check finds as an issue of a schema. */
@@ -0,0 +1,11 @@
1
+ # The harness's RabbitMQ client: amqplib 2.2.0 on Node 24.21.0. It runs as a container in the client's
2
+ # network namespace, as the harness's own TCP client does, and prints the same JSON lines, with the
3
+ # events of the connection beside them.
4
+ # lab.ts stages the build context: this folder, under harness/image/amqp. The lockfile is
5
+ # npm-lock.json here, because npm leaves each package-lock.json out of a published tarball.
6
+ FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
7
+ WORKDIR /opt/harness/amqp
8
+ COPY harness/image/amqp/package.json ./
9
+ COPY harness/image/amqp/npm-lock.json ./package-lock.json
10
+ RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
11
+ COPY harness/image/amqp/client-amqp.mjs ./
@@ -0,0 +1,195 @@
1
+ // client-amqp HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH] [--before]
2
+ // [--options JSON]
3
+ //
4
+ // The client of a RabbitMQ cell: amqplib 2.2.0 on Node 24.21.0, with one connection and one channel.
5
+ // Each event is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds
6
+ // (process.hrtime, which libuv reads from it). The prober stamps the records of the kernel on the
7
+ // same clock, and the injector stamps each fault on it.
8
+ //
9
+ // The operations: operation 1 connects, opens the channel and declares a queue that the broker names
10
+ // (assertQueue). Each later operation declares that queue again, passively (checkQueue). Each is an
11
+ // RPC of the channel: the broker answers at once with declare-ok, and amqplib has no timer on it. The
12
+ // queue is exclusive, and it goes with the connection. RabbitMQ 4.3.6 refuses a transient queue that
13
+ // is not exclusive: transient_nonexcl_queues is denied by default (rabbit_amqqueue.erl:113-119, :228-241).
14
+ // The broker names it, because the exclusive queue of the client before can still exist: 405
15
+ // RESOURCE_LOCKED.
16
+ //
17
+ // The options (--options JSON): `heartbeat` in seconds goes in the URL. Without it, amqplib takes
18
+ // the heartbeat that the broker proposes. The others are the socket options of amqplib: noDelay,
19
+ // keepAlive, keepAliveDelay (ms) and timeout (ms). `user` and `password` are the user of the broker:
20
+ // guest, as amqplib gives it, when the options name none.
21
+ //
22
+ // The lines:
23
+ // - start: the versions of amqplib and Node, the mode and the options.
24
+ // - connecting: the start of operation 1. Its stamp is the stamp of the first sent line.
25
+ // - socket, socket-end, socket-error: the socket of the connection, with its local address.
26
+ // - connected: that socket. The TCP client of the harness names its own socket so.
27
+ // - connection, channel: each event of the connection (error, close, blocked, unblocked) and of
28
+ // the channel (error, close), with the error as data.
29
+ // - ready: the reply to operation 1: the connection is idle (idle, inflight), or the stream starts.
30
+ // - sent, ack, fail: each operation by its sequence number, with its error.
31
+ // - data, error: the end of the operation in flight, as the TCP client of the harness tells it. In
32
+ // idle mode, the first error of the connection is an error line too. The errno of an error line
33
+ // is the error of the connection when it has one: the RPC itself fails only with 'Channel ended'.
34
+ // - done, deadline: the end of the client.
35
+ //
36
+ // The modes: idle does operation 1, then holds until --max-s. inflight does operation 1, waits for
37
+ // the file --go, does one more operation into the fault, and holds after its end. stream does an
38
+ // operation each --interval-ms after the reply before, until --max-s or SIGTERM. --before starts the
39
+ // client into a fault that holds already: its connect meets it.
40
+
41
+ import { existsSync, readFileSync } from "node:fs";
42
+
43
+ const now = () => process.hrtime.bigint();
44
+ const emit = (ev, fields = {}, at = now()) => process.stdout.write(`${JSON.stringify({ ev, t: Number(at), ...fields })}\n`);
45
+
46
+ const argv = process.argv.slice(2);
47
+ const flag = (name) => {
48
+ const at = argv.indexOf(name);
49
+ return at === -1 ? undefined : argv[at + 1];
50
+ };
51
+ const [host, port] = argv;
52
+ const mode = flag("--mode");
53
+ if (host === undefined || port === undefined || !["idle", "inflight", "stream"].includes(mode)) {
54
+ emit("fail", { why: "client-amqp HOST PORT --mode idle|inflight|stream" });
55
+ process.exit(1);
56
+ }
57
+ const interval = Number(flag("--interval-ms") ?? 50);
58
+ const max = Number(flag("--max-s") ?? 120) * 1000;
59
+ const go = flag("--go") ?? "/tmp/go";
60
+ const before = argv.includes("--before");
61
+ // the options of the connection. A cell gives each option that it changes, and amqplib gives the others
62
+ const options = JSON.parse(flag("--options") ?? "{}");
63
+ const { heartbeat, user = "guest", password = "guest", ...socketOptions } = options;
64
+
65
+ const { default: amqp } = await import("amqplib");
66
+ const versionOf = (name) => JSON.parse(readFileSync(new URL(`./node_modules/${name}/package.json`, import.meta.url), "utf8")).version;
67
+
68
+ const bootId = () => {
69
+ try {
70
+ return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
71
+ } catch {
72
+ return null;
73
+ }
74
+ };
75
+ emit("start", { boot_id: bootId(), client: "amqp", driver: versionOf("amqplib"), node: process.versions.node, mode, options });
76
+
77
+ const url = `amqp://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${heartbeat === undefined ? "" : `?heartbeat=${heartbeat}`}`;
78
+
79
+ /** This function gives the error of amqplib as data: its name, its code and its message. */
80
+ const failure = (error) => (error === undefined || error === null ? null : { name: error.name, code: error.code ?? null, message: error.message });
81
+ /** This function gives the errno of an error: the code of the socket (ECONNRESET), the reply code of the broker, or the message of amqplib. */
82
+ const errnoOf = (error) => error?.code ?? error?.message ?? null;
83
+
84
+ // ---- the connection and its channel
85
+ let connection = null;
86
+ let channel = null;
87
+ // the queue of the client: the broker names it at operation 1
88
+ let queue = null;
89
+ // the first error of the connection: the cause of the failure of an RPC in flight
90
+ let cause = null;
91
+ const open = async () => {
92
+ connection = await amqp.connect(url, socketOptions);
93
+ const socket = connection.connection.stream;
94
+ const local = `${socket.localAddress}:${socket.localPort}`;
95
+ emit("socket", { role: "connection", local });
96
+ emit("connected", { local });
97
+ socket.once("close", (hadError) => emit("socket-end", { role: "connection", local, error: hadError }));
98
+ socket.once("error", (error) => emit("socket-error", { role: "connection", local, errno: errnoOf(error), message: error.message }));
99
+ // the first end of the connection: its error, or its close. The connection in idle mode: that
100
+ // end is the detection, as for the TCP client
101
+ const ended = (error) => {
102
+ if (cause !== null) return;
103
+ cause = error;
104
+ if (mode === "idle") emit("error", { errno: errnoOf(error), message: error.message });
105
+ };
106
+ // amqplib emits each error once, then its close. Without a listener, Node throws the error
107
+ connection.on("error", (error) => {
108
+ emit("connection", { event: "error", failure: failure(error) });
109
+ ended(error);
110
+ });
111
+ // a close of the broker with 320 (CONNECTION_FORCED) is not an error for amqplib: only its close tells
112
+ connection.on("close", (error) => {
113
+ emit("connection", { event: "close", failure: failure(error) });
114
+ if (!stopped) ended(error ?? new Error("Connection closed"));
115
+ });
116
+ connection.on("blocked", (reason) => emit("connection", { event: "blocked", reason }));
117
+ connection.on("unblocked", () => emit("connection", { event: "unblocked" }));
118
+ channel = await connection.createChannel();
119
+ channel.on("error", (error) => emit("channel", { event: "error", failure: failure(error) }));
120
+ channel.on("close", () => emit("channel", { event: "close" }));
121
+ };
122
+
123
+ // ---- the end of the client
124
+ let stopped = false;
125
+ const stop = (code = 0) => {
126
+ if (stopped) return;
127
+ stopped = true;
128
+ emit("done");
129
+ const closing = connection === null ? Promise.resolve() : connection.close().catch(() => undefined);
130
+ // a dead connection does not answer its close: the client does not wait for it
131
+ Promise.race([closing, new Promise((resolve) => setTimeout(resolve, 1000))]).finally(() => setTimeout(() => process.exit(code), 50));
132
+ };
133
+ process.on("SIGTERM", () => stop(0));
134
+ setTimeout(() => {
135
+ emit("deadline");
136
+ stop(0);
137
+ }, max);
138
+
139
+ /** This function does operation n. Its stamp is before the call. It gives null for a reply, or the error. */
140
+ const operate = async (n) => {
141
+ const at = now();
142
+ // a band of a cell that starts into a fault counts from the start of the connect
143
+ if (n === 1) emit("connecting", { at: `${host}:${port}` }, at);
144
+ emit("sent", { n }, at);
145
+ try {
146
+ if (n === 1) {
147
+ await open();
148
+ ({ queue } = await channel.assertQueue("", { durable: false, exclusive: true }));
149
+ } else {
150
+ await channel.checkQueue(queue);
151
+ }
152
+ emit("ack", { n });
153
+ return null;
154
+ } catch (error) {
155
+ emit("fail", { n, ...failure(error) });
156
+ return error;
157
+ }
158
+ };
159
+
160
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
161
+
162
+ if (mode === "stream") {
163
+ const failed = await operate(1);
164
+ if (failed === null) {
165
+ emit("ready", { at: `${host}:${port}` });
166
+ for (let n = 2; !stopped; n += 1) {
167
+ if ((await operate(n)) !== null) break;
168
+ await sleep(interval);
169
+ }
170
+ } else {
171
+ emit("error", { errno: errnoOf(cause ?? failed), message: (cause ?? failed).message });
172
+ stop(1);
173
+ }
174
+ } else {
175
+ // operation 1 makes the connection. Its reply is the last data before the fault
176
+ const failed = await operate(1);
177
+ if (failed !== null) {
178
+ emit("error", { errno: errnoOf(cause ?? failed), message: (cause ?? failed).message });
179
+ stop(before ? 0 : 1);
180
+ } else {
181
+ emit("ready", { reply: "ack" });
182
+ if (mode === "inflight") {
183
+ await new Promise((resolve) => {
184
+ const wait = setInterval(() => {
185
+ if (!existsSync(go)) return;
186
+ clearInterval(wait);
187
+ resolve();
188
+ }, 2);
189
+ });
190
+ const error = await operate(2);
191
+ if (error === null) emit("data", { n: 2 });
192
+ else emit("error", { errno: errnoOf(cause ?? error), message: (cause ?? error).message });
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "loom-harness-amqp-client",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "loom-harness-amqp-client",
8
+ "dependencies": {
9
+ "amqplib": "2.2.0"
10
+ }
11
+ },
12
+ "node_modules/amqplib": {
13
+ "version": "2.2.0",
14
+ "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-2.2.0.tgz",
15
+ "integrity": "sha512-VifzN9aHs/Q3zTruVHXh2yCisQyXexBN91CQ2E5iSyUKyNjH6ilYhJGte5AXZH/Bj1PaH7SwPhyuSf6Msd2udw==",
16
+ "license": "MIT",
17
+ "engines": {
18
+ "node": ">=18"
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "loom-harness-amqp-client",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "amqplib": "2.2.0"
7
+ }
8
+ }
@@ -27,13 +27,17 @@
27
27
  //! toxic of Toxiproxy. The stamps bracket the request and its answer, after the connect, so the
28
28
  //! commit of the toxic is known on the same clock as the kernel's records.
29
29
  //!
30
- //! - `signal NAME SIGNAL`: SIGNAL (KILL, STOP, CONT or TERM) to each process whose name is NAME,
31
- //! in the process namespace this sidecar shares with a member of a group. STOP and CONT freeze
32
- //! a server and thaw it while its kernel stays up; KILL stops it, and its kernel closes its
33
- //! sockets. The stamps bracket the kill(2) calls.
30
+ //! - `signal NAME SIGNAL`: SIGNAL (KILL, STOP, CONT, TERM or HUP) to each process whose name is
31
+ //! NAME, in the process namespace this sidecar shares with a member of a group or a hop. STOP and
32
+ //! CONT freeze a server and thaw it while its kernel stays up; KILL stops it, and its kernel
33
+ //! closes its sockets; HUP asks a pooler to read its configuration again. The stamps bracket the
34
+ //! kill(2) calls.
34
35
  //! - `resp ADDR:PORT ARG...`: one command to a Redis-protocol server, for example
35
36
  //! `SENTINEL FAILOVER mymaster` to a Sentinel. The stamps bracket the write of the command and
36
37
  //! the first line of its reply, so a planned failover starts at a known instant.
38
+ //! - `query ADDR:PORT USER DATABASE SQL`: one simple query to a server of the PostgreSQL protocol
39
+ //! that trusts the user, for example `PAUSE` on the console of PgBouncer. The stamps bracket the
40
+ //! write of the query and its ReadyForQuery, after the login, so the end of a PAUSE is known.
37
41
  //!
38
42
  //! A filter rule's PEER can be a list, `A,B,C`: the rules for each peer go in the same commit, so
39
43
  //! a part of a group loses all of its peers at one instant.
@@ -252,6 +256,7 @@ fn signal_number(name: &str) -> Result<libc::c_int, Box<dyn Error>> {
252
256
  "STOP" => libc::SIGSTOP,
253
257
  "CONT" => libc::SIGCONT,
254
258
  "TERM" => libc::SIGTERM,
259
+ "HUP" => libc::SIGHUP,
255
260
  other => return Err(format!("not a signal this injector sends: {other}").into()),
256
261
  })
257
262
  }
@@ -296,7 +301,7 @@ fn processes(name: &str) -> Result<Vec<libc::pid_t>, Box<dyn Error>> {
296
301
  /// `signal NAME SIGNAL [--at NS]`: the signal to each process of that name, bracketed by stamps
297
302
  fn signal(args: &[String]) -> Result<(), Box<dyn Error>> {
298
303
  let name = process_name(args.get(1).ok_or("signal NAME SIGNAL [--at NS]")?)?;
299
- let sig = signal_number(args.get(2).ok_or("and the signal: KILL, STOP, CONT or TERM")?)?;
304
+ let sig = signal_number(args.get(2).ok_or("and the signal: KILL, STOP, CONT, TERM or HUP")?)?;
300
305
  let opts = options(&args[3..])?;
301
306
  let pids = processes(name)?;
302
307
  if pids.is_empty() {
@@ -357,6 +362,98 @@ fn resp(args: &[String]) -> Result<(), Box<dyn Error>> {
357
362
  Ok(())
358
363
  }
359
364
 
365
+ /// a message of the PostgreSQL protocol from the client: its type, its length and its body
366
+ fn pg_message(kind: u8, body: &[u8]) -> Vec<u8> {
367
+ let mut out = vec![kind];
368
+ out.extend_from_slice(&((body.len() + 4) as u32).to_be_bytes());
369
+ out.extend_from_slice(body);
370
+ out
371
+ }
372
+
373
+ /// the startup message of the protocol 3.0 for a user and a database: it has no type byte
374
+ fn pg_startup(user: &str, database: &str) -> Vec<u8> {
375
+ let mut body = 196_608u32.to_be_bytes().to_vec();
376
+ for (key, value) in [("user", user), ("database", database)] {
377
+ body.extend_from_slice(key.as_bytes());
378
+ body.push(0);
379
+ body.extend_from_slice(value.as_bytes());
380
+ body.push(0);
381
+ }
382
+ body.push(0);
383
+ let mut out = ((body.len() + 4) as u32).to_be_bytes().to_vec();
384
+ out.extend_from_slice(&body);
385
+ out
386
+ }
387
+
388
+ /// one message of the server: its type and its body
389
+ fn pg_read(stream: &mut impl std::io::Read) -> Result<(u8, Vec<u8>), Box<dyn Error>> {
390
+ let mut head = [0u8; 5];
391
+ stream.read_exact(&mut head)?;
392
+ let length = u32::from_be_bytes([head[1], head[2], head[3], head[4]]) as usize;
393
+ if length < 4 {
394
+ return Err("a message of the server is shorter than its length field".into());
395
+ }
396
+ let mut body = vec![0u8; length - 4];
397
+ stream.read_exact(&mut body)?;
398
+ Ok((head[0], body))
399
+ }
400
+
401
+ /// the text of an ErrorResponse: its field M, or the whole body when it has none
402
+ fn pg_error(body: &[u8]) -> String {
403
+ body.split(|byte| *byte == 0)
404
+ .filter_map(|field| field.split_first())
405
+ .find(|(kind, _)| **kind == b'M')
406
+ .map(|(_, text)| String::from_utf8_lossy(text).to_string())
407
+ .unwrap_or_else(|| String::from_utf8_lossy(body).to_string())
408
+ }
409
+
410
+ /// `query ADDR:PORT USER DATABASE SQL [--at NS]`: one simple query, and its ReadyForQuery, bracketed
411
+ /// by stamps. The login must need no password: the console of PgBouncer with auth_type trust is one
412
+ fn query(args: &[String]) -> Result<(), Box<dyn Error>> {
413
+ use std::net::{SocketAddrV4, TcpStream};
414
+ let usage = "query ADDR:PORT USER DATABASE SQL [--at NS]";
415
+ let addr: SocketAddrV4 = args.get(1).ok_or(usage)?.parse()?;
416
+ let user = args.get(2).ok_or(usage)?;
417
+ let database = args.get(3).ok_or(usage)?;
418
+ let sql = args.get(4).ok_or(usage)?;
419
+ let opts = options(&args[5..])?;
420
+ let mut stream = TcpStream::connect(addr)?;
421
+ stream.write_all(&pg_startup(user, database))?;
422
+ // the login: AuthenticationOk, the parameters of the server, then ReadyForQuery
423
+ loop {
424
+ let (kind, body) = pg_read(&mut stream)?;
425
+ match kind {
426
+ b'R' if body.get(..4) == Some(&[0u8, 0, 0, 0][..]) => {}
427
+ b'R' => return Err("the server asks for a password: the query needs a user that it trusts".into()),
428
+ b'E' => return Err(format!("the login failed: {}", pg_error(&body)).into()),
429
+ b'Z' => break,
430
+ _ => {}
431
+ }
432
+ }
433
+ if let Some(at) = opts.at {
434
+ until(at)?;
435
+ }
436
+ let t0 = monotonic_ns();
437
+ let mut text = sql.as_bytes().to_vec();
438
+ text.push(0);
439
+ stream.write_all(&pg_message(b'Q', &text))?;
440
+ let mut tag = String::new();
441
+ loop {
442
+ let (kind, body) = pg_read(&mut stream)?;
443
+ match kind {
444
+ b'C' => tag = String::from_utf8_lossy(body.split(|byte| *byte == 0).next().unwrap_or(&[])).to_string(),
445
+ b'E' => return Err(format!("{sql} failed: {}", pg_error(&body)).into()),
446
+ b'Z' => break,
447
+ _ => {}
448
+ }
449
+ }
450
+ let t1 = monotonic_ns();
451
+ // Terminate: the server closes the session, and the sidecar does not wait for it
452
+ stream.write_all(&pg_message(b'X', &[]))?;
453
+ println!("{}", json!({ "action": "query", "query": sql, "tag": tag, "t0": t0, "t1": t1 }));
454
+ Ok(())
455
+ }
456
+
360
457
  /// the peers of a filter rule: one address, or a list `A,B,C`
361
458
  fn peers(text: &str) -> Result<Vec<Ipv4Addr>, Box<dyn Error>> {
362
459
  let list: Vec<Ipv4Addr> = text.split(',').map(str::parse).collect::<Result<_, _>>()?;
@@ -378,6 +475,9 @@ fn run() -> Result<(), Box<dyn Error>> {
378
475
  if action == "resp" {
379
476
  return resp(&args);
380
477
  }
478
+ if action == "query" {
479
+ return query(&args);
480
+ }
381
481
  match action {
382
482
  "netem" | "clear-netem" | "offload" => {
383
483
  let dev = device(args.get(1).ok_or("and a device")?)?;
@@ -522,7 +622,8 @@ mod tests {
522
622
  assert_eq!(signal_number("STOP").unwrap(), libc::SIGSTOP);
523
623
  assert_eq!(signal_number("CONT").unwrap(), libc::SIGCONT);
524
624
  assert_eq!(signal_number("KILL").unwrap(), libc::SIGKILL);
525
- assert!(signal_number("HUP").is_err());
625
+ assert_eq!(signal_number("HUP").unwrap(), libc::SIGHUP);
626
+ assert!(signal_number("USR1").is_err());
526
627
  assert!(process_name("valkey-server").is_ok());
527
628
  assert!(process_name("valkey-sentinel").is_ok());
528
629
  for name in ["", "a b", "x;y", "much-too-long-a-name"] {
@@ -543,6 +644,22 @@ mod tests {
543
644
  );
544
645
  }
545
646
 
647
+ #[test]
648
+ fn a_query_is_a_startup_message_and_a_simple_query() {
649
+ // the length counts itself, then the protocol 3.0, then each pair, then a final zero
650
+ let startup = pg_startup("postgres", "pgbouncer");
651
+ assert_eq!(&startup[..4], &(startup.len() as u32).to_be_bytes());
652
+ assert_eq!(&startup[4..8], &[0, 3, 0, 0]);
653
+ assert_eq!(&startup[8..], b"user\0postgres\0database\0pgbouncer\0\0");
654
+ assert_eq!(pg_message(b'Q', b"PAUSE\0"), b"Q\0\0\0\x0aPAUSE\0".to_vec());
655
+ assert_eq!(pg_message(b'X', &[]), b"X\0\0\0\x04".to_vec());
656
+ let mut reply: &[u8] = b"Z\0\0\0\x05I";
657
+ assert_eq!(pg_read(&mut reply).unwrap(), (b'Z', b"I".to_vec()));
658
+ let mut short: &[u8] = b"Z\0\0\0\x03";
659
+ assert!(pg_read(&mut short).is_err());
660
+ assert_eq!(pg_error(b"SERROR\0C08P01\0Mno such command\0\0"), "no such command");
661
+ }
662
+
546
663
  #[test]
547
664
  fn http_requests_take_only_a_method_a_path_and_a_length() {
548
665
  assert!(http_path("/proxies/redis/toxics").is_ok());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/harness",
4
- "version": "0.11.14",
4
+ "version": "0.11.16",
5
5
  "description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
6
6
  "keywords": [
7
7
  "fault-injection",
@@ -72,6 +72,10 @@
72
72
  "image/hikari/Dockerfile",
73
73
  "image/hikari/ClientHikari.java",
74
74
  "image/hikari/jars.sha256",
75
+ "image/amqp/Dockerfile",
76
+ "image/amqp/client-amqp.mjs",
77
+ "image/amqp/package.json",
78
+ "image/amqp/npm-lock.json",
75
79
  "image/procedures/Dockerfile",
76
80
  "image/procedures/package.json",
77
81
  "image/procedures/npm-lock.json",
@@ -1 +0,0 @@
1
- {"version":3,"file":"image-DIJ1AFg_.js","names":[],"sources":["../src/image.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, posix, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { BuildImage, Client, ClientImage, ImageSource, Shipped, ShippedImage, Subject, Target } from \"./cell.ts\";\n\n/**\n * This module resolves the source of each image (`ImageSource`) to an image that a lab can start.\n * A pinned image is its own name. A build context, or an image that the harness ships, becomes a\n * build with a tag from its content. Each image of the harness is a shipped source in one table.\n * Thus the procedures image of a subject is data, as the image of a caller is.\n */\n\n/**\n * These are the roots of the two packages that the shipped images read: this package, and the\n * prober that Node resolves for it. In a workspace, they are two folders side by side. From npm,\n * the prober is a link, and a Docker build context does not follow a link. Thus each image builds\n * from a context that the harness copies from the two roots.\n */\nconst ROOTS: Readonly<Record<string, string>> = {\n harness: fileURLToPath(new URL(\"..\", import.meta.url)),\n prober: dirname(realpathSync(createRequire(import.meta.url).resolve(\"@mark1russell7/prober/package.json\"))),\n};\n\n/** This function gives the path of a part of a shipped image, which is `<package>/<path>` in the context. */\nconst pathOf = (part: string): string => {\n const [root, ...rest] = part.split(\"/\");\n const base = ROOTS[root!];\n if (base === undefined) throw new Error(`${part} is in no package that the images read`);\n return join(base, ...rest);\n};\n\n/** A shipped context is the build context of a shipped image: its Dockerfile, and each file or folder that the Dockerfile reads, as `<package>/<path>`. */\ninterface ShippedContext {\n readonly name: string;\n readonly dockerfile: string;\n readonly from: readonly string[];\n}\n\n/** These are the build contexts of the images that the harness ships, by their names. */\nconst SHIPPED: Readonly<Record<Shipped, ShippedContext>> = {\n // the prober, the TCP client, the injector and the namespace admin\n harness: {\n name: \"loom-harness\",\n dockerfile: \"harness/image/Dockerfile\",\n from: [\"harness/image/Dockerfile\", \"harness/image/native\", \"prober/bpf\", \"prober/native\", \"prober/fixtures\"],\n },\n // the Node client: ioredis 6.0.0 on Node 24.21.0\n node: { name: \"loom-harness-node\", dockerfile: \"harness/image/node/Dockerfile\", from: [\"harness/image/node\"] },\n // the Go HTTP client: the http.Transport of Go 1.27.2\n \"go-http\": { name: \"loom-harness-go-http\", dockerfile: \"harness/image/go-http/Dockerfile\", from: [\"harness/image/go-http\"] },\n // the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2\n \"node-http\": { name: \"loom-harness-node-http\", dockerfile: \"harness/image/node-http/Dockerfile\", from: [\"harness/image/node-http\"] },\n // the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script\n mongo: { name: \"loom-harness-mongo\", dockerfile: \"harness/image/mongo/Dockerfile\", from: [\"harness/image/mongo\"] },\n // the PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0\n pg: { name: \"loom-harness-pg\", dockerfile: \"harness/image/pg/Dockerfile\", from: [\"harness/image/pg\"] },\n // the client of HikariCP 7.1.0 over pgjdbc 42.7.13 on Temurin 21.0.12.1: its jars by their SHA-256\n hikari: { name: \"loom-harness-hikari\", dockerfile: \"harness/image/hikari/Dockerfile\", from: [\"harness/image/hikari\"] },\n // @procedures/client 0.7.0 on Node 24.21.0: its WebSocket client, and the script of its server\n procedures: { name: \"loom-harness-procedures\", dockerfile: \"harness/image/procedures/Dockerfile\", from: [\"harness/image/procedures\"] },\n // the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow\n router: { name: \"loom-harness-router\", dockerfile: \"harness/image/router/Dockerfile\", from: [\"harness/image/router\"] },\n // PgBouncer 1.26.1, a pooler hop: built from its release tarball, by its SHA-256\n pgbouncer: { name: \"loom-harness-pgbouncer\", dockerfile: \"harness/image/pgbouncer/Dockerfile\", from: [\"harness/image/pgbouncer\"] },\n // Pgpool-II 4.7.3, a pooler hop: built from the source of its tag, by its SHA-256\n pgpool: { name: \"loom-harness-pgpool\", dockerfile: \"harness/image/pgpool/Dockerfile\", from: [\"harness/image/pgpool\"] },\n};\n\n/** These are the names of the images that the harness ships. */\nexport const SHIPPED_NAMES: readonly [Shipped, ...Shipped[]] = Object.keys(SHIPPED) as [Shipped, ...Shipped[]];\n\n/** This image is the stock base of the Node images of the harness: Node 24.21.0 on Alpine, pinned by digest. */\nconst NODE_BASE = \"node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1\";\n\n/**\n * These are the subjects that the harness ships, by their names. `procedures` is\n * `@procedures/client` on Node: its WebSocket client (`client-ws.mjs`) and its server\n * (`server-ws.mjs`), from one image. Thus the client and the server use one release.\n */\nexport const SUBJECTS: Readonly<Record<\"procedures\", Subject>> = {\n procedures: {\n name: \"procedures\",\n image: { kind: \"shipped\", name: \"procedures\" },\n base: NODE_BASE,\n programs: {\n client: [\"node\", \"/opt/harness/procedures/client-ws.mjs\"],\n server: [\"node\", \"/opt/harness/procedures/server-ws.mjs\"],\n },\n },\n};\n\n/** This function gives the source of a shipped image. */\nconst shipped = (name: Shipped): ImageSource => ({ kind: \"shipped\", name });\n\n/**\n * These are the clients that the harness ships, by their names (`Client`): the image of each, and\n * its command. A cell that names a client gets this image and this command.\n */\nexport const SHIPPED_CLIENTS: Readonly<Record<Client, ClientImage>> = {\n tcp: { image: shipped(\"harness\"), command: [\"/opt/harness/client\"] },\n ioredis: { image: shipped(\"node\"), command: [\"node\", \"/opt/harness/node/client-ioredis.mjs\"] },\n \"go-http\": { image: shipped(\"go-http\"), command: [\"/opt/harness/client-go\"] },\n \"http-agent\": { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-agent.mjs\"] },\n undici: { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-undici.mjs\"] },\n mongodb: { image: shipped(\"mongo\"), command: [\"node\", \"/opt/harness/mongo/client-mongo.mjs\"] },\n pg: { image: shipped(\"pg\"), command: [\"node\", \"/opt/harness/pg/client-pg.mjs\"] },\n hikari: {\n image: shipped(\"hikari\"),\n command: [\"java\", \"-Dslf4j.internal.verbosity=ERROR\", \"-cp\", \"/opt/harness/hikari/classes:/opt/harness/hikari/lib/*\", \"ClientHikari\"],\n },\n \"procedures-ws\": { image: SUBJECTS.procedures.image, command: SUBJECTS.procedures.programs[\"client\"]! },\n};\n\n/** These are the names of the clients that the harness ships. */\nexport const CLIENT_NAMES: readonly [Client, ...Client[]] = Object.keys(SHIPPED_CLIENTS) as [Client, ...Client[]];\n\n/** This function gives the command of a program of a subject by its role. It fails when the subject has no such program. */\nexport const programOf = (subject: Subject, role: string): readonly string[] => {\n const program = Object.hasOwn(subject.programs, role) ? subject.programs[role] : undefined;\n if (program === undefined) throw new Error(`the subject ${subject.name} has no program ${role}`);\n return program;\n};\n\n/**\n * This function gives the client of a subject for a cell (`Cell.client`): the image of the subject,\n * with the command of a program. A client that the harness ships goes by its name, thus its records\n * stay the same.\n */\nexport const subjectClient = (subject: Subject, role = \"client\"): Client | ClientImage => {\n const client: ClientImage = { image: subject.image, command: programOf(subject, role) };\n return CLIENT_NAMES.find((name) => isDeepStrictEqual(SHIPPED_CLIENTS[name], client)) ?? client;\n};\n\n/** This pattern matches an image pinned by digest: a name, a tag or none, then `@sha256:` and 64 hex digits. */\nexport const PINNED: RegExp = /^[a-z0-9][A-Za-z0-9._\\-/:]*@sha256:[0-9a-f]{64}$/;\n/** This pattern matches the repository of a tag that the harness builds. Its parts are lower case, with `/` between them. */\nexport const REPOSITORY: RegExp = /^[a-z0-9]+(?:[._-]+[a-z0-9]+)*(?:\\/[a-z0-9]+(?:[._-]+[a-z0-9]+)*)*$/;\n\n/** This function tells if a path stays in its context: relative, with `/` between its parts, and no `..`. */\nexport const inContext = (path: string): boolean =>\n typeof path === \"string\" &&\n path.length > 0 &&\n !path.startsWith(\"/\") &&\n !path.includes(\"\\\\\") &&\n !/^[A-Za-z]:/.test(path) &&\n !path.split(\"/\").includes(\"..\");\n\n/** This function tells if a value is a command: an array of one text or more. */\nconst isCommand = (value: unknown): boolean =>\n Array.isArray(value) && value.length > 0 && value.every((word) => typeof word === \"string\" && word.length > 0);\n\n/**\n * This function gives the problems of the source of an image, as data. A pinned image names a\n * digest. A build names a repository, a context, and paths that stay in the context. A shipped image\n * is one that the harness ships.\n */\nexport const imageProblems = (source: ImageSource): string[] => {\n const kind = (source as { readonly kind?: unknown } | null)?.kind;\n if (kind === \"pinned\") {\n const { image } = source as { readonly image?: unknown };\n return typeof image === \"string\" && PINNED.test(image) ? [] : [`${String(image)} is not pinned by digest: name@sha256:<64 hex digits>`];\n }\n if (kind === \"shipped\") {\n const { name } = source as { readonly name?: unknown };\n return typeof name === \"string\" && Object.hasOwn(SHIPPED, name) ? [] : [`the harness ships no image ${String(name)}: ${SHIPPED_NAMES.join(\", \")}`];\n }\n if (kind === \"build\") {\n const build = source as Partial<BuildImage>;\n const problems: string[] = [];\n if (typeof build.name !== \"string\" || !REPOSITORY.test(build.name)) problems.push(`${String(build.name)} is not the name of a repository`);\n if (typeof build.context !== \"string\" || build.context.length === 0) problems.push(\"the build names no context\");\n if (build.files !== undefined && (!Array.isArray(build.files) || build.files.length === 0)) problems.push(\"the build names no files\");\n for (const path of [build.dockerfile ?? \"Dockerfile\", ...(Array.isArray(build.files) ? build.files : [])])\n if (!inContext(path)) problems.push(`${String(path)} is not a path in the context`);\n return problems;\n }\n return [`${JSON.stringify(kind)} is not a kind of image source: pinned, build or shipped`];\n};\n\n/**\n * This function gives the problems of a subject, as data. A subject has a name and a base that a\n * digest pins. Its image source has no problems, and each of its programs is a command.\n */\nexport const subjectProblems = (subject: Subject): string[] => {\n const programs = (subject as { readonly programs?: unknown }).programs;\n const roles = typeof programs === \"object\" && programs !== null ? Object.entries(programs) : [];\n return [\n ...(typeof subject.name === \"string\" && subject.name.length > 0 ? [] : [\"the subject has no name\"]),\n ...(typeof subject.base === \"string\" && PINNED.test(subject.base) ? [] : [`the base ${String(subject.base)} is not pinned by digest`]),\n ...imageProblems(subject.image),\n ...(roles.length === 0 ? [\"the subject has no programs\"] : []),\n ...roles.flatMap(([role, command]) => (isCommand(command) ? [] : [`the program ${role} is not a command`])),\n ];\n};\n\n/**\n * This function gives each `FROM` of a Dockerfile that no digest pins. A `FROM` of an earlier stage\n * by its name, or of `scratch`, needs no digest. A base from an `ARG` has no digest that the harness\n * can read, thus it is in the list too.\n */\nexport const unpinnedBases = (dockerfile: string): string[] => {\n const stages = new Set<string>([\"scratch\"]);\n const found: string[] = [];\n for (const line of dockerfile.replace(/\\\\\\r?\\n/g, \" \").split(/\\r?\\n/)) {\n const from = /^\\s*FROM\\s+(.+)$/i.exec(line)?.[1];\n if (from === undefined) continue;\n const [image, as, stage] = from.split(/\\s+/).filter((word) => !word.startsWith(\"--\"));\n if (image === undefined) continue;\n if (!stages.has(image.toLowerCase()) && !PINNED.test(image)) found.push(image);\n if (as?.toLowerCase() === \"as\" && stage !== undefined) stages.add(stage.toLowerCase());\n }\n return found;\n};\n\n/** These are the lockfiles of each manifest of packages. A manifest without a lockfile installs other packages each time. */\nconst LOCKFILES: Readonly<Record<string, readonly string[]>> = {\n \"package.json\": [\"package-lock.json\", \"npm-lock.json\", \"npm-shrinkwrap.json\", \"pnpm-lock.yaml\", \"yarn.lock\", \"bun.lock\"],\n \"Cargo.toml\": [\"Cargo.lock\"],\n};\n\n/**\n * This function gives each manifest of packages in a context (`package.json`, `Cargo.toml`) that\n * has no lockfile in its folder or in a folder above it. The paths are the paths in the context.\n */\nexport const unlockedManifests = (paths: readonly string[]): string[] => {\n const present = new Set(paths);\n return paths.filter((path) => {\n const parts = path.split(\"/\");\n const locks = LOCKFILES[parts.at(-1)!];\n if (locks === undefined) return false;\n for (let depth = parts.length - 1; depth >= 0; depth -= 1) {\n const folder = parts.slice(0, depth).join(\"/\");\n if (locks.some((lock) => present.has(folder === \"\" ? lock : `${folder}/${lock}`))) return false;\n }\n return true;\n });\n};\n\n/**\n * An image plan tells how the harness gets the image of a source. It is a pull of an image by its\n * name, or a build of a context. A build has its tag, its Dockerfile, and each file with its path.\n */\nexport type ImagePlan =\n | { readonly kind: \"pull\"; readonly image: string }\n | {\n readonly kind: \"build\";\n readonly tag: string;\n readonly dockerfile: string;\n readonly files: ReadonlyArray<{ readonly file: string; readonly at: string }>;\n };\n\n/** This function gives each file under a path, in a stable sequence, but not the output of a local build. */\nconst sources = (path: string): string[] => {\n if (statSync(path).isFile()) return [path];\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.name !== \"target\" && entry.name !== \"node_modules\")\n .flatMap((entry) => sources(join(path, entry.name)))\n .sort();\n};\n\n/** A part is a file or a folder of a context: its path on the machine, and its path in the context. */\ninterface Part {\n readonly path: string;\n readonly at: string;\n}\n\n/** This function gives the name, the Dockerfile and the parts of the context of a build or of a shipped image. */\nconst contextOf = (source: BuildImage | ShippedImage): { name: string; dockerfile: string; parts: Part[] } => {\n if (source.kind === \"shipped\") {\n const { name, dockerfile, from } = SHIPPED[source.name];\n return { name, dockerfile, parts: from.map((part) => ({ path: pathOf(part), at: part })) };\n }\n const root = resolve(source.context);\n return {\n name: source.name,\n dockerfile: posix.normalize(source.dockerfile ?? \"Dockerfile\"),\n parts: (source.files ?? [\".\"]).map((file) => ({ path: resolve(root, file), at: file === \".\" ? \"\" : file })),\n };\n};\n\n/** This function gives a text for a source in a message. */\nconst labelOf = (source: ImageSource): string =>\n source.kind === \"pinned\" ? source.image : `the ${source.kind === \"build\" ? \"build\" : \"shipped image\"} ${source.name}`;\n\nconst shippedPlans = new Map<Shipped, ImagePlan>();\n\n/**\n * This function gives the plan of the image of a source. A stock image by its name, or a pinned\n * image, is a pull. A build or a shipped image is a build. Its tag is its name, then 12 digits of a\n * sha256. The sha256 reads the path of the Dockerfile, and the path and the content of each file.\n * Thus a changed program cannot operate as an old one.\n *\n * A build fails before it starts on a problem of its source. It also fails on a `FROM` that no\n * digest pins, and on a manifest of packages without a lockfile.\n */\nexport const planOf = (source: string | ImageSource): ImagePlan => {\n if (typeof source === \"string\") return { kind: \"pull\", image: source };\n const problems = imageProblems(source);\n if (problems.length > 0) throw new Error(`${labelOf(source)}: ${problems.join(\", \")}`);\n if (source.kind === \"pinned\") return { kind: \"pull\", image: source.image };\n const known = source.kind === \"shipped\" ? shippedPlans.get(source.name) : undefined;\n if (known !== undefined) return known;\n const { name, dockerfile, parts } = contextOf(source);\n const files = parts.flatMap((part) =>\n sources(part.path).map((file) => ({ file, at: join(part.at, relative(part.path, file)).replaceAll(\"\\\\\", \"/\") })),\n );\n const docker = files.find((each) => each.at === dockerfile);\n if (docker === undefined) throw new Error(`${labelOf(source)}: the context has no ${dockerfile}`);\n const unpinned = unpinnedBases(readFileSync(docker.file, \"utf8\"));\n const unlocked = unlockedManifests(files.map((each) => each.at));\n if (unpinned.length > 0 || unlocked.length > 0)\n throw new Error(\n `${labelOf(source)}: ${[\n ...unpinned.map((image) => `the base ${image} is not pinned by digest`),\n ...unlocked.map((path) => `${path} has no lockfile`),\n ].join(\", \")}`,\n );\n const hash = createHash(\"sha256\");\n hash.update(dockerfile);\n for (const { file, at } of files) {\n hash.update(at);\n hash.update(readFileSync(file));\n }\n const plan: ImagePlan = { kind: \"build\", tag: `${name}:${hash.digest(\"hex\").slice(0, 12)}`, dockerfile, files };\n if (source.kind === \"shipped\") shippedPlans.set(source.name, plan);\n return plan;\n};\n\n/** This function builds a staged context with Docker, through Testcontainers. */\nconst dockerBuild = async (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>): Promise<void> => {\n const { GenericContainer } = await import(\"testcontainers\");\n await GenericContainer.fromDockerfile(context, plan.dockerfile).build(plan.tag, { deleteOnExit: false });\n};\n\nconst built = new Map<string, Promise<string>>();\n\n/**\n * This function builds the image of a plan one time for each process, from a new context that it\n * copies the files into. Docker keeps its layers between runs. A build that fails leaves the cache.\n * Thus a later cell builds again after a fault of Docker or of the network.\n */\nexport const buildOnce = (\n plan: Extract<ImagePlan, { readonly kind: \"build\" }>,\n builder: (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>) => Promise<void> = dockerBuild,\n): Promise<string> => {\n let tag = built.get(plan.tag);\n if (tag === undefined) {\n const building = (async () => {\n const context = mkdtempSync(join(tmpdir(), `${plan.tag.split(\":\")[0]!.replaceAll(\"/\", \"-\")}-`));\n try {\n for (const { file, at } of plan.files) {\n mkdirSync(dirname(join(context, at)), { recursive: true });\n copyFileSync(file, join(context, at));\n }\n await builder(context, plan);\n } finally {\n rmSync(context, { recursive: true, force: true });\n }\n return plan.tag;\n })();\n // a failed build does not stay: the next caller tries again\n building.catch(() => {\n if (built.get(plan.tag) === building) built.delete(plan.tag);\n });\n built.set(plan.tag, building);\n tag = building;\n }\n return tag;\n};\n\n/**\n * This function gives an image that a lab can start, from its source. It is the name of a stock\n * image or of a pinned image, which the lab pulls, or the tag of a build. This function loads\n * Testcontainers only for a build.\n */\nexport const imageOf = async (source: string | ImageSource): Promise<string> => {\n const plan = planOf(source);\n return plan.kind === \"pull\" ? plan.image : buildOnce(plan);\n};\n\n/** This function gives the image of the harness: the prober, the TCP client, the injector and the namespace admin. */\nexport const harnessImage = (): Promise<string> => imageOf(shipped(\"harness\"));\n/** This function gives the image of the Node client. */\nexport const nodeClientImage = (): Promise<string> => imageOf(shipped(\"node\"));\n/** This function gives the image of the router. */\nexport const routerImage = (): Promise<string> => imageOf(shipped(\"router\"));\n/** This function gives the image of the Go HTTP client. */\nexport const goHttpImage = (): Promise<string> => imageOf(shipped(\"go-http\"));\n/** This function gives the image of the Node HTTP clients. */\nexport const nodeHttpImage = (): Promise<string> => imageOf(shipped(\"node-http\"));\n/** This function gives the image of the MongoDB client. */\nexport const mongoClientImage = (): Promise<string> => imageOf(shipped(\"mongo\"));\n/** This function gives the image of the PostgreSQL client. */\nexport const pgClientImage = (): Promise<string> => imageOf(shipped(\"pg\"));\n/** This function gives the image of the client of HikariCP over pgjdbc. */\nexport const hikariClientImage = (): Promise<string> => imageOf(shipped(\"hikari\"));\n/** This function gives the image of the procedures subject: the WebSocket client of `@procedures/client`, and its server. */\nexport const proceduresImage = (): Promise<string> => imageOf(SUBJECTS.procedures.image);\n\n/**\n * This function gives the source of the image that a target operates. It is the image of its\n * subject (`Target.subject`), a shipped image by its short form (`Target.harness`), or its stock image.\n */\nexport const targetSourceOf = (target: Target): string | ImageSource =>\n target.subject ?? (target.harness === undefined ? target.image : shipped(target.harness));\n\n/** This function gives the image that a target operates: the image of its subject, or its stock image. */\nexport const targetImageOf = (target: Target): Promise<string> => imageOf(targetSourceOf(target));\n"],"mappings":"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"}