feat(gui): docker orchestration and fleet supervision (bus daemon)
This commit is contained in:
+6
-3
@@ -42,6 +42,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
COPY --from=build /workspace/target/release/tines /usr/local/bin/
|
COPY --from=build /workspace/target/release/tines /usr/local/bin/
|
||||||
COPY --from=build /workspace/target/release/tines-busd /usr/local/bin/
|
COPY --from=build /workspace/target/release/tines-busd /usr/local/bin/
|
||||||
|
|
||||||
# The config path comes as the container arg (BT or bus mode, whichever the
|
# The container command is the pinned argv from the fleet console:
|
||||||
# config selects); no default CMD.
|
# `tines /data/config.json` — the binary comes first because the service
|
||||||
ENTRYPOINT ["tines"]
|
# reads the config path from argv[1]. No ENTRYPOINT (it would swallow the
|
||||||
|
# argv); no default CMD (a bare `docker run` without the command is an
|
||||||
|
# error, which is what we want).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
//! Docker orchestration: build the runtime image and drive one container
|
||||||
|
//! per node. Every command runs through `std::process::Command` with
|
||||||
|
//! pinned argv vectors — nothing is ever shell-interpolated.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Output};
|
||||||
|
|
||||||
|
use crate::config::fleet_dir;
|
||||||
|
|
||||||
|
/// A docker container id (the value `docker run -d` prints).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ContainerId(pub String);
|
||||||
|
|
||||||
|
/// Failures the docker module can report.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DockerError {
|
||||||
|
/// The `docker` binary is not on PATH.
|
||||||
|
NotInstalled,
|
||||||
|
/// The docker process could not be spawned.
|
||||||
|
Spawn(io::Error),
|
||||||
|
/// Docker ran but exited nonzero.
|
||||||
|
Exit {
|
||||||
|
/// The exit code.
|
||||||
|
code: i32,
|
||||||
|
/// The process's captured stderr.
|
||||||
|
stderr: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DockerError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DockerError::NotInstalled => write!(f, "docker is not installed or not on PATH"),
|
||||||
|
DockerError::Spawn(e) => write!(f, "failed to spawn docker: {e}"),
|
||||||
|
DockerError::Exit { code, stderr } => {
|
||||||
|
write!(f, "docker exited {code}: {}", stderr.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for DockerError {}
|
||||||
|
|
||||||
|
/// The docker frontend. A unit struct: docker state lives in the daemon,
|
||||||
|
/// not here.
|
||||||
|
pub struct Docker;
|
||||||
|
|
||||||
|
impl Docker {
|
||||||
|
/// The pinned runtime image tag.
|
||||||
|
pub fn image() -> &'static str {
|
||||||
|
"tines-stack:latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the runtime image from the repo root — the crate's parent
|
||||||
|
/// directory, baked in at compile time (`CARGO_MANIFEST_DIR` points at
|
||||||
|
/// `tines-gui/`).
|
||||||
|
pub fn build_image(&self) -> Result<(), DockerError> {
|
||||||
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.expect("tines-gui lives one level below the repo root")
|
||||||
|
.to_path_buf();
|
||||||
|
let argv: Vec<String> = vec![
|
||||||
|
"docker".into(),
|
||||||
|
"build".into(),
|
||||||
|
"-f".into(),
|
||||||
|
"Dockerfile.tines".into(),
|
||||||
|
"-t".into(),
|
||||||
|
Self::image().into(),
|
||||||
|
".".into(),
|
||||||
|
];
|
||||||
|
let mut cmd = spawn(&argv)?;
|
||||||
|
cmd.current_dir(root);
|
||||||
|
run(&mut cmd)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exact argv for `start_bt`. Exposed for tests and debugging;
|
||||||
|
/// [`Self::start_bt`] runs this vector verbatim.
|
||||||
|
pub fn start_bt_argv(&self, name: &str, fleet: &str) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"docker".into(),
|
||||||
|
"run".into(),
|
||||||
|
"-d".into(),
|
||||||
|
"--name".into(),
|
||||||
|
format!("tines-bt-{name}"),
|
||||||
|
"--privileged".into(),
|
||||||
|
"--net=host".into(),
|
||||||
|
"-v".into(),
|
||||||
|
"/run/dbus/system_bus_socket:/run/dbus/system_bus_socket".into(),
|
||||||
|
"-v".into(),
|
||||||
|
format!("{}:/data", node_dir(fleet, name).display()),
|
||||||
|
Self::image().into(),
|
||||||
|
"tines".into(),
|
||||||
|
"/data/config.json".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exact argv for `start_sim`. Exposed for tests and debugging;
|
||||||
|
/// [`Self::start_sim`] runs this vector verbatim.
|
||||||
|
pub fn start_sim_argv(&self, name: &str, fleet: &str) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"docker".into(),
|
||||||
|
"run".into(),
|
||||||
|
"-d".into(),
|
||||||
|
"--name".into(),
|
||||||
|
format!("tines-sim-{name}"),
|
||||||
|
"-v".into(),
|
||||||
|
format!("{}:/data", node_dir(fleet, name).display()),
|
||||||
|
Self::image().into(),
|
||||||
|
"tines".into(),
|
||||||
|
"/data/config.json".into(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exact argv for `stop`. Exposed for tests and debugging.
|
||||||
|
pub fn stop_argv(&self, id: &ContainerId) -> Vec<String> {
|
||||||
|
vec!["docker".into(), "stop".into(), id.0.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exact argv for `remove`. Exposed for tests and debugging.
|
||||||
|
pub fn remove_argv(&self, id: &ContainerId) -> Vec<String> {
|
||||||
|
vec!["docker".into(), "rm".into(), id.0.clone()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exact argv for `logs`. Exposed for tests and debugging.
|
||||||
|
pub fn logs_argv(&self, id: &ContainerId, tail: u32) -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"docker".into(),
|
||||||
|
"logs".into(),
|
||||||
|
"--tail".into(),
|
||||||
|
tail.to_string(),
|
||||||
|
id.0.clone(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a bt-mode node container: privileged, host networking, the
|
||||||
|
/// system D-Bus socket (for the host Bluetooth adapter), and the node's
|
||||||
|
/// fleet dir mounted at `/data`. Returns the container id.
|
||||||
|
pub fn start_bt(&self, name: &str, fleet: &str) -> Result<ContainerId, DockerError> {
|
||||||
|
let out = run_output(&self.start_bt_argv(name, fleet))?;
|
||||||
|
Ok(ContainerId(out.trim().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a sim-mode node container: same shape, no privileges or host
|
||||||
|
/// networking. Returns the container id.
|
||||||
|
pub fn start_sim(&self, name: &str, fleet: &str) -> Result<ContainerId, DockerError> {
|
||||||
|
let out = run_output(&self.start_sim_argv(name, fleet))?;
|
||||||
|
Ok(ContainerId(out.trim().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop a container.
|
||||||
|
pub fn stop(&self, id: &ContainerId) -> Result<(), DockerError> {
|
||||||
|
run(&mut spawn(&self.stop_argv(id))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a (stopped) container.
|
||||||
|
pub fn remove(&self, id: &ContainerId) -> Result<(), DockerError> {
|
||||||
|
run(&mut spawn(&self.remove_argv(id))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The container's recent logs (stdout and stderr merged), newest
|
||||||
|
/// `tail` lines last.
|
||||||
|
pub fn logs(&self, id: &ContainerId, tail: u32) -> Result<String, DockerError> {
|
||||||
|
run_output(&self.logs_argv(id, tail))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `docker inspect` succeeds for the id — i.e. the container
|
||||||
|
/// exists (the inspect exit status is the pin).
|
||||||
|
pub fn is_running(&self, id: &ContainerId) -> bool {
|
||||||
|
match Command::new("docker").arg("inspect").arg(&id.0).output() {
|
||||||
|
Ok(Output { status, .. }) => status.success(),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `<fleet_dir>/<name>` — the dir mounted at `/data` inside the container.
|
||||||
|
fn node_dir(fleet: &str, name: &str) -> PathBuf {
|
||||||
|
fleet_dir(fleet).join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a command expecting success, capturing stdout.
|
||||||
|
fn run_output(argv: &[String]) -> Result<String, DockerError> {
|
||||||
|
let out = spawn(argv)?.output().map_err(DockerError::Spawn)?;
|
||||||
|
if out.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||||
|
} else {
|
||||||
|
Err(DockerError::Exit {
|
||||||
|
code: out.status.code().unwrap_or(-1),
|
||||||
|
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a command expecting success, output inherited.
|
||||||
|
fn run(cmd: &mut Command) -> Result<(), DockerError> {
|
||||||
|
let status = cmd.status().map_err(DockerError::Spawn)?;
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(DockerError::Exit {
|
||||||
|
code: status.code().unwrap_or(-1),
|
||||||
|
stderr: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The docker binary or a `NotInstalled` error.
|
||||||
|
fn spawn(argv: &[String]) -> Result<Command, DockerError> {
|
||||||
|
if Command::new("docker").arg("--version").output().is_err() {
|
||||||
|
return Err(DockerError::NotInstalled);
|
||||||
|
}
|
||||||
|
let mut cmd = Command::new(&argv[0]);
|
||||||
|
cmd.args(&argv[1..]);
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//! A fleet: the on-disk layout plus the bus daemon that serves it.
|
||||||
|
//!
|
||||||
|
//! [`Fleet::create`] makes the fleet dir (`0700`, via
|
||||||
|
//! [`crate::config::fleet_dir`]) and spawns `tines-busd
|
||||||
|
//! <fleet_dir>/bus.sock` as a supervised child — the daemon is what lets
|
||||||
|
//! every sim-mode node on the fleet exchange frames over the host IPC bus.
|
||||||
|
//! Node configs land under `<fleet_dir>/<name>/config.json` and mount into
|
||||||
|
//! the containers at `/data`, so all container-side paths are `/data/...`.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
use crate::config::{NodeConfig, fleet_dir, generate_node_id};
|
||||||
|
|
||||||
|
/// How a node connects to its peers.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Mode {
|
||||||
|
/// A real Bluetooth radio (privileged container, host adapter).
|
||||||
|
Bt,
|
||||||
|
/// The host IPC bus (simulated fleet).
|
||||||
|
Sim,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What `add_node` produced: the node's name and its config dir.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NodeSpec {
|
||||||
|
/// The node name (also the dir and container-name stem).
|
||||||
|
pub name: String,
|
||||||
|
/// `<fleet_dir>/<name>` — holds `config.json`.
|
||||||
|
pub dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A live fleet with a supervised bus daemon.
|
||||||
|
pub struct Fleet {
|
||||||
|
/// The fleet name.
|
||||||
|
fleet: String,
|
||||||
|
/// `<fleet_dir>`.
|
||||||
|
dir: PathBuf,
|
||||||
|
/// The daemon child; `None` after `stop_bus`.
|
||||||
|
busd: Option<Child>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fleet {
|
||||||
|
/// Create the fleet dir (`0700`) and spawn the bus daemon on
|
||||||
|
/// `<fleet_dir>/bus.sock`. `tines-busd` must be on PATH.
|
||||||
|
pub fn create(fleet: &str) -> io::Result<Fleet> {
|
||||||
|
let dir = fleet_dir(fleet);
|
||||||
|
let child = Command::new("tines-busd")
|
||||||
|
.arg(dir.join("bus.sock"))
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!("cannot spawn tines-busd (is it on PATH?): {e}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(Fleet {
|
||||||
|
fleet: fleet.to_string(),
|
||||||
|
dir,
|
||||||
|
busd: Some(child),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bus daemon's socket: `<fleet_dir>/bus.sock`.
|
||||||
|
pub fn bus_socket(&self) -> PathBuf {
|
||||||
|
self.dir.join("bus.sock")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a node's service config and return its spec. A missing
|
||||||
|
/// `node_id` gets a fresh generated one. Bus mode points `bus_socket`
|
||||||
|
/// at the in-container `/data/bus.sock` (the fleet dir is mounted at
|
||||||
|
/// `/data`); log and IPC paths are `/data/<name>/log` and
|
||||||
|
/// `/data/<name>/sock`.
|
||||||
|
pub fn add_node(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
node_id: Option<&str>,
|
||||||
|
mode: Mode,
|
||||||
|
peer: Option<&str>,
|
||||||
|
) -> io::Result<NodeSpec> {
|
||||||
|
let node_id = match node_id {
|
||||||
|
Some(id) => id.to_string(),
|
||||||
|
None => generate_node_id(),
|
||||||
|
};
|
||||||
|
let peer = peer.map(str::to_string);
|
||||||
|
let log_path = format!("/data/{name}/log");
|
||||||
|
let socket_path = format!("/data/{name}/sock");
|
||||||
|
let config = match mode {
|
||||||
|
Mode::Bt => NodeConfig::bt(node_id, peer, log_path, socket_path),
|
||||||
|
Mode::Sim => NodeConfig::bus(
|
||||||
|
node_id,
|
||||||
|
peer,
|
||||||
|
log_path,
|
||||||
|
socket_path,
|
||||||
|
"/data/bus.sock".into(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
config.write(&self.fleet, name)?;
|
||||||
|
Ok(NodeSpec {
|
||||||
|
name: name.to_string(),
|
||||||
|
dir: self.dir.join(name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the bus daemon.
|
||||||
|
pub fn stop_bus(&mut self) {
|
||||||
|
if let Some(mut child) = self.busd.take() {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Fleet {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop_bus();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,5 +8,7 @@
|
|||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod docker;
|
||||||
|
pub mod fleet;
|
||||||
pub mod ipc_client;
|
pub mod ipc_client;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//! Command-building tests for the docker module: the exact argv vectors,
|
||||||
|
//! no real docker involved.
|
||||||
|
|
||||||
|
use tines_gui::config::fleet_dir;
|
||||||
|
use tines_gui::docker::{ContainerId, Docker};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_bt_args_match_pinned_flags() {
|
||||||
|
let docker = Docker;
|
||||||
|
let name = "scout";
|
||||||
|
let fleet = "yard";
|
||||||
|
let mounted = fleet_dir(fleet).join(name);
|
||||||
|
let mounted = mounted.display().to_string();
|
||||||
|
|
||||||
|
let argv = docker.start_bt_argv(name, fleet);
|
||||||
|
let expected: Vec<String> = vec![
|
||||||
|
"docker".into(),
|
||||||
|
"run".into(),
|
||||||
|
"-d".into(),
|
||||||
|
"--name".into(),
|
||||||
|
format!("tines-bt-{name}"),
|
||||||
|
"--privileged".into(),
|
||||||
|
"--net=host".into(),
|
||||||
|
"-v".into(),
|
||||||
|
"/run/dbus/system_bus_socket:/run/dbus/system_bus_socket".into(),
|
||||||
|
"-v".into(),
|
||||||
|
format!("{mounted}:/data"),
|
||||||
|
"tines-stack:latest".into(),
|
||||||
|
"tines".into(),
|
||||||
|
"/data/config.json".into(),
|
||||||
|
];
|
||||||
|
assert_eq!(argv, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_sim_args_match_pinned_flags() {
|
||||||
|
let docker = Docker;
|
||||||
|
let name = "cargo";
|
||||||
|
let fleet = "yard";
|
||||||
|
let mounted = fleet_dir(fleet).join(name);
|
||||||
|
let mounted = mounted.display().to_string();
|
||||||
|
|
||||||
|
let argv = docker.start_sim_argv(name, fleet);
|
||||||
|
let expected: Vec<String> = vec![
|
||||||
|
"docker".into(),
|
||||||
|
"run".into(),
|
||||||
|
"-d".into(),
|
||||||
|
"--name".into(),
|
||||||
|
format!("tines-sim-{name}"),
|
||||||
|
"-v".into(),
|
||||||
|
format!("{mounted}:/data"),
|
||||||
|
"tines-stack:latest".into(),
|
||||||
|
"tines".into(),
|
||||||
|
"/data/config.json".into(),
|
||||||
|
];
|
||||||
|
assert_eq!(argv, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stop_and_remove_args() {
|
||||||
|
let docker = Docker;
|
||||||
|
let id = ContainerId("abc123".into());
|
||||||
|
assert_eq!(docker.stop_argv(&id), vec!["docker", "stop", "abc123"]);
|
||||||
|
assert_eq!(docker.remove_argv(&id), vec!["docker", "rm", "abc123"]);
|
||||||
|
assert_eq!(
|
||||||
|
docker.logs_argv(&id, 25),
|
||||||
|
vec!["docker", "logs", "--tail", "25", "abc123"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_name_is_pinned() {
|
||||||
|
assert_eq!(Docker::image(), "tines-stack:latest");
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//! Fleet layout and node-config tests. The bus daemon is a fake `tines-busd`
|
||||||
|
//! seeded onto PATH (the real binary isn't on PATH for unit tests): it
|
||||||
|
//! records its argv and creates the socket file, so the fleet's spawn
|
||||||
|
//! protocol is what's under test — the real daemon is covered by tines-bus's
|
||||||
|
//! own suite and Task 4's e2e.
|
||||||
|
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use tines_gui::fleet::{Fleet, Mode};
|
||||||
|
|
||||||
|
/// One shared scratch setup for every test in this binary: a temp dir that
|
||||||
|
/// becomes both XDG_STATE_HOME (fleet dirs) and a PATH dir holding the fake
|
||||||
|
/// `tines-busd`. Env is process-global, so it is set exactly once before any
|
||||||
|
/// test touches fleet_dir.
|
||||||
|
fn setup() -> &'static PathBuf {
|
||||||
|
static DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
DIR.get_or_init(|| {
|
||||||
|
let dir = std::env::temp_dir().join(format!("tines-gui-fleet-test-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
|
||||||
|
// Fake tines-busd: touch the socket named by argv[1], then stay alive.
|
||||||
|
let fake = dir.join("tines-busd");
|
||||||
|
std::fs::write(&fake, "#!/bin/sh\ntouch \"$1\"\nsleep 60\n").unwrap();
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
|
||||||
|
let path = format!(
|
||||||
|
"{}:{}",
|
||||||
|
dir.display(),
|
||||||
|
std::env::var("PATH").unwrap_or_default()
|
||||||
|
);
|
||||||
|
// SAFETY: set once, before any fleet_dir call; this process is a
|
||||||
|
// test binary that exits afterwards, so nothing else observes it.
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("XDG_STATE_HOME", &dir);
|
||||||
|
std::env::set_var("PATH", path);
|
||||||
|
}
|
||||||
|
dir
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fleet_name(tag: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"test-{tag}-{}",
|
||||||
|
std::thread::current().name().unwrap_or("t")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fleet_dir_layout() {
|
||||||
|
setup();
|
||||||
|
let fleet = Fleet::create(&fleet_name("layout")).expect("fleet creates");
|
||||||
|
let dir = tines_gui::config::fleet_dir(&fleet_name("layout"));
|
||||||
|
let meta = std::fs::metadata(&dir).expect("fleet dir exists");
|
||||||
|
assert!(meta.is_dir());
|
||||||
|
assert_eq!(meta.permissions().mode() & 0o777, 0o700);
|
||||||
|
|
||||||
|
// The fake daemon creates the bus socket the fleet asked it to bind.
|
||||||
|
let socket = fleet.bus_socket();
|
||||||
|
assert_eq!(socket, dir.join("bus.sock"));
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
while !socket.exists() {
|
||||||
|
assert!(
|
||||||
|
Instant::now() < deadline,
|
||||||
|
"bus.sock never appeared: {}",
|
||||||
|
socket.display()
|
||||||
|
);
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_node_bus_config_mounts_data() {
|
||||||
|
setup();
|
||||||
|
let fleet_name = fleet_name("bus");
|
||||||
|
let fleet = Fleet::create(&fleet_name).expect("fleet creates");
|
||||||
|
let spec = fleet
|
||||||
|
.add_node("alpha", Some(&"11".repeat(16)), Mode::Sim, None)
|
||||||
|
.expect("node added");
|
||||||
|
|
||||||
|
let json = std::fs::read_to_string(spec.dir.join("config.json")).unwrap();
|
||||||
|
let parsed: tines::config::Config =
|
||||||
|
serde_json::from_str(&json).expect("config parses as the service config");
|
||||||
|
assert_eq!(parsed.transport, Some(tines::config::TransportKind::Bus));
|
||||||
|
assert_eq!(parsed.bus_socket, Some("/data/bus.sock".to_string()));
|
||||||
|
assert_eq!(parsed.socket_path, "/data/alpha/sock".to_string());
|
||||||
|
assert_eq!(parsed.log_path, "/data/alpha/log".to_string());
|
||||||
|
assert_eq!(parsed.node_id, "11".repeat(16));
|
||||||
|
assert_eq!(parsed.validate(), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_node_bt_config_has_peer() {
|
||||||
|
setup();
|
||||||
|
let fleet_name = fleet_name("bt");
|
||||||
|
let fleet = Fleet::create(&fleet_name).expect("fleet creates");
|
||||||
|
let peer = "22".repeat(16);
|
||||||
|
let spec = fleet
|
||||||
|
.add_node("bravo", None, Mode::Bt, Some(&peer))
|
||||||
|
.expect("node added");
|
||||||
|
|
||||||
|
let json = std::fs::read_to_string(spec.dir.join("config.json")).unwrap();
|
||||||
|
let parsed: tines::config::Config =
|
||||||
|
serde_json::from_str(&json).expect("config parses as the service config");
|
||||||
|
assert_eq!(parsed.transport, Some(tines::config::TransportKind::Bt));
|
||||||
|
assert_eq!(parsed.peer, Some(peer));
|
||||||
|
assert_eq!(parsed.bus_socket, None);
|
||||||
|
assert_eq!(parsed.validate(), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_id_roundtrips() {
|
||||||
|
setup();
|
||||||
|
let fleet_name = fleet_name("gen");
|
||||||
|
let fleet = Fleet::create(&fleet_name).expect("fleet creates");
|
||||||
|
let spec = fleet
|
||||||
|
.add_node("charlie", None, Mode::Sim, None)
|
||||||
|
.expect("node added");
|
||||||
|
|
||||||
|
let json = std::fs::read_to_string(spec.dir.join("config.json")).unwrap();
|
||||||
|
let parsed: tines::config::Config =
|
||||||
|
serde_json::from_str(&json).expect("config parses as the service config");
|
||||||
|
assert_eq!(parsed.node_id.len(), 32);
|
||||||
|
assert!(
|
||||||
|
parsed
|
||||||
|
.node_id
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
|
||||||
|
"generated id must be lowercase hex"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user