mosquitto: run mosquitto_ctrl inside the broker's container #249
@@ -18,6 +18,7 @@ import { randomBytes } from "node:crypto";
|
|||||||
import { connect as tcpConnect } from "node:net";
|
import { connect as tcpConnect } from "node:net";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
import { basename, dirname } from "node:path";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
import { missingAcls, parseRoleAcls, staleAcls, wantedAcls } from "./topics.js";
|
import { missingAcls, parseRoleAcls, staleAcls, wantedAcls } from "./topics.js";
|
||||||
@@ -30,6 +31,14 @@ export interface MqttConn {
|
|||||||
/** The Dynamic Security admin client the runtime authenticates as. */
|
/** The Dynamic Security admin client the runtime authenticates as. */
|
||||||
readonly adminUser: string;
|
readonly adminUser: string;
|
||||||
readonly adminPassword: string;
|
readonly adminPassword: string;
|
||||||
|
/**
|
||||||
|
* The broker's own container, when `mosquitto_ctrl` is run inside it rather than from this
|
||||||
|
* machine's packages. The broker's image carries the tool at the broker's version, and inside it
|
||||||
|
* the broker listens on 127.0.0.1:1883 whatever port the machine publishes.
|
||||||
|
*/
|
||||||
|
readonly container?: string;
|
||||||
|
/** The broker's image, to seed the security file before the broker has ever started. */
|
||||||
|
readonly image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MosquittoClient {
|
export class MosquittoClient {
|
||||||
@@ -53,7 +62,11 @@ export class MosquittoClient {
|
|||||||
"mosquitto host or admin password is not set — mosquitto's own code cannot reach the broker",
|
"mosquitto host or admin password is not set — mosquitto's own code cannot reach the broker",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new MosquittoClient({ host, port, adminUser, adminPassword: adminPassword ?? "" });
|
return new MosquittoClient({
|
||||||
|
host, port, adminUser, adminPassword: adminPassword ?? "",
|
||||||
|
container: env.MESH_MQTT_CTRL_CONTAINER || undefined,
|
||||||
|
image: env.MESH_MQTT_CTRL_IMAGE || undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get host(): string {
|
get host(): string {
|
||||||
@@ -84,16 +97,18 @@ export class MosquittoClient {
|
|||||||
* to this single-purpose runtime container; see the module README.
|
* to this single-purpose runtime container; see the module README.
|
||||||
*/
|
*/
|
||||||
async ctl(...args: string[]): Promise<string> {
|
async ctl(...args: string[]): Promise<string> {
|
||||||
|
const inside = this.conn.container !== undefined;
|
||||||
const base = [
|
const base = [
|
||||||
"-h", this.conn.host,
|
"-h", inside ? "127.0.0.1" : this.conn.host,
|
||||||
"-p", String(this.conn.port),
|
"-p", inside ? "1883" : String(this.conn.port),
|
||||||
"-u", this.conn.adminUser,
|
"-u", this.conn.adminUser,
|
||||||
"-P", this.conn.adminPassword,
|
"-P", this.conn.adminPassword,
|
||||||
];
|
];
|
||||||
let stdout: string;
|
let stdout: string;
|
||||||
let stderr: string;
|
let stderr: string;
|
||||||
try {
|
try {
|
||||||
({ stdout, stderr } = await run("mosquitto_ctrl", [...base, "dynsec", ...args], {
|
const [command, argv] = this.ctrl([...base, "dynsec", ...args]);
|
||||||
|
({ stdout, stderr } = await run(command, argv, {
|
||||||
maxBuffer: 16 << 20,
|
maxBuffer: 16 << 20,
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
}));
|
}));
|
||||||
@@ -240,10 +255,27 @@ export class MosquittoClient {
|
|||||||
async initBootstrapFile(configFile: string): Promise<void> {
|
async initBootstrapFile(configFile: string): Promise<void> {
|
||||||
// `dynsec init <file> <admin-username> [admin-password]` is an offline file operation — it does
|
// `dynsec init <file> <admin-username> [admin-password]` is an offline file operation — it does
|
||||||
// not connect to the broker. The password is a positional argument (omitting it prompts).
|
// not connect to the broker. The password is a positional argument (omitting it prompts).
|
||||||
|
if (this.conn.image) {
|
||||||
|
// Before the broker has ever started there is no container to enter: a throwaway one from the
|
||||||
|
// broker's own image writes the file into the directory the broker will mount.
|
||||||
|
await run("docker", [
|
||||||
|
"run", "--rm", "--entrypoint", "mosquitto_ctrl",
|
||||||
|
"-v", `${dirname(configFile)}:/mosquitto/data`,
|
||||||
|
this.conn.image,
|
||||||
|
"dynsec", "init", `/mosquitto/data/${basename(configFile)}`, this.conn.adminUser, this.conn.adminPassword,
|
||||||
|
], { maxBuffer: 16 << 20 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
await run("mosquitto_ctrl", ["dynsec", "init", configFile, this.conn.adminUser, this.conn.adminPassword], {
|
await run("mosquitto_ctrl", ["dynsec", "init", configFile, this.conn.adminUser, this.conn.adminPassword], {
|
||||||
maxBuffer: 16 << 20,
|
maxBuffer: 16 << 20,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How `mosquitto_ctrl` is run here: inside the broker's container when one is named. */
|
||||||
|
ctrl(argv: string[]): [string, string[]] {
|
||||||
|
if (this.conn.container) return ["docker", ["exec", this.conn.container, "mosquitto_ctrl", ...argv]];
|
||||||
|
return ["mosquitto_ctrl", argv];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generate a URL-safe password with no argv- or MQTT-hostile characters. */
|
/** Generate a URL-safe password with no argv- or MQTT-hostile characters. */
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "${dir:state}/bootstrap.env",
|
"path": "${dir:state}/bootstrap.env",
|
||||||
"mode": "0600",
|
"mode": "0600",
|
||||||
"content": "MESH_PROVISION_MQTT=127.0.0.1:${port:1883}\nMESH_PROVISION_ADMIN_USER=mesh-admin\nMESH_PROVISION_PASSWORD_FILE=${dir:mesh-state}/admin\nMESH_DYNSEC_FILE=${dir:data}/dynamic-security.json\n"
|
"content": "MESH_PROVISION_MQTT=127.0.0.1:${port:1883}\nMESH_PROVISION_ADMIN_USER=mesh-admin\nMESH_PROVISION_PASSWORD_FILE=${dir:mesh-state}/admin\nMESH_DYNSEC_FILE=${dir:data}/dynamic-security.json\nMESH_MQTT_CTRL_IMAGE=eclipse-mosquitto@sha256:38c0da4f2ef84284d47b3b3eeea1cb3bdeabe81ee10caf0cd5c5ff61ee3ea408\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bootstrap",
|
"id": "bootstrap",
|
||||||
@@ -125,11 +125,6 @@
|
|||||||
"${dir:data}:/mosquitto/data",
|
"${dir:data}:/mosquitto/data",
|
||||||
"${dir:state}/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro"
|
"${dir:state}/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "client",
|
|
||||||
"type": "package",
|
|
||||||
"package": "mosquitto"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"build": {
|
"build": {
|
||||||
@@ -153,7 +148,8 @@
|
|||||||
"MESH_RECEIVES": "${dir:grants}/mesh.json",
|
"MESH_RECEIVES": "${dir:grants}/mesh.json",
|
||||||
"MESH_PROVISION_MQTT": "127.0.0.1:${port:1883}",
|
"MESH_PROVISION_MQTT": "127.0.0.1:${port:1883}",
|
||||||
"MESH_PROVISION_ADMIN_USER": "mesh-admin",
|
"MESH_PROVISION_ADMIN_USER": "mesh-admin",
|
||||||
"MESH_PROVISION_PASSWORD_FILE": "${dir:mesh-state}/admin"
|
"MESH_PROVISION_PASSWORD_FILE": "${dir:mesh-state}/admin",
|
||||||
|
"MESH_MQTT_CTRL_CONTAINER": "mosquitto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Run after `npm run build`.
|
||||||
|
// mosquitto_ctrl runs inside the broker's own container when the manifest names it, so a machine
|
||||||
|
// needs no mosquitto package (whose index may be too stale to install from) and the tool always
|
||||||
|
// matches the broker's version.
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { MosquittoClient } from "../dist/client.js"; // compiled: client.ts uses parameter properties, which type stripping cannot run
|
||||||
|
|
||||||
|
const env = { MESH_PROVISION_MQTT: "127.0.0.1:21883", MESH_MQTT_PASSWORD: "pw" };
|
||||||
|
|
||||||
|
test("named, the broker's container runs mosquitto_ctrl", () => {
|
||||||
|
const c = MosquittoClient.fromEnv({ ...env, MESH_MQTT_CTRL_CONTAINER: "mosquitto" });
|
||||||
|
assert.deepEqual(c.ctrl(["dynsec", "listClients"]), ["docker", ["exec", "mosquitto", "mosquitto_ctrl", "dynsec", "listClients"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unnamed, this machine's mosquitto_ctrl runs", () => {
|
||||||
|
const c = MosquittoClient.fromEnv(env);
|
||||||
|
assert.deepEqual(c.ctrl(["dynsec", "listClients"]), ["mosquitto_ctrl", ["dynsec", "listClients"]]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user