Compare commits

...

3 Commits

Author SHA1 Message Date
R Tyler Croy fbde2ed863 Implement the execution of plans via the CLI 2020-12-31 11:10:12 -08:00
R Tyler Croy 8981370222 Refactoring the parsing code a fair bit to make way for the zplan parser
Turns out that a single module using #[derive(Parser)] cannot have two parsers
in the same module
2020-12-31 09:48:39 -08:00
R Tyler Croy a7efcc6085 Switch ztasks to be file-based
Played around with using symbols, and figuring out the namespacing is going to
be too much of a pain in the ass
2020-12-31 09:30:04 -08:00
10 changed files with 578 additions and 339 deletions

View File

@ -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"
}
----

View File

@ -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, &parameters);
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, &parameters);
// 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,
&parameters,
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,
&parameters,
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::*;

13
examples/basic.zplan Normal file
View File

@ -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"
}

View File

View File

@ -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;

48
parser/src/plan.pest Normal file
View File

@ -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 }

196
parser/src/plan.rs Normal file
View File

@ -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);
}
}

View File

@ -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 = _{ "=" }

181
parser/src/task.rs Normal file
View File

@ -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");
}
}

View File

@ -6,6 +6,7 @@ task Echo {
type = string
}
}
script {
inline = "echo {{msg}}"
}