Compare commits
3 Commits
ff3f3c5263
...
fbde2ed863
Author | SHA1 | Date |
---|---|---|
R Tyler Croy | fbde2ed863 | |
R Tyler Croy | 8981370222 | |
R Tyler Croy | a7efcc6085 |
155
README.adoc
155
README.adoc
|
@ -12,134 +12,65 @@ Zap borrows ideas from
|
|||
link:https://puppet.com/docs/bolt/latest/bolt.html[Puppet Bolt]. but leaves
|
||||
some of the Puppet-based legacy from Bolt behind.
|
||||
|
||||
== Examples
|
||||
|
||||
== Goals
|
||||
|
||||
* Support BSD and Linux with ease
|
||||
* Make creating a plan very easy, including adding some simple logic
|
||||
* Explore tags-based task assignment
|
||||
* Pulll dependencies in through git-subtree or other, no inventing a new
|
||||
package distribution mechanism
|
||||
|
||||
|
||||
|
||||
== Sketches
|
||||
|
||||
.install.ztask
|
||||
[source]
|
||||
----
|
||||
/*
|
||||
* The FileSync should be an internal task type
|
||||
*/
|
||||
task FileSync {
|
||||
❯ zap task tasks/echo.ztask -p msg="Hello World" -t zap-freebsd
|
||||
Running task with: TaskOpts { task: "tasks/echo.ztask", parameter: ["msg=Hello World"], targets: "zap-freebsd" }
|
||||
Hello World
|
||||
|
||||
----
|
||||
|
||||
[source]
|
||||
----
|
||||
❯ zap plan ./examples/basic.zplan -t zap-freebsd
|
||||
Running plan with: PlanOpts { plan: "./examples/basic.zplan", targets: "zap-freebsd" }
|
||||
Hello from the wonderful world of zplans!
|
||||
This is nice
|
||||
|
||||
----
|
||||
|
||||
=== Task
|
||||
|
||||
A task is a simple container of some form of execution. Typically this will be
|
||||
a wrapped shell/ruby/python script which does some specific piece of
|
||||
functionality. Tasks may also take parameters, which allow for some
|
||||
pluggability of new values.
|
||||
|
||||
.echo.ztask
|
||||
[source]
|
||||
---
|
||||
task Echo {
|
||||
parameters {
|
||||
localfile {
|
||||
msg {
|
||||
required = true
|
||||
help = "String to echo back to the client"
|
||||
type = string
|
||||
help = "Path on the local system for the file to sync"
|
||||
required = true
|
||||
}
|
||||
remotefile {
|
||||
type = "string"
|
||||
help = "Path on the remote system for the file"
|
||||
required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sketch of a user-defined task
|
||||
task Install {
|
||||
// The restrict block is a collection of expressions which ensure that the
|
||||
// task is not run anywhere it cannot run. Will fail if it's tried to run
|
||||
// somewhere it cannot be run
|
||||
restrict {
|
||||
// Regular expression to define what
|
||||
match_fact("platform", "(.*)-freebsd")
|
||||
match_fact("hostname", "www1")
|
||||
}
|
||||
|
||||
parameters {
|
||||
package {
|
||||
type = string
|
||||
help = 'The package to be installed'
|
||||
required = true
|
||||
}
|
||||
|
||||
// Unless should be implied on every task
|
||||
unless {
|
||||
type = string
|
||||
help = "Script which when returns zero if the package has been installed, i.e. `test -f /usr/bin/nginx`"
|
||||
}
|
||||
// provides should be implied on every task
|
||||
}
|
||||
|
||||
// Parameters exposed as environment variables
|
||||
// including the "ZAP_NOOP" variable which will be set if the script should
|
||||
// be run in noop
|
||||
script {
|
||||
// either the file or the inline must be defined
|
||||
file = "path/to/file/in/tree"
|
||||
// When using `program`, the task should expect to find:
|
||||
// command-name_x86_64-unknown-linux-gnu
|
||||
program = "/path/to/exes/in/tree"
|
||||
inline = """
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/ Exploring module syntax for namespacing these */
|
||||
module Install {
|
||||
task Pkg {
|
||||
restrict {}
|
||||
parameters {}
|
||||
script {}
|
||||
}
|
||||
|
||||
task Zypper {
|
||||
restrict {}
|
||||
parameters {}
|
||||
script {}
|
||||
inline = "echo {{msg}}"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Sketch of a zplan
|
||||
=== Plan
|
||||
|
||||
A plan is a collection of tasks which can be applied to a target or targets.
|
||||
Tasks are referenced with the parameters that should be passed into them, and
|
||||
will be executed in the order that they are defined.
|
||||
|
||||
|
||||
.simple.zplan
|
||||
[source]
|
||||
----
|
||||
// Should things be just done proceduraly?
|
||||
|
||||
task("sync-tree.ztask") { }
|
||||
|
||||
task('tasks/install.ztask') {
|
||||
name = "install-nginx"
|
||||
package = "nginx"
|
||||
unless = "test -f /usr/bin/nginx"
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
}
|
||||
|
||||
// Run another plan from within this plan (supdawg)
|
||||
plan("plans/prepare-website.zplan") {
|
||||
// What parameters make sense here?
|
||||
}
|
||||
|
||||
|
||||
// Relationships between tasks, option A
|
||||
task('tasks/sync-tree.zplan') {
|
||||
source = './foo'
|
||||
destination = '/'
|
||||
|
||||
then = ["install-nginx"]
|
||||
}
|
||||
|
||||
// Relationships between tasks, option B
|
||||
task('tasks/sync-tree.zplan') {
|
||||
source = './foo'
|
||||
destination = '/'
|
||||
|
||||
then {
|
||||
task("tasks/install.ztask") {
|
||||
package = "nginx"
|
||||
unless = "test -f /usr/bin/nginx"
|
||||
}
|
||||
}
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "This is nice"
|
||||
}
|
||||
----
|
||||
|
|
129
cli/src/main.rs
129
cli/src/main.rs
|
@ -3,13 +3,15 @@ use gumdrop::Options;
|
|||
use log::*;
|
||||
use std::collections::HashMap;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod inventory;
|
||||
mod transport;
|
||||
|
||||
use crate::inventory::*;
|
||||
use crate::transport::ssh::Ssh;
|
||||
use zap_parser::*;
|
||||
use zap_parser::plan::Plan;
|
||||
use zap_parser::task::Task;
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
@ -32,36 +34,70 @@ fn main() {
|
|||
match opts.command.unwrap() {
|
||||
Command::Cmd(opts) => handle_cmd(opts, &runner, inventory),
|
||||
Command::Task(opts) => handle_task(opts, &runner, inventory),
|
||||
Command::Plan(opts) => handle_plan(opts, &runner, inventory),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_ztasks() -> Vec<Task> {
|
||||
use glob::glob;
|
||||
let mut tasks = vec![];
|
||||
/**
|
||||
* This function will parse and execute a plan
|
||||
*/
|
||||
fn handle_plan(opts: PlanOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
println!("{}", format!("Running plan with: {:?}", opts).green());
|
||||
let mut exit: i32 = -1;
|
||||
|
||||
for entry in glob("tasks/**/*.ztask").expect("Failed to read glob pattern") {
|
||||
match entry {
|
||||
Ok(path) => {
|
||||
if let Ok(task) = Task::from_path(&path) {
|
||||
info!("loaded ztask: {}", task.name);
|
||||
tasks.push(task);
|
||||
}
|
||||
match Plan::from_path(&opts.plan) {
|
||||
Ok(plan) => {
|
||||
info!("Plan located, preparing to execute");
|
||||
for task in plan.tasks {
|
||||
info!("Running executable task: {:?}", task);
|
||||
exit = execute_task_on(
|
||||
opts.targets.clone(),
|
||||
&task.task,
|
||||
&task.parameters,
|
||||
runner,
|
||||
&inventory,
|
||||
);
|
||||
}
|
||||
Err(e) => println!("{:?}", e),
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Failed to load plan: {:?}", err);
|
||||
}
|
||||
}
|
||||
tasks
|
||||
std::process::exit(exit);
|
||||
}
|
||||
|
||||
fn execute_task_on(
|
||||
targets: String,
|
||||
task: &Task,
|
||||
parameters: &HashMap<String, String>,
|
||||
runner: &dyn crate::transport::Transport,
|
||||
inventory: &Inventory,
|
||||
) -> i32 {
|
||||
if let Some(script) = task.get_script() {
|
||||
let command = render_command(&script, ¶meters);
|
||||
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == targets) {
|
||||
return runner.run_group(&command, &group, &inventory);
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == targets) {
|
||||
return runner.run(&command, &target);
|
||||
}
|
||||
}
|
||||
error!("Failed to locate a script to execute for the task!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will handle a task
|
||||
*/
|
||||
fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
println!("running task: {:?}", opts);
|
||||
println!("{}", format!("Running task with: {:?}", opts).green());
|
||||
|
||||
for task in load_ztasks() {
|
||||
if task.name == opts.task {
|
||||
match Task::from_path(&opts.task) {
|
||||
Ok(task) => {
|
||||
info!("Task located, preparing to execute");
|
||||
let mut parameters = HashMap::new();
|
||||
|
||||
/*
|
||||
|
@ -74,19 +110,16 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
parameters.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(script) = task.get_script() {
|
||||
let command = render_command(&script, ¶meters);
|
||||
|
||||
// TODO: refactor with handle_cmd
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == opts.targets) {
|
||||
std::process::exit(runner.run_group(&command, &group, &inventory));
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == opts.targets) {
|
||||
std::process::exit(runner.run(&command, &target));
|
||||
}
|
||||
}
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
¶meters,
|
||||
runner,
|
||||
&inventory,
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Failed to load task: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,19 +135,16 @@ fn handle_task(opts: TaskOpts, runner: &dyn crate::transport::Transport, invento
|
|||
* non-zero.
|
||||
*/
|
||||
fn handle_cmd(opts: CmdOpts, runner: &dyn crate::transport::Transport, inventory: Inventory) {
|
||||
if let Some(group) = inventory.groups.iter().find(|g| g.name == opts.targets) {
|
||||
std::process::exit(runner.run_group(&opts.command, &group, &inventory));
|
||||
}
|
||||
|
||||
if let Some(target) = inventory.targets.iter().find(|t| t.name == opts.targets) {
|
||||
println!("{}", format!("run a command: {:?}", opts).green());
|
||||
std::process::exit(runner.run(&opts.command, &target));
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Couldn't find a target named `{}`", opts.targets).red()
|
||||
);
|
||||
let mut task = Task::new("Dynamic");
|
||||
task.inline = Some(opts.command);
|
||||
let parameters = HashMap::new();
|
||||
std::process::exit(execute_task_on(
|
||||
opts.targets,
|
||||
&task,
|
||||
¶meters,
|
||||
runner,
|
||||
&inventory,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -172,6 +202,8 @@ enum Command {
|
|||
Cmd(CmdOpts),
|
||||
#[options(help = "Execute a task on a target(s)")]
|
||||
Task(TaskOpts),
|
||||
#[options(help = "Execute a plan on a target(s)")]
|
||||
Plan(PlanOpts),
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
|
@ -186,16 +218,25 @@ struct CmdOpts {
|
|||
#[options(help = "Name of a target or group")]
|
||||
targets: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
struct TaskOpts {
|
||||
#[options(free, help = "Task to execute, must exist in ZAP_PATH")]
|
||||
task: String,
|
||||
#[options(free, help = "Task to execute")]
|
||||
task: PathBuf,
|
||||
#[options(short = "p", help = "Parameter values")]
|
||||
parameter: Vec<String>,
|
||||
#[options(help = "Name of a target or group")]
|
||||
targets: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Options)]
|
||||
struct PlanOpts {
|
||||
#[options(free, help = "Plan to execute")]
|
||||
plan: PathBuf,
|
||||
#[options(help = "Name of a target or group")]
|
||||
targets: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* This zplan just loads a couple tasks and then executes them
|
||||
*
|
||||
* It is expected to be run from the root of the project tree.
|
||||
*/
|
||||
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
}
|
||||
|
||||
task "tasks/echo.ztask" {
|
||||
msg = "This is nice"
|
||||
}
|
|
@ -3,183 +3,5 @@ extern crate pest;
|
|||
#[macro_use]
|
||||
extern crate pest_derive;
|
||||
|
||||
use pest::error::Error as PestError;
|
||||
use pest::error::ErrorVariant;
|
||||
use pest::iterators::Pairs;
|
||||
use pest::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "task.pest"]
|
||||
struct TaskParser;
|
||||
|
||||
pub struct Task {
|
||||
pub name: String,
|
||||
inline: Option<String>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn get_script(&self) -> Option<&String> {
|
||||
self.inline.as_ref()
|
||||
}
|
||||
|
||||
pub fn new(name: &str) -> Self {
|
||||
Task {
|
||||
name: name.to_string(),
|
||||
inline: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(parser: &mut Pairs<Rule>) -> Result<Self, PestError<Rule>> {
|
||||
let mut task: Option<Self> = None;
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::identifier => {
|
||||
task = Some(Task::new(parsed.as_str()));
|
||||
}
|
||||
Rule::script => {
|
||||
let script = parse_str(&mut parsed.into_inner())?;
|
||||
|
||||
if let Some(ref mut task) = task {
|
||||
task.inline = Some(script);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task) = task {
|
||||
return Ok(task);
|
||||
} else {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not find a valid task definition".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
|
||||
let mut parser = TaskParser::parse(Rule::task, buf)?;
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::task => {
|
||||
return Task::parse(&mut parsed.into_inner());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not find a valid task definition".to_string(),
|
||||
},
|
||||
pest::Position::from_start(buf),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
match File::open(path) {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut contents) {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
} else {
|
||||
return Self::from_str(&contents);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser utility function to fish out the _actual_ string value for something
|
||||
* that is looking like a string Rule
|
||||
*
|
||||
*/
|
||||
fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::string => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::double_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_double_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not parse out a string value".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple_script_task() {
|
||||
let buf = r#"task Install {
|
||||
parameters {
|
||||
package {
|
||||
required = true
|
||||
help = "Name of package to be installed"
|
||||
type = string
|
||||
}
|
||||
}
|
||||
script {
|
||||
inline = "zypper in -y ${ZAP_PACKAGE}"
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_parameters() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = "env"
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_task_fn() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = "env"
|
||||
}
|
||||
}"#;
|
||||
let task = Task::from_str(buf).expect("Failed to parse the task");
|
||||
assert_eq!(task.name, "PrintEnv");
|
||||
assert_eq!(task.get_script().unwrap(), "env");
|
||||
}
|
||||
}
|
||||
pub mod plan;
|
||||
pub mod task;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// This describes the plan definition grammar for zap
|
||||
|
||||
planfile = _{ SOI
|
||||
~ task+
|
||||
~ EOI }
|
||||
|
||||
|
||||
task = { "task"
|
||||
~ string
|
||||
~ opening_brace
|
||||
~ kwarg*
|
||||
~ closing_brace
|
||||
}
|
||||
|
||||
kwarg = { identifier ~ equals ~ arg }
|
||||
// Right now only string arguments are supported
|
||||
arg = { string }
|
||||
|
||||
// Unfortunately pest doesn't yet support sharing rules between grammars
|
||||
// so everything below this line is copy/pasted between task.pest and
|
||||
// plan.pest, when making changes to one, make sure to change the other
|
||||
|
||||
// An identifier will be used to refer to the task later
|
||||
identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||
|
||||
opening_brace = _{ "{" }
|
||||
closing_brace = _{ "}" }
|
||||
equals = _{ "=" }
|
||||
quote = _{ "\"" }
|
||||
|
||||
string = { double_quoted }
|
||||
double_quoted = ${ (quote ~ inner_double_str ~ quote) }
|
||||
inner_double_str = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner_double_str)? }
|
||||
escape = @{ "\\" ~ ("\"" | "\\" | "r" | "n" | "t" | "0" | "'" | code | unicode) }
|
||||
code = @{ "x" ~ hex_digit{2} }
|
||||
unicode = @{ "u" ~ opening_brace ~ hex_digit{2, 6} ~ closing_brace }
|
||||
hex_digit = @{ '0'..'9' | 'a'..'f' | 'A'..'F' }
|
||||
|
||||
typedef = { string_type }
|
||||
string_type = { "string" }
|
||||
|
||||
bool = { truthy | falsey }
|
||||
truthy = { "true" }
|
||||
falsey = { "false" }
|
||||
|
||||
block_comment = _{ "/*" ~ (block_comment | !"*/" ~ ANY)* ~ "*/" }
|
||||
COMMENT = _{ block_comment | ("//" ~ (!NEWLINE~ ANY)*) }
|
||||
WHITESPACE = _{ " " | "\t" | NEWLINE }
|
|
@ -0,0 +1,196 @@
|
|||
use log::*;
|
||||
use pest::error::Error as PestError;
|
||||
use pest::error::ErrorVariant;
|
||||
use pest::iterators::Pairs;
|
||||
use pest::Parser;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "plan.pest"]
|
||||
struct PlanParser;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExecutableTask {
|
||||
pub task: crate::task::Task,
|
||||
pub parameters: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl ExecutableTask {
|
||||
fn new(task: crate::task::Task, parameters: HashMap<String, String>) -> Self {
|
||||
Self { task, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Plan {
|
||||
pub tasks: Vec<ExecutableTask>,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
pub fn new() -> Self {
|
||||
Self { tasks: vec![] }
|
||||
}
|
||||
|
||||
pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
|
||||
let mut parser = PlanParser::parse(Rule::planfile, buf)?;
|
||||
let mut plan = Plan::new();
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::task => {
|
||||
let mut raw_task = None;
|
||||
let mut parameters: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for pair in parsed.into_inner() {
|
||||
match pair.as_rule() {
|
||||
Rule::string => {
|
||||
let path = PathBuf::from(parse_str(&mut pair.into_inner())?);
|
||||
|
||||
match crate::task::Task::from_path(&path) {
|
||||
Ok(task) => raw_task = Some(task),
|
||||
Err(err) => {
|
||||
error!("Failed to parse task: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Rule::kwarg => {
|
||||
let (key, val) = parse_kwarg(&mut pair.into_inner())?;
|
||||
parameters.insert(key, val);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task) = raw_task {
|
||||
plan.tasks.push(ExecutableTask::new(task, parameters));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
match File::open(path) {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut contents) {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
} else {
|
||||
return Self::from_str(&contents);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_kwarg(parser: &mut Pairs<Rule>) -> Result<(String, String), PestError<Rule>> {
|
||||
let mut identifier = None;
|
||||
let mut arg = None;
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::identifier => identifier = Some(parsed.as_str().to_string()),
|
||||
Rule::arg => arg = Some(parse_str(&mut parsed.into_inner())?),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if identifier.is_some() && arg.is_some() {
|
||||
return Ok((identifier.unwrap(), arg.unwrap()));
|
||||
}
|
||||
Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not parse keyword arguments for parameters".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser utility function to fish out the _actual_ string value for something
|
||||
* that is looking like a string Rule
|
||||
*/
|
||||
fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::string => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::double_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_double_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not parse out a string value".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple_plan() {
|
||||
let buf = r#"/*
|
||||
* This zplan just loads a couple tasks and then executes them
|
||||
*
|
||||
* It is expected to be run from the root of the project tree.
|
||||
*/
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
}
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
msg = "This can actually take inline shells too: $(date)"
|
||||
}"#;
|
||||
let _plan = PlanParser::parse(Rule::planfile, buf)
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_plan_fn() {
|
||||
let buf = r#"task "../tasks/echo.ztask" {
|
||||
msg = "Hello from the wonderful world of zplans!"
|
||||
}
|
||||
|
||||
task "../tasks/echo.ztask" {
|
||||
msg = "This can actually take inline shells too: $(date)"
|
||||
}"#;
|
||||
let plan = Plan::from_str(buf).expect("Failed to parse the plan");
|
||||
assert_eq!(plan.tasks.len(), 2);
|
||||
}
|
||||
}
|
|
@ -12,9 +12,6 @@ task = { "task"
|
|||
~ closing_brace
|
||||
}
|
||||
|
||||
// An identifier will be used to refer to the task later
|
||||
identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||
|
||||
parameters = { "parameters"
|
||||
~ opening_brace
|
||||
~ parameter+
|
||||
|
@ -40,6 +37,15 @@ script = { "script"
|
|||
}
|
||||
script_inline = _{ "inline" ~ equals ~ string }
|
||||
|
||||
|
||||
|
||||
// Unfortunately pest doesn't yet support sharing rules between grammars
|
||||
// so everything below this line is copy/pasted between task.pest and
|
||||
// plan.pest, when making changes to one, make sure to change the other
|
||||
|
||||
// An identifier will be used to refer to the task later
|
||||
identifier = { ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* }
|
||||
|
||||
opening_brace = _{ "{" }
|
||||
closing_brace = _{ "}" }
|
||||
equals = _{ "=" }
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
use pest::error::Error as PestError;
|
||||
use pest::error::ErrorVariant;
|
||||
use pest::iterators::Pairs;
|
||||
use pest::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[grammar = "task.pest"]
|
||||
struct TaskParser;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Task {
|
||||
pub name: String,
|
||||
pub inline: Option<String>,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn get_script(&self) -> Option<&String> {
|
||||
self.inline.as_ref()
|
||||
}
|
||||
|
||||
pub fn new(name: &str) -> Self {
|
||||
Task {
|
||||
name: name.to_string(),
|
||||
inline: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(parser: &mut Pairs<Rule>) -> Result<Self, PestError<Rule>> {
|
||||
let mut task: Option<Self> = None;
|
||||
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::identifier => {
|
||||
task = Some(Task::new(parsed.as_str()));
|
||||
}
|
||||
Rule::script => {
|
||||
let script = parse_str(&mut parsed.into_inner())?;
|
||||
|
||||
if let Some(ref mut task) = task {
|
||||
task.inline = Some(script);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task) = task {
|
||||
return Ok(task);
|
||||
} else {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not find a valid task definition".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(buf: &str) -> Result<Self, PestError<Rule>> {
|
||||
let mut parser = TaskParser::parse(Rule::task, buf)?;
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::task => {
|
||||
return Task::parse(&mut parsed.into_inner());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not find a valid task definition".to_string(),
|
||||
},
|
||||
pest::Position::from_start(buf),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn from_path(path: &PathBuf) -> Result<Self, PestError<Rule>> {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
match File::open(path) {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut contents) {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
} else {
|
||||
return Self::from_str(&contents);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: format!("{}", e),
|
||||
},
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser utility function to fish out the _actual_ string value for something
|
||||
* that is looking like a string Rule
|
||||
*/
|
||||
fn parse_str(parser: &mut Pairs<Rule>) -> Result<String, PestError<Rule>> {
|
||||
while let Some(parsed) = parser.next() {
|
||||
match parsed.as_rule() {
|
||||
Rule::string => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::double_quoted => {
|
||||
return parse_str(&mut parsed.into_inner());
|
||||
}
|
||||
Rule::inner_double_str => {
|
||||
return Ok(parsed.as_str().to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
return Err(PestError::new_from_pos(
|
||||
ErrorVariant::CustomError {
|
||||
message: "Could not parse out a string value".to_string(),
|
||||
},
|
||||
/* TODO: Find a better thing to report */
|
||||
pest::Position::from_start(""),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple_script_task() {
|
||||
let buf = r#"task Install {
|
||||
parameters {
|
||||
package {
|
||||
required = true
|
||||
help = "Name of package to be installed"
|
||||
type = string
|
||||
}
|
||||
}
|
||||
script {
|
||||
inline = "zypper in -y ${ZAP_PACKAGE}"
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_parameters() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = "env"
|
||||
}
|
||||
}"#;
|
||||
let _task = TaskParser::parse(Rule::task, buf).unwrap().next().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_task_fn() {
|
||||
let buf = r#"task PrintEnv {
|
||||
script {
|
||||
inline = "env"
|
||||
}
|
||||
}"#;
|
||||
let task = Task::from_str(buf).expect("Failed to parse the task");
|
||||
assert_eq!(task.name, "PrintEnv");
|
||||
assert_eq!(task.get_script().unwrap(), "env");
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ task Echo {
|
|||
type = string
|
||||
}
|
||||
}
|
||||
|
||||
script {
|
||||
inline = "echo {{msg}}"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue