Implement the simplest of runners using the step libraries

Using the following test pipeline file:

  # This file is just a pretend primitive test pipeline with a bunch of steps
  ---
  steps:
    - symbol: sh
      parameters:
        script: 'ls -lah | tail -n 5'
    - symbol: sh
      parameters:
        script: 'echo "Hello world from a script"'
    - symbol: unknown
      parameters:
        message: 'this should fail'

And then invoking as such:

  ❯ STEPS_DIR=tmp ./target/debug/primitive-agent test-pipeline.yml
  sh exists
  sh exists
  unknown/manifest.yml does not exist, step cannot execute
  NORMALLY THIS WOULD ERROR BEFORE ANYTHING EXECUTES
  ---
  entry: "tmp/sh/sh-step"
  -rw-r--r--  1 tyler users 1.1K Feb 20  2020 system.dot
  -rw-r--r--  1 tyler users  43K Feb 20  2020 system.png
  drwxr-xr-x  7 tyler users 4.0K Oct 17 15:25 target
  -rw-r--r--  1 tyler users  304 Oct 18 15:04 test-pipeline.yml
  drwxr-xr-x  4 tyler users 4.0K Oct 17 16:07 tmp
  entry: "tmp/sh/sh-step"
  Hello world from a script

So at a _very_ _very_ primitive level the concept is working 👏
This commit is contained in:
R Tyler Croy 2020-10-18 15:05:05 -07:00
parent 3760508cb8
commit a5de9294aa
4 changed files with 95 additions and 16 deletions

3
Cargo.lock generated
View File

@ -628,12 +628,13 @@ dependencies = [
]
[[package]]
name = "primitive"
name = "primitive-agent"
version = "0.1.0"
dependencies = [
"osp 0.1.0",
"serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]

View File

@ -1,5 +1,5 @@
[package]
name = "primitive"
name = "primitive-agent"
version = "0.1.0"
authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
edition = "2018"
@ -8,3 +8,4 @@ edition = "2018"
serde_yaml = "~0.8.13"
serde = {version = "~1.0.117", features = ["rc", "derive"]}
osp = { path = "../../osp" }
tempfile = "~3.1.0"

View File

@ -1,8 +1,13 @@
use serde::Deserialize;
use serde_yaml::Value;
use std::collections::HashMap;
use std::fs::File;
use std::io::{stdout, stderr, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::NamedTempFile;
#[derive(Clone, Debug, Deserialize)]
struct Pipeline {
@ -12,10 +17,82 @@ struct Pipeline {
#[derive(Clone, Debug, Deserialize)]
struct Step {
symbol: String,
parameters: HashMap<String, String>,
parameters: Value,
}
fn run(steps_dir: &str, steps: &Vec<Step>) -> std::io::Result<()> {
let dir = Path::new(steps_dir);
fn main() {
println!("Hello, world!");
if ! dir.is_dir() {
panic!("STEPS_DIR must be a directory! {:?}", dir);
}
let mut manifests: HashMap<String, osp::Manifest> = HashMap::new();
let mut m_paths: HashMap<String, PathBuf> = HashMap::new();
for step in steps.iter() {
let manifest_file = dir.join(&step.symbol).join("manifest.yml");
if manifest_file.is_file() {
println!("{} exists", step.symbol);
let file = File::open(manifest_file)?;
// TODO: This is dumb and inefficient
m_paths.insert(step.symbol.clone(), dir.join(&step.symbol).to_path_buf());
manifests.insert(step.symbol.clone(),
serde_yaml::from_reader::<File, osp::Manifest>(file).expect("Failed to parse manifest")
);
}
else {
println!("{}/manifest.yml does not exist, step cannot execute", step.symbol);
println!("NORMALLY THIS WOULD ERROR BEFORE ANYTHING EXECUTES");
}
}
println!("---");
// Now that things are valid and collected, let's executed
for step in steps.iter() {
if let Some(runner) = manifests.get(&step.symbol) {
let m_path = m_paths.get(&step.symbol).expect("Failed to grab the step library path");
let entrypoint = m_path.join(&runner.entrypoint.path);
println!("entry: {:?}", entrypoint);
let mut file = NamedTempFile::new()?;
let mut step_args = HashMap::new();
step_args.insert("parameters", &step.parameters);
serde_yaml::to_writer(&mut file, &step_args)
.expect("Failed to write temporary file for script");
let output = Command::new(entrypoint)
.arg(file.path())
.output()
.expect("Failed to invoke the script");
stdout().write_all(&output.stdout).unwrap();
stderr().write_all(&output.stderr).unwrap();
}
}
Ok(())
}
fn main() -> std::io::Result<()>{
let args: Vec<String> = std::env::args().collect();
let steps_dir = std::env::var("STEPS_DIR").expect("STEPS_DIR must be defined");
if args.len() != 2 {
panic!("The sh step can only accept a single argument: the parameters file path");
}
let file = File::open(&args[1])?;
match serde_yaml::from_reader::<File, Pipeline>(file) {
Err(e) => {
panic!("Failed to parse parameters file: {:#?}", e);
}
Ok(invoke) => {
run(&steps_dir, &invoke.steps);
},
};
Ok(())
}

View File

@ -6,11 +6,11 @@ use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Manifest {
symbol: String,
description: String,
includes: Vec<Include>,
entrypoint: Entrypoint,
parameters: Vec<Parameter>,
pub symbol: String,
pub description: String,
pub includes: Vec<Include>,
pub entrypoint: Entrypoint,
pub parameters: Vec<Parameter>,
}
impl Manifest {
@ -52,21 +52,21 @@ impl Manifest {
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Include {
pub struct Include {
name: String,
#[serde(default = "default_false")]
flatten: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Entrypoint {
path: PathBuf,
pub struct Entrypoint {
pub path: PathBuf,
#[serde(default = "default_false")]
multiarch: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Parameter {
pub struct Parameter {
name: String,
required: bool,
#[serde(rename = "type")]
@ -75,7 +75,7 @@ struct Parameter {
}
#[derive(Clone, Debug, Deserialize, Serialize)]
enum ParameterType {
pub enum ParameterType {
#[serde(rename = "string")]
StringParameter,
#[serde(rename = "boolean")]