Files
radicle-native-ci/src/engine.rs
T
2024-01-16 19:36:55 +02:00

244 lines
8.0 KiB
Rust

use std::path::PathBuf;
use uuid::Uuid;
use radicle::prelude::Profile;
use radicle_ci_broker::msg::{Id, Oid, Request, RunId, RunResult};
use crate::{
config::{Config, ConfigError},
logfile::{AdminLog, LogError},
msg::{
read_request, write_errored, write_failed, write_succeeded, write_triggered,
NativeMessageError,
},
report,
run::{Run, RunError},
runinfo::{RunInfo, RunInfoBuilder, RunInfoError},
runlog::RunLogError,
runspec::RunSpecError,
};
/// Run CI for a project once.
#[derive(Debug)]
pub struct Engine {
config: Config,
adminlog: AdminLog,
run_info_builder: RunInfoBuilder,
}
impl Engine {
/// Create a new engine and prepare for actually running CI. This
/// may fail, for various reasons, such as the configuration file
/// not existing. The caller should handle the error in some
/// suitable way, such as report that to its stderr, and exit
/// non-zero.
pub fn new() -> Result<Self, EngineError> {
// Get config, open admin log for writing. If either of these
// fails, we can't write about the problem to the admin log,
// but we can return an error to the caller.
let config = Config::load_via_env()?;
let adminlog = config.open_log()?;
// From here on, it's plain sailing.
Ok(Self {
config,
adminlog,
run_info_builder: RunInfo::builder(),
})
}
/// Set up and run CI on a project once: read the trigger request
/// from stdin, write responses to stdout. Update node admin log
/// with any problems that aren't inherent in the git repository
/// (those go into the run log).
pub fn run(&mut self) -> Result<bool, EngineError> {
let req = match self.setup() {
Ok(req) => req,
Err(e) => {
self.adminlog.writeln(&format!("Error setting up: {}", e))?;
return Err(e);
}
};
// Check that we got the right kind of request.
let mut success = false;
match req {
Request::Trigger { repo, commit } => {
match self.run_helper(repo, commit) {
Ok(true) => success = true,
Ok(false) => (),
Err(e) => {
// If the run helper return an error, something
// went wrong in that is not due to the repository
// under test. So we don't put it in the run log,
// but the admin log and return it to the caller.
self.adminlog.writeln(&format!("Error running CI: {}", e))?;
return Err(e);
}
}
}
_ => {
// Protocol error. Log in admin log and report to caller.
self.adminlog
.writeln("First request was not a message to trigger a run.")?;
return Err(EngineError::NotTrigger(req));
}
}
if let Err(e) = self.finish() {
self.adminlog
.writeln(&format!("Error finishing up: {}", e))?;
return Err(e);
}
Ok(success)
}
// Set up CI to run. If something goes wrong, return the error,
// and assume the caller logs it to the admin log.
fn setup(&mut self) -> Result<Request, EngineError> {
// Write something to the admin log to indicate we start.
self.adminlog.writeln("Native CI run starts")?;
// Read request and log it.
let req = read_request()?;
self.adminlog.writeln(&format!("request: {:#?}", req))?;
Ok(req)
}
// Finish up after a CI run. If something goes wrong, return the
// error, and assume the caller logs it to the admin log.
fn finish(&mut self) -> Result<(), EngineError> {
// Write response message indicating the run has finished.
let ri = self.run_info_builder.build()?;
match &ri.result {
RunResult::Success => write_succeeded()?,
RunResult::Failure => write_failed()?,
RunResult::Error(s) => write_errored(s)?,
_ => write_errored(&format!("unknown result {}", ri.result))?,
}
// Persist the run info. If this fails, it's a node admin problem.
if let Err(e) = ri.write() {
self.adminlog
.writeln(&format!("failed to write run info: {}", e))?;
}
// Generate a report page of all CI runs.
report::build_report(&self.config.state)?;
// Log that we've reached the end successfully.
self.adminlog.writeln("Native CI ends successfully")?;
Ok(())
}
// Execute the CI run. Log any problems to a log for this run, and
// persist that. Update the run info builder as needed.
fn run_helper(&mut self, rid: Id, commit: Oid) -> Result<bool, EngineError> {
// Pick a run id and create a directory for files related to
// the run.
let (run_id, run_dir) = mkdir_run(&self.config)?;
let run_id = RunId::from(format!("{}", run_id).as_str());
// Set the file where the run info should be written, now that
// we have the run directory.
self.run_info_builder.run_info(run_dir.join("run.yaml"));
// Get node git storage. We do this before responding to the
// trigger, so that if this fails, we haven't said that a run
// has started.
let profile = Profile::load().map_err(EngineError::LoadProfile)?;
let storage = profile.storage.path();
// Write response to indicate run has been triggered.
write_triggered(&run_id)?;
// Create and set up the run.
let mut run = Run::new(&run_dir, run_id)?;
run.set_repository(rid);
run.set_commit(commit);
run.set_storage(storage);
run.set_timeout(self.config.timeout());
// Actually run. Examine the run log to decide if the run
// succeeded or failed.
let run_log = run.run()?;
let result = if run_log.all_commands_succeeded() {
RunResult::Success
} else {
RunResult::Failure
};
self.run_info_builder.result(result);
Ok(run_log.all_commands_succeeded())
}
/// Report results to caller (via stdout) and to users (via report
/// on web page).
pub fn report(&mut self) -> Result<(), EngineError> {
Ok(())
}
}
/// Create a per-run directory.
fn mkdir_run(config: &Config) -> Result<(Uuid, PathBuf), EngineError> {
let state = &config.state;
if !state.exists() {
std::fs::create_dir_all(state).map_err(|e| EngineError::CreateState(state.into(), e))?;
}
let run_id = Uuid::new_v4();
let run_dir = state.join(run_id.to_string());
std::fs::create_dir(&run_dir).map_err(|e| EngineError::CreateRunDir(run_dir.clone(), e))?;
Ok((run_id, run_dir))
}
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
#[error("failed to create per-run parent directory {0}")]
CreateState(PathBuf, #[source] std::io::Error),
#[error("failed to create per-run directory {0}")]
CreateRunDir(PathBuf, #[source] std::io::Error),
#[error("failed to load Radicle profile")]
LoadProfile(#[source] radicle::profile::Error),
#[error("failed to remove {0}")]
RemoveDir(PathBuf, #[source] std::io::Error),
#[error("programming error: failed to set field {0}")]
Unset(&'static str),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Log(#[from] LogError),
#[error(transparent)]
Message(#[from] NativeMessageError),
#[error(transparent)]
Report(#[from] report::ReportError),
#[error(transparent)]
RunInfo(#[from] RunInfoError),
#[error(transparent)]
RunLog(#[from] RunLogError),
#[error(transparent)]
RunSpec(#[from] RunSpecError),
#[error(transparent)]
Run(#[from] RunError),
#[error("request message was not a trigger message: {0:?}")]
NotTrigger(Request),
}