use std::cell::{Cell, RefCell};
use std::fs;
use std::path::{Path, PathBuf};
use shep_client::RequestError;
use shep_client::shep_core::config::AppConfig;
use shep_client::shep_core::protocol::{ProcessInfo, RpcError, RpcErrorCode};
use shep_client::shep_core::status::ProcStatus;
use crate::daemon::Daemon;
use crate::error::Error;
use crate::fixtures;
use crate::paths::Tree;
use crate::state::State;
pub(super) fn write_record(home: &Path, sheep: &str, state: &State) {
let tree = Tree::for_sheep(home, sheep);
fs::create_dir_all(tree.state_file().parent().expect("has a parent"))
.expect("create target dir");
state.write(&tree.state_file()).expect("write state");
}
pub(super) fn write_target_with_origin(
home: &Path,
sheep: &str,
origin_cwd: &str,
origin_script: &str,
) {
let state = State {
deployed: Some(fixtures::OTHER_SHA.to_owned()),
origin_cwd: Some(PathBuf::from(origin_cwd)),
origin_script: Some(origin_script.to_owned()),
checkout: PathBuf::from(origin_cwd),
..fixtures::state()
};
write_record(home, sheep, &state);
}
pub(super) fn write_target_with_full_origin(home: &Path, sheep: &str, origin: &AppConfig) {
let cwd = origin.cwd.clone().expect("an origin with a cwd");
let state = State {
deployed: Some(fixtures::OTHER_SHA.to_owned()),
origin_cwd: Some(PathBuf::from(&cwd)),
origin_script: Some(origin.script.clone()),
checkout: PathBuf::from(&cwd),
origin: Some(origin.clone()),
..fixtures::state()
};
write_record(home, sheep, &state);
}
pub(super) fn write_target_with_origin_absent(home: &Path, sheep: &str) {
let state = State {
deployed: Some(fixtures::OTHER_SHA.to_owned()),
checkout: PathBuf::from("/srv/deploy-tree"),
..fixtures::state()
};
write_record(home, sheep, &state);
}
pub(super) enum Refuse {
Never,
FirstOnly,
Always,
}
pub(super) struct Recording {
sheep: Vec<&'static str>,
refuse: Refuse,
instances: u32,
describe_fails: bool,
delete_fails_at: Option<u32>,
unreadable_roll: bool,
registered_cwd: Option<String>,
calls: RefCell<Vec<&'static str>>,
starts: RefCell<Vec<AppConfig>>,
attempts: Cell<usize>,
delete_attempts: Cell<u32>,
roll_dir: tempfile::TempDir,
}
impl Recording {
pub(super) fn new(sheep: &[&'static str], refuse: Refuse) -> Self {
Self {
sheep: sheep.to_vec(),
refuse,
instances: 1,
describe_fails: false,
delete_fails_at: None,
unreadable_roll: false,
registered_cwd: None,
calls: RefCell::new(Vec::new()),
starts: RefCell::new(Vec::new()),
attempts: Cell::new(0),
delete_attempts: Cell::new(0),
roll_dir: tempfile::tempdir().expect("tempdir"),
}
}
pub(super) fn with_registered(sheep: &[&'static str]) -> Self {
Self::new(sheep, Refuse::Never)
}
pub(super) fn registered_at(mut self, cwd: &std::path::Path) -> Self {
self.registered_cwd = Some(cwd.display().to_string());
self
}
pub(super) fn refusing_first_start_only(sheep: &[&'static str]) -> Self {
Self::new(sheep, Refuse::FirstOnly)
}
pub(super) fn refusing_every_start(sheep: &[&'static str]) -> Self {
Self::new(sheep, Refuse::Always)
}
pub(super) fn with_unreadable_roll() -> Self {
let mut this = Self::new(&[], Refuse::Never);
this.unreadable_roll = true;
this
}
pub(super) fn refusing_describe(sheep: &[&'static str]) -> Self {
let mut this = Self::new(sheep, Refuse::Never);
this.describe_fails = true;
this
}
pub(super) fn refusing_first_delete_only(sheep: &[&'static str]) -> Self {
let mut this = Self::new(sheep, Refuse::Never);
this.delete_fails_at = Some(1);
this
}
pub(super) fn refusing_delete_partway(sheep: &[&'static str]) -> Self {
let mut this = Self::new(sheep, Refuse::Never);
this.instances = 2;
this.delete_fails_at = Some(2);
this
}
pub(super) fn calls(&self) -> Vec<&'static str> {
self.calls.borrow().clone()
}
pub(super) fn started(&self) -> Vec<AppConfig> {
self.starts.borrow().clone()
}
}
impl Daemon for Recording {
async fn describe(&self, sheep: &str) -> Result<Vec<ProcessInfo>, Error> {
if self.describe_fails {
return Err(Error::Request(RequestError::Rpc(RpcError {
code: RpcErrorCode::Internal,
message: "describe refused".to_owned(),
daemon_version: None,
})));
}
Ok((0..self.instances)
.map(|offset| {
ProcessInfo::builder(offset + 1, sheep, ProcStatus::Online)
.pid(Some(1000 + offset))
.build()
})
.collect())
}
async fn start(&self, apps: Vec<AppConfig>) -> Result<Vec<u32>, Error> {
let attempt = self.attempts.get();
self.attempts.set(attempt + 1);
let refused = match self.refuse {
Refuse::Never => false,
Refuse::FirstOnly => attempt == 0,
Refuse::Always => true,
};
self.starts.borrow_mut().extend(apps);
if refused {
return Err(Error::Request(RequestError::Rpc(RpcError {
code: RpcErrorCode::Internal,
message: "refused".to_owned(),
daemon_version: None,
})));
}
self.calls.borrow_mut().push("start");
Ok(Vec::new())
}
async fn delete(&self, _id: u32) -> Result<(), Error> {
let attempt = self.delete_attempts.get() + 1;
self.delete_attempts.set(attempt);
if self.delete_fails_at == Some(attempt) {
return Err(Error::Request(RequestError::Rpc(RpcError {
code: RpcErrorCode::Internal,
message: "delete refused".to_owned(),
daemon_version: None,
})));
}
self.calls.borrow_mut().push("delete");
Ok(())
}
async fn save_roll(&self) -> Result<PathBuf, Error> {
let path = self.roll_dir.path().join("flock.json");
if self.unreadable_roll {
fs::write(&path, "{\"apps\":[{\"app\":{\"name\":123}}]}").expect("write roll");
return Ok(path);
}
let apps: Vec<String> = self
.sheep
.iter()
.map(|name| {
let cwd = self
.registered_cwd
.clone()
.unwrap_or_else(|| "/srv/deploy-tree/current".to_owned());
format!(
"{{\"app\":{{\"name\":{name:?},\"script\":\"the-shepherds-own-script\",\
\"cwd\":{cwd:?}}}}}"
)
})
.collect();
fs::write(&path, format!("{{\"apps\":[{}]}}", apps.join(","))).expect("write roll");
Ok(path)
}
crate::fixtures::daemon_methods!(unimplemented;
dog_config, list_flock, reload, restart, set_smit,
);
}
pub(super) fn write_target_never_cut_over(
home: &Path,
sheep: &str,
origin_cwd: &str,
script: &str,
) {
let state = State {
origin_cwd: Some(PathBuf::from(origin_cwd)),
origin_script: Some(script.to_owned()),
checkout: PathBuf::from(origin_cwd),
..fixtures::state()
};
write_record(home, sheep, &state);
}