use crate::git::get_user_info;
use crate::{CommandExt, Error, Project, Recipe, Stage, Template};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs::{create_dir_all, read_dir, File};
use std::hash::{Hash, Hasher};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use tracing::{debug, info, instrument};
pub const SERVICE_FOLDER: &str = "srv";
pub const SERVICE_CFG_FILE: &str = "orcs.toml";
pub const SERVICE_CFG_TEMPLATE: &str = "maintainers = [
\"{{{ user_info }}}\"
]
recipes = []
##################
# Stages #
##################
# BUILD STAGE
[stages.build]
depends_on = []
# Actions
actions = [
\"echo Hello from $ORCS_SERVICE\"
]
# Check
check = \"false\"
# DEPLOY STAGE
[stages.deploy]
depends_on = []
# Actions
actions = []
# Check
check = \"false\"
";
#[derive(Clone, Serialize, Deserialize)]
pub struct Service {
#[serde(skip)]
project: Arc<Project>,
#[serde(skip)]
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
maintainers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
homepage: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
recipes: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
stages: HashMap<String, Stage>,
}
impl Service {
#[instrument(skip(project))]
pub fn create(project: Arc<Project>, name: &str, template: Option<&str>) -> Result<(), Error> {
info!("Create service {}", name);
if name.contains("..") {
return Err(Error::CreateServiceError(
"illegal sequence in service name",
name.to_string(),
));
}
let path = get_service_path(&project.canonical_path(), name);
debug!("Create service folder {:?}", path);
create_dir_all(&path)?;
let user_info = get_user_info()?;
let template = match template {
Some(template) => Template::from_string(template),
None => {
let mut template_map = HashMap::new();
template_map.insert("orcs.toml".to_string(), SERVICE_CFG_TEMPLATE.to_string());
Template::from_map(template_map)
}
};
let mut template_data = HashMap::new();
template_data.insert("service_name", name);
template_data.insert("user_info", &user_info);
template.render(path.as_path(), &template_data)?;
Ok(())
}
#[instrument(skip(project))]
pub fn from_name(project: Arc<Project>, name: &str) -> Result<Arc<Self>, Error> {
{
if let Some(service) = project.services.read().unwrap().get(name) {
return Ok(service.clone());
}
}
info!("Load service {}", name);
let path = get_service_path(&project.canonical_path(), name);
debug!("Load service configuration file");
let file_path = path.join(SERVICE_CFG_FILE);
let mut file = File::open(file_path)?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let service = Arc::new(Self::from_toml(project.clone(), name, &data)?);
project
.services
.write()
.unwrap()
.insert(name.to_string(), service.clone());
Ok(service)
}
fn from_toml(project: Arc<Project>, name: &str, data: &str) -> Result<Self, Error> {
let mut service = toml::from_str::<Self>(data)?;
service.project = project;
service.name = name.to_string();
service.with_recipes()
}
fn with_recipes(&self) -> Result<Self, Error> {
debug!("Parse recipes for service {}", self.name);
let mut service = self.clone();
for recipe_name in &service.recipes {
let recipe = Recipe::from_name(service.project.clone(), &recipe_name)?;
for (stage_name, recipe_stage) in recipe.stages.clone() {
match service.stages.get_mut(&stage_name) {
Some(service_stage) => {
service_stage.use_recipe(recipe_stage)
}
None => {
service
.stages
.insert(stage_name.clone(), recipe_stage.clone());
}
}
}
}
Ok(service)
}
pub fn path(&self) -> PathBuf {
get_service_path(&self.project.canonical_path(), &self.name)
}
pub fn depends_on(&self, stage_name: &str) -> Vec<String> {
let stage = match self.stages.get(stage_name) {
Some(stage) => stage,
None => return Default::default(),
};
stage.depends_on.clone()
}
#[instrument(skip(self))]
pub fn check_deps(&self, stage_name: &str) -> Result<bool, Error> {
debug!(
"Check dependencies for service {} in stage {}",
self.name, stage_name
);
let stage = match self.stages.get(stage_name) {
Some(stage) => stage,
None => return Ok(true),
};
for dep_name in &stage.depends_on {
debug!("Check {}", dep_name);
let dep = Service::from_name(self.project.clone(), dep_name)?;
if !dep.run_check(stage_name)? {
return Ok(false);
}
}
Ok(true)
}
#[instrument(skip(self))]
pub fn check_dep_stages(&self, stage_name: &str) -> Result<bool, Error> {
debug!(
"Check stage dependencies for service {} in stage {}",
self.name, stage_name
);
let stage = match self.project.stages.get(stage_name) {
Some(stage) => stage,
None => return Ok(true),
};
for dep_stage in &stage.depends_on {
println!("dep_stage: {}", dep_stage);
if !self.run_check(dep_stage)? {
return Err(Error::RunStageError(
"needs to run another stage before this one",
stage_name.to_string(),
));
}
}
Ok(true)
}
#[instrument(skip(self))]
pub fn run_check(&self, stage_name: &str) -> Result<bool, Error> {
info!(
"Running check for service {} in stage {}",
self.name, stage_name
);
let stage = match self.stages.get(stage_name) {
Some(stage) => stage,
None => return Ok(true),
};
let result = Command::new("bash")
.current_dir(self.path())
.with_context(&self.project, &self, stage_name)
.arg("-e")
.arg("-c")
.arg(format!("{}", stage.check))
.status()?
.success();
if result {
debug!(
"Service {} in stage {} is up-to-date",
self.name, stage_name
);
} else {
debug!(
"Service {} in stage {} is not up-to-date",
self.name, stage_name
);
}
Ok(result)
}
#[instrument(skip(self))]
pub fn run(&self, stage_name: &str) -> Result<(), Error> {
info!(
"Start running service {} in stage {}",
self.name, stage_name
);
let stage = match self.stages.get(stage_name) {
Some(stage) => stage,
None => return Ok(()),
};
debug!("Running commands:\n{}", stage.actions);
match Command::new("bash")
.current_dir(self.path())
.with_context(&self.project, &self, stage_name)
.arg("-e")
.arg("-c")
.arg(format!("{}", stage.actions))
.status()?
.success()
{
true => info!("Done running service {} in stage {}", self.name, stage_name),
false => {
return Err(Error::RunStageError(
"Failed to run stage",
stage_name.to_string(),
))
}
}
Ok(())
}
}
impl Eq for Service {}
impl Hash for Service {
fn hash<H: Hasher>(&self, state: &mut H) {
self.project.path.hash(state);
self.name.hash(state);
}
}
impl PartialEq for Service {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.project == other.project
}
}
impl fmt::Debug for Service {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Service")
.field("name", &self.name)
.field("description", &self.description)
.field("maintainers", &self.maintainers)
.field("homepage", &self.homepage)
.field("recipes", &self.recipes)
.field("stages", &self.stages)
.finish()
}
}
#[instrument(skip(project))]
pub fn get_all_services(project: Arc<Project>) -> Result<HashMap<String, Arc<Service>>, Error> {
debug!("Load all services for project {}", (*project).path);
let path = Path::new(&project.path).join(SERVICE_FOLDER);
if !path.is_dir() {
return Ok(HashMap::new());
}
visit_dir(project, &path)
}
fn visit_dir(project: Arc<Project>, dir: &Path) -> Result<HashMap<String, Arc<Service>>, Error> {
let mut services = HashMap::<String, Arc<Service>>::new();
for entry in read_dir(dir)? {
let path = entry?.path();
if !path.is_dir() {
continue;
}
if !path.join(SERVICE_CFG_FILE).is_file() {
services.extend(visit_dir(project.clone(), &path)?);
} else {
let name = get_name_from_path(project.clone(), &path);
services.insert(name.clone(), Service::from_name(project.clone(), &name)?);
}
}
Ok(services)
}
fn get_name_from_path(project: Arc<Project>, path: &Path) -> String {
let root = Path::new(&project.path).join(SERVICE_FOLDER);
path.strip_prefix(root)
.unwrap()
.to_str()
.unwrap()
.to_string()
}
fn get_service_path<'a>(project_path: &'a Path, name: &str) -> PathBuf {
project_path.join(SERVICE_FOLDER).join(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_name_from_path() {
let mut project = Project::default();
project.path = "project".to_string();
let path = Path::new(&project.path)
.join(SERVICE_FOLDER)
.join("a")
.join("b")
.join("c");
let name = get_name_from_path(Arc::new(project), path.as_path());
assert_eq!(&name, if cfg!(windows) { "a\\b\\c" } else { "a/b/c" });
}
#[test]
fn test_get_service_path() {
let project_path = Path::new("project");
let name = if cfg!(windows) { "a\\b\\c" } else { "a/b/c" };
let path = get_service_path(&project_path, name);
assert_eq!(
path,
Path::new(project_path).join(SERVICE_FOLDER).join(name)
)
}
}