pub mod bark;
pub mod metrics;
pub use shep_client::dogs::DogRuntime;
use std::time::Duration;
use shep_client::dogs::DogIdentity;
use shep_core::paths::ShepPaths;
use crate::exit::ExitCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BuiltInDog {
Metrics,
Bark,
}
impl BuiltInDog {
const ALL: [Self; 2] = {
match Self::Metrics {
Self::Metrics | Self::Bark => {}
}
[Self::Metrics, Self::Bark]
};
const fn name(self) -> &'static str {
match self {
Self::Metrics => "metrics",
Self::Bark => "bark",
}
}
fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|dog| dog.name() == name)
}
}
pub(crate) const BUILT_IN_DOGS: [&str; BuiltInDog::ALL.len()] = {
let mut names = [""; BuiltInDog::ALL.len()];
let mut i = 0;
while i < names.len() {
names[i] = BuiltInDog::ALL[i].name();
i += 1;
}
names
};
const SHEPHERD_RETURN_BUDGET: Duration = shep_daemon::dogs::DOG_SILENCE_BUDGET;
pub(crate) fn builtin_schema(name: &str) -> Option<serde_json::Value> {
use shep_client::dogs::config_schema;
let schema = match BuiltInDog::from_name(name)? {
BuiltInDog::Metrics => config_schema::<metrics::MetricsConfig>(),
BuiltInDog::Bark => config_schema::<bark::BarkConfig>(),
};
serde_json::to_value(schema).ok()
}
pub async fn run_dog(name: &str, paths: ShepPaths) -> ExitCode {
let Some(dog) = BuiltInDog::from_name(name) else {
let known: Vec<String> = BUILT_IN_DOGS
.iter()
.map(|known| format!("{known:?}"))
.collect();
eprintln!(
"shep dog: unknown dog {name:?}; the built-in dogs are {}",
known.join(", ")
);
return ExitCode::Usage;
};
let runtime = match DogRuntime::start(DogIdentity::named(name), paths).await {
Ok(runtime) => runtime,
Err(err) => {
eprintln!("shep dog {name}: {err}");
return ExitCode::from(&err);
}
};
match dog {
BuiltInDog::Metrics => metrics::run(runtime).await,
BuiltInDog::Bark => bark::run(runtime).await,
}
}
#[cfg(test)]
mod builtin_schema_tests {
use super::*;
#[test]
fn both_built_ins_answer_and_a_stranger_does_not() {
assert!(builtin_schema("metrics").is_some());
let bark = builtin_schema("bark").expect("bark is a built-in");
assert_eq!(
bark["properties"]["sinks"][shep_core::dogs::SECRET_KEY],
true
);
assert!(builtin_schema("otel").is_none());
}
#[test]
fn every_built_in_is_listed_by_name_and_parses_back() {
assert_eq!(BUILT_IN_DOGS, BuiltInDog::ALL.map(BuiltInDog::name));
for dog in BuiltInDog::ALL {
assert_eq!(BuiltInDog::from_name(dog.name()), Some(dog));
}
assert_eq!(BuiltInDog::from_name("otel"), None);
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::time::Duration;
use shep_client::testing::{sample_ack, serve_one_request};
use shep_core::protocol::{Request, Response};
use super::*;
#[test]
fn the_budget_is_the_five_seconds_the_docs_promise() {
assert_eq!(
SHEPHERD_RETURN_BUDGET,
Duration::from_secs(5),
"docs/dogs.md, the web dogs page and three doc comments all say five \
seconds; change them together or not at all"
);
}
pub(in crate::dog) fn test_paths(dir: &Path, socket: PathBuf) -> ShepPaths {
let home = dir.to_path_buf();
ShepPaths {
daemon_config: home.join("shep.toml"),
dogs_config: home.join("dogs.toml"),
snapshot: home.join("flock.json"),
logs: home.join("logs"),
pids: home.join("pids"),
run: home.join("run"),
socket,
barks: home.join("barks.jsonl"),
kv: home.join("kv.json"),
overrides: home.join("overrides.json"),
secrets: home.join("secrets.json"),
secrets_cache: home.join("secrets-cache.json"),
home,
}
}
#[tokio::test]
async fn an_unknown_dog_name_is_usage_without_touching_the_socket() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(
dir.path(),
shep_client::testing::control_address(dir.path()),
);
let code = run_dog("otel", paths).await;
assert_eq!(code, ExitCode::Usage);
}
#[tokio::test]
async fn run_dog_reaches_bark() {
let dir = tempfile::tempdir().unwrap();
let socket = shep_client::testing::control_address(dir.path());
let response = Response::DogSection {
toml: String::new().into(),
};
let handle = serve_one_request(&socket, sample_ack(), response).await;
let paths = test_paths(dir.path(), socket);
let task = tokio::spawn(run_dog("bark", paths));
let envelope = tokio::time::timeout(Duration::from_secs(5), handle)
.await
.expect("run_dog must reach the wire")
.unwrap();
assert_eq!(
envelope.body,
Request::DogConfigResolved {
name: "bark".to_string()
}
);
task.abort();
}
#[tokio::test]
async fn run_dog_reaches_metrics() {
let dir = tempfile::tempdir().unwrap();
let socket = shep_client::testing::control_address(dir.path());
let response = Response::DogSection {
toml: "bind = \"127.0.0.1:0\"\n".to_string().into(),
};
let handle = serve_one_request(&socket, sample_ack(), response).await;
let paths = test_paths(dir.path(), socket);
let task = tokio::spawn(run_dog("metrics", paths));
let envelope = tokio::time::timeout(Duration::from_secs(5), handle)
.await
.expect("run_dog must reach the wire")
.unwrap();
assert_eq!(
envelope.body,
Request::DogConfigResolved {
name: "metrics".to_string()
}
);
task.abort();
}
#[tokio::test]
async fn run_dog_reports_daemon_unreachable_with_no_shepherd_running() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(
dir.path(),
shep_client::testing::control_address(dir.path()),
);
let code = run_dog("metrics", paths).await;
assert_eq!(code, ExitCode::DaemonUnreachable);
}
}