use colored::*;
use serde_yaml::Value;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs;
use std::io::Write;
use std::process::exit;
#[derive(Debug)]
struct Pipeline {
pipeline_type: PipelineType,
stages: Vec<Stage>,
jobs: Vec<Job>,
script: Script,
variables: HashMap<String, String>,
}
impl Pipeline {
fn new(pipeline_file: &str) -> Pipeline {
let pipeline_data = get_pipeline_data(pipeline_file);
match get_pipeline_type(pipeline_data.clone()) {
PipelineType::Script => {
let script_data = get_script(pipeline_data.clone()).unwrap();
let new_pipeline = Pipeline {
pipeline_type: PipelineType::Script,
stages: vec![], jobs: vec![], script: Script::new(script_data), variables: get_variables(pipeline_data),
};
return new_pipeline;
}
PipelineType::Jobs => {
let mut pipeline_jobs: Vec<Job> = vec![];
let pipeline_variables = get_variables(pipeline_data.clone());
let jobs_data = get_jobs(pipeline_data.clone()).unwrap();
let mut a = 0;
loop {
if jobs_data.get(a).is_some() {
let job_name = jobs_data.get(a).unwrap().as_str().unwrap().to_owned();
let job_script = if get_job_or_stage(
pipeline_data.clone(),
job_name.clone(),
)
.is_some()
{
get_job_or_stage(pipeline_data.clone(), job_name.clone()).unwrap()
} else {
break;
};
pipeline_jobs.push(Job::new(
job_name.clone(),
job_script.clone(),
pipeline_variables.clone(),
));
} else {
break;
}
a += 1;
}
let new_pipeline = Pipeline {
pipeline_type: PipelineType::Jobs,
stages: vec![], jobs: pipeline_jobs,
script: Script { data: vec![] }, variables: pipeline_variables,
};
return new_pipeline;
}
PipelineType::Stages => {
let mut pipeline_stages: Vec<Stage> = vec![];
let pipeline_variables = get_variables(pipeline_data.clone());
let stages_data = get_stages(pipeline_data.clone()).unwrap();
let mut a = 0;
loop {
if stages_data.get(a).is_some() {
let stage_name = stages_data.get(a).unwrap().as_str().unwrap().to_owned();
pipeline_stages.push(Stage::new(
stage_name,
pipeline_data.clone(),
pipeline_variables.clone(),
));
} else {
break;
}
a += 1;
}
let new_pipeline = Pipeline {
pipeline_type: PipelineType::Stages,
stages: pipeline_stages,
jobs: vec![], script: Script { data: vec![] }, variables: pipeline_variables,
};
return new_pipeline;
}
PipelineType::Null => panic!("Error in pipeline format!"),
}
}
fn exec_pipeline(&self) -> bool {
let timestamp: String = create_artifact_dir();
let stage_begin_spacer = "###############";
let stage_end_spacer = "####################################";
match self.pipeline_type {
PipelineType::Script => {
println!(
"{} {} {}",
stage_begin_spacer.green(),
"main".blue(),
stage_begin_spacer.green()
);
let success = self
.script
.exec(self.variables.clone(), timestamp.as_str(), "main");
println!("{}", stage_end_spacer.green());
return success;
}
PipelineType::Jobs => {
let mut success = true;
println!(
"{} {} {}",
stage_begin_spacer.green(),
"main".blue(),
stage_begin_spacer.green()
);
for a in 0..self.jobs.len() {
let job_success = self.jobs[a].exec(timestamp.as_str());
if !job_success {
success = false
}
}
println!("{}", stage_end_spacer.green());
return success;
}
PipelineType::Stages => {
for a in 0..self.stages.len() {
println!(
"{} {} {}",
stage_begin_spacer.green(),
self.stages[a].get_name().to_string().blue(),
stage_begin_spacer.green()
);
let stage_success = self.stages[a].exec(timestamp.as_str());
println!("{}", "###############################\n".green());
if !stage_success {
return false;
}
}
return true;
}
PipelineType::Null => panic!("Error in pipeline format!"),
}
}
}
#[derive(Debug)]
struct Script {
data: Vec<String>,
}
impl Script {
fn new(script_data: Value) -> Script {
let mut a = 0;
let mut commands: Vec<String> = vec![];
loop {
let next_command = get_command(script_data.clone(), a);
if next_command.is_some() {
commands.push(next_command.unwrap());
} else {
break;
}
a += 1;
}
let new_script = Script { data: commands };
return new_script;
}
fn exec(&self, variables: HashMap<String, String>, timestamp: &str, job_name: &str) -> bool {
print!("Job: \"{}\"{} ", job_name.blue(), "...".blink());
std::io::stdout().flush().unwrap(); let mut script_string = "".to_owned(); script_string += "set -ebuxo pipefail\n"; script_string += "exec 2>&1\n";
for (key, value) in variables {
script_string += key.as_str();
script_string += "=";
script_string += value.as_str();
script_string += "\n";
}
for command in self.data.iter() {
script_string += "\n";
script_string += command;
}
script_string += "\n";
let script_file: String =
".simple-ci/run_".to_string() + timestamp + "/" + job_name + ".sh";
write_to_file("####\n", script_file.as_str());
write_to_file(script_string.as_str(), script_file.as_str());
let res = exec_command(script_string);
if res.0 == true {
let log_file: String =
".simple-ci/run_".to_string() + timestamp + "/" + job_name + ".log";
write_to_file("####\n", log_file.as_str());
write_to_file(res.1.as_str(), log_file.as_str());
println!("{}", "OK".blink().on_green());
return true;
} else {
let error_log_file: String =
".simple-ci/run_".to_string() + timestamp + "/" + job_name + "_ERROR.log";
write_to_file("####\n", error_log_file.as_str());
write_to_file(res.2.as_str(), error_log_file.as_str());
println!("{}", "Error".blink().on_red());
return false;
}
}
}
#[derive(Debug)]
struct Job {
name: String,
scripts: Script,
variables: HashMap<String, String>,
}
impl Job {
fn new(job_name: String, job_data: Value, herited_variables: HashMap<String, String>) -> Job {
let mut all_vars = herited_variables;
for (key, value) in get_variables(job_data.clone()).iter() {
all_vars.insert(key.to_owned(), value.to_owned());
}
return Job {
name: job_name,
scripts: Script::new(job_data.get("script").unwrap().to_owned()),
variables: all_vars,
};
}
fn exec(&self, timestamp: &str) -> bool {
let success = self
.scripts
.exec(self.variables.clone(), timestamp, self.name.as_str());
return success;
}
}
#[derive(Debug)]
struct Stage {
name: String,
jobs: Vec<Job>,
}
impl Stage {
fn new(
stage_name: String,
pipeline_data: Value,
herited_variables: HashMap<String, String>,
) -> Stage {
let mut stage_jobs: Vec<Job> = vec![];
let stage_data = if get_job_or_stage(pipeline_data.clone(), stage_name.clone()).is_some() {
get_job_or_stage(pipeline_data.clone(), stage_name.clone()).unwrap()
} else {
panic!(
"The stage '{}' doesn't exists in pipeline",
stage_name.as_str().red()
);
};
let mut all_vars = herited_variables;
for (key, value) in get_variables(stage_data.clone()).iter() {
all_vars.insert(key.to_owned(), value.to_owned());
}
let jobs_data = if get_jobs(stage_data.clone()).is_some() {
get_jobs(stage_data.clone()).unwrap()
} else {
panic!("Jobs not found for stage '{}", stage_name.as_str().red())
};
let mut a = 0;
loop {
if jobs_data.get(a).is_some() {
let job_name = jobs_data.get(a).unwrap().as_str().unwrap().to_owned();
let job_script =
if get_job_or_stage(pipeline_data.clone(), job_name.clone()).is_some() {
get_job_or_stage(pipeline_data.clone(), job_name.clone()).unwrap()
} else {
a += 1;
continue;
};
stage_jobs.push(Job::new(
job_name.clone(),
job_script.clone(),
all_vars.clone(), ));
} else {
break;
}
a += 1;
}
return Stage {
name: stage_name,
jobs: stage_jobs,
};
}
fn get_name(&self) -> String {
return self.name.clone();
}
fn exec(&self, timestamp: &str) -> bool {
let mut success = true;
for job in self.jobs.iter() {
let job_success = job.exec(timestamp);
if !job_success {
success = false;
}
}
return success;
}
}
fn get_variables<'t>(data: Value) -> HashMap<String, String> {
let variables = data.get("variables");
if variables.is_some() {
if variables.unwrap().is_mapping() {
let mut map = HashMap::new();
for var in variables.unwrap().to_owned().as_mapping().unwrap().iter() {
let key = var.0.as_str().unwrap().to_owned();
let value = var.1.as_str().unwrap().to_owned();
map.insert(key, value);
}
return map;
} else {
println!(
"{}",
"Variables data is incorrect! (not a dictionnay)".red()
);
exit(1)
}
} else {
return HashMap::new();
}
}
fn get_script<'t>(data: Value) -> Option<Value> {
let script = data.get("script");
match script {
Some(_) => return Option::Some(script.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_command<'t>(script: Value, i: usize) -> Option<std::string::String> {
let command = script.get(i);
if command.is_some() {
return Option::Some(command.unwrap().as_str().unwrap().to_owned());
} else {
return Option::None;
}
}
fn get_jobs<'t>(data: Value) -> Option<Value> {
let jobs = data.get("jobs");
match jobs {
Some(_) => return Option::Some(jobs.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_stages<'t>(pipeline: Value) -> Option<Value> {
let stages = pipeline.get("stages");
match stages {
Some(_) => return Option::Some(stages.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_job_or_stage<'t>(pipeline: Value, job_or_stage_name: String) -> Option<Value> {
let stages = pipeline.get(job_or_stage_name);
match stages {
Some(_) => return Option::Some(stages.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_pipeline_data<'t>(file: &str) -> Value {
let content = fs::read_to_string(file).expect("Could not read the pipeline file");
let pipeline: Value = serde_yaml::from_str(&content).unwrap();
return pipeline;
}
fn exec_command(command: std::string::String) -> (bool, std::string::String, std::string::String) {
let command_args = OsString::from(command.as_str());
let mut command_base = std::process::Command::new("bash");
let command_executed = command_base
.arg("-c")
.arg(command_args)
.output()
.expect("Error in command: '{command}'");
let command_success = command_executed.status.success();
let command_stdout = std::str::from_utf8(&command_executed.stdout[..])
.unwrap()
.to_owned();
let command_stderr = std::str::from_utf8(&command_executed.stdout[..])
.unwrap()
.to_owned();
return (command_success, command_stdout, command_stderr);
}
#[derive(Debug)]
enum PipelineType {
Null,
Stages,
Jobs,
Script,
}
fn get_pipeline_type(pipeline_data: Value) -> PipelineType {
let stages = get_stages(pipeline_data.clone());
let jobs = get_jobs(pipeline_data.clone());
let script = get_script(pipeline_data.clone());
let pipeline_type_found: PipelineType;
if stages.is_some() {
pipeline_type_found = PipelineType::Stages;
} else if jobs.is_some() {
pipeline_type_found = PipelineType::Jobs;
} else if script.is_some() {
pipeline_type_found = PipelineType::Script;
} else {
pipeline_type_found = PipelineType::Null
}
return pipeline_type_found;
}
fn create_artifact_dir() -> String {
let tstmp: String = chrono::offset::Utc::now()
.format("%Y.%m.%d.%H.%M.%S")
.to_string();
fs::create_dir_all(".simple-ci/run_".to_string() + tstmp.as_str()).unwrap();
return tstmp;
}
fn write_to_file(text: &str, file_name: &str) {
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(file_name)
.unwrap();
file.write(text.as_bytes()).unwrap();
}
pub fn main_exec(myfile: &str) {
let main_pipeline = Pipeline::new(myfile);
if main_pipeline.exec_pipeline() {
println!("{}", "Pipeline passed".green())
} else {
println!("{}", "Pipeline failed".red())
}
}
pub fn main_start() {
println!("Start: Not implemented yet!")
}
pub fn main_stop() {
println!("Stop: Not implemented yet!")
}
pub fn main_init() {
println!("Init: Not implemented yet!")
}
pub fn main_clean() {
println!("Clean: Not implemented yet!")
}