diff --git a/README.md b/README.md index 903876bc..7d097aad 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Here is Rash! ### Declarative vs imperative -`entrypoint.sh`: +Imperative: `entrypoint.sh`: ```bash #!/bin/bash set -e @@ -47,7 +47,7 @@ $VAULT_URL/v1/$VAULT_SECRET_PATH | jq -r .data.api_key) exec "$@" ``` -`entrypoint.rh` +Declarative: `entrypoint.rh` ```yaml #!/bin/rash @@ -74,6 +74,10 @@ You could use it in your favorite IoT chips running Linux or in containers from ## Status +Currently, **Under heavy development**. + +Full working funcionallity show in gif, does not expect more (or less): + ![Examples](https://media.giphy.com/media/YqQQGUib5yzNM2GvFe/giphy.gif) [Jinja2](https://tera.netlify.app/docs/#templates) template engine support by [Tera](https://github.com/Keats/tera). @@ -82,7 +86,11 @@ Current [modules](./rash_core/src/modules/) ## Roadmap -Roadmap is defined in our Concept Map but some more concrete examples could be found below: +Roadmap is defined in our +[Concept Map](https://mind42.com/mindmap/f299679e-8dc5-48d8-b0f0-4d65235cdf56) but some more +concrete examples could be found below. + +This are just ideas of the possibilities of `rash` ### Lookups @@ -93,15 +101,14 @@ Roadmap is defined in our Concept Map but some more concrete examples could be f #!/bin/rash - name: file from s3 - copy: - content: "{{ lookup('s3', env.MYBUNDLE_S3_PATH) }}" - dest: /myapp/i18n/bundle.json + template: + content: "{{ lookup('s3', bucket='mybucket', object='config.json.j2')}}" + dest: /myapp/config.json mode: 0400 - name: launch docker CMD command: {{ input.args }} transfer_ownership: yes - ``` #### vault @@ -143,6 +150,24 @@ Roadmap is defined in our Concept Map but some more concrete examples could be f mode: 0400 ``` +#### S3 + +`s3.rh`: +```yaml +#!/bin/rash + +- name: file from s3 + s3: + bucket: mybucket + object: "{{ env.MYBUNDLE_S3_PATH }}" + dest: /myapp/i18n/bundle.json + mode: 0400 + +- name: launch docker CMD + command: {{ input.args }} + transfer_ownership: yes +``` + #### Template ```yaml diff --git a/design/concept_map.mm b/design/concept_map.mm index f2630d98..6546abbb 100644 --- a/design/concept_map.mm +++ b/design/concept_map.mm @@ -1,24 +1,27 @@ - + - + - + - + - - - -

Todos:
env - Normal priority - 100% - 05/11/2020
cli - Normal priority - 0% - 05/15/2020

-
- - -
+ + + + + +

Todos:
env - Normal priority - 100% - 05/11/2020
cli - Normal priority - 100% - 05/15/2020

+
+ + +
+
- + - + @@ -28,11 +31,11 @@ - + - + @@ -42,7 +45,7 @@ - + @@ -52,38 +55,38 @@ - + - + - + - + - + - + - + - + - + @@ -95,7 +98,7 @@ - + @@ -104,34 +107,45 @@ - + + + + + + + + +

Run a block of tasks until some condition reached

+ + +
- + - + - + - + - + - + - + - + @@ -141,14 +155,14 @@ - + - + - + @@ -160,42 +174,42 @@ - + - + - + - + - + - + - + - + - + - + - + @@ -209,7 +223,7 @@ - + @@ -218,7 +232,7 @@ - + @@ -230,7 +244,7 @@ - + @@ -241,45 +255,56 @@ - + - + - + - + - + - + - + - +
`N` flavours, compile from args (envars) or target file (read modules and compile just necessary ones)
+
alpine and scratch are redundant (both statically linked) but in the future alpine could be dynamically linked
- + + + + +
Just copy from docker images
+
+
+
+ + +
- + - + @@ -289,21 +314,21 @@ - + - + - + - + - + diff --git a/rash_core/Cargo.toml b/rash_core/Cargo.toml index 85aac97b..d9892c5f 100644 --- a/rash_core/Cargo.toml +++ b/rash_core/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "rash_core" version = "0.1.0" +description = "Declarative shell scripting using Rust native bindings" authors = ["Pando85 "] edition = "2018" +license-file = "../LICENSE" +homepage = "https://github.com/pando85/rash" +repository = "https://github.com/pando85/rash" +readme = "../README.md" [lib] name = "rash_core" diff --git a/rash_core/src/context.rs b/rash_core/src/context.rs index 3f80da68..b82c0900 100644 --- a/rash_core/src/context.rs +++ b/rash_core/src/context.rs @@ -11,6 +11,11 @@ use crate::task::Task; #[cfg(test)] use crate::facts::test_example as facts_text_example; +/// Main data structure in `rash`. +/// It contents all [`task::Tasks`] with its [`facts::Facts`] to be executed +/// +/// [`task::Tasks`]: ../task/type.Tasks.html +/// [`facts::Facts`]: ../facts/type.Facts.html #[derive(Debug)] pub struct Context { tasks: Tasks, @@ -18,20 +23,26 @@ pub struct Context { } impl Context { + /// Create a new context from [`task::Tasks`] and [`facts::Facts`].Error + /// + /// [`task::Tasks`]: ../task/type.Tasks.html + /// [`facts::Facts`]: ../facts/type.Facts.html pub fn new(tasks: Tasks, facts: Facts) -> Self { Context { tasks, facts } } - /// Execute task using inventory + /// Execute first [`task::Task`] and return a new context without that executed [`task::Task`] + /// + /// [`task::Task`]: ../task/struct.Task.html pub fn exec_task(&self) -> Result { - let mut next_tasks = self.tasks.clone(); - if next_tasks.is_empty() { + if self.tasks.is_empty() { return Err(Error::new( ErrorKind::EmptyTaskStack, format!("No more tasks in context stack: {:?}", self), )); } + let mut next_tasks = self.tasks.clone(); let next_task = next_tasks.remove(0); info!(target: "task", "[{}] - {} to go - ", @@ -46,6 +57,12 @@ impl Context { }) } + /// Execute all Tasks in Context until empty. + /// + /// If it finish correctly it will return an [`error::Error`] with [`ErrorKind::EmptyTaskStack`] + /// + /// [`error::Error`]: ../error/struct.Error.html + /// [`ErrorKind::EmptyTaskStack`]: ../error/enum.ErrorKind.html pub fn exec(context: Self) -> Result { // https://prev.rust-lang.org/en-US/faq.html#does-rust-do-tail-call-optimization Self::exec(context.exec_task()?) diff --git a/rash_core/src/error.rs b/rash_core/src/error.rs index 9099622c..0e835638 100644 --- a/rash_core/src/error.rs +++ b/rash_core/src/error.rs @@ -5,16 +5,10 @@ use std::result; use yaml_rust::scanner::ScanError; -/// A specialized [`Result`](../result/enum.Result.html) type rash -/// operations. -/// -/// [`Error`]: ../struct.Error.html -/// [`Result`]: ../result/enum.Result.html +/// A specialized type `rash` operations. pub type Result = result::Result; -/// The error type for rash executions. -/// -/// [`ErrorKind`]: enum.ErrorKind.html +/// The error type for `rash` executions. pub struct Error { repr: Repr, } @@ -36,7 +30,7 @@ struct Custom { error: Box, } -/// A list specifying general categories of rash error. +/// A list specifying general categories of `rash` error. /// /// This list is intended to grow over time and it is not recommended to /// exhaustively match against it. @@ -56,7 +50,7 @@ pub enum ErrorKind { SubprocessFail, /// Task stack is empty EmptyTaskStack, - /// Any rash error not part of this list. + /// Any `rash` error not part of this list. Other, } @@ -89,8 +83,8 @@ impl From for Error { /// assert_eq!("entity not found", format!("{}", error)); /// ``` /// - /// [`ErrorKind`]: ../../std/io/enum.ErrorKind.html - /// [`Error`]: ../../std/io/struct.Error.html + /// [`ErrorKind`]: enum.ErrorKind.html + /// [`Error`]: struct.Error.html #[inline] fn from(kind: ErrorKind) -> Error { Error { @@ -118,8 +112,24 @@ impl From for Error { } impl Error { - /// Creates a new rash error from a known kind of error as well as an + /// Creates a new `rash` error from a known kind of error as well as an /// arbitrary error payload. + /// + /// # Examples + /// + /// ``` + /// use rash_core::error::{Error, ErrorKind}; + /// + /// let invalid_data_err = Error::new( + /// ErrorKind::InvalidData, + /// "no valid data", + /// ); + /// let custom_error = Error::new( + /// ErrorKind::Other, + /// invalid_data_err, + /// ); + + /// ``` pub fn new(kind: ErrorKind, error: E) -> Error where E: Into>, @@ -133,6 +143,26 @@ impl Error { } } + /// Returns the corresponding `ErrorKind` for this error. + /// + /// # Examples + /// + /// ``` + /// use rash_core::error::{Error, ErrorKind}; + /// + /// fn print_error(err: Error) { + /// println!("{:?}", err.kind()); + /// } + /// + /// print_error(Error::new(ErrorKind::InvalidData, "oh no!")); + /// ``` + pub fn kind(&self) -> ErrorKind { + match self.repr { + Repr::Custom(ref c) => c.kind, + Repr::Simple(kind) => kind, + } + } + /// Returns the OS error that this error represents (if any). /// /// # Examples @@ -273,26 +303,6 @@ impl Error { Repr::Custom(c) => Some(c.error), } } - - /// Returns the corresponding `ErrorKind` for this error. - /// - /// # Examples - /// - /// ``` - /// use rash_core::error::{Error, ErrorKind}; - /// - /// fn print_error(err: Error) { - /// println!("{:?}", err.kind()); - /// } - /// - /// print_error(Error::new(ErrorKind::InvalidData, "oh no!")); - /// ``` - pub fn kind(&self) -> ErrorKind { - match self.repr { - Repr::Custom(ref c) => c.kind, - Repr::Simple(kind) => kind, - } - } } impl fmt::Debug for Repr { diff --git a/rash_core/src/facts/env.rs b/rash_core/src/facts/env.rs index 6fead42f..d8febf4f 100644 --- a/rash_core/src/facts/env.rs +++ b/rash_core/src/facts/env.rs @@ -20,6 +20,19 @@ impl From for Env { } } +/// Create [`Facts`] from environment variables plus input vector overwriting them. +/// +/// [`Facts`]: ../type.Facts.html +/// +/// # Example +/// +/// ``` +/// use rash_core::facts::env::load; +/// +/// use std::env; +/// +/// let facts = load(vec![("foo".to_string(), "boo".to_string())]).unwrap(); +/// ``` pub fn load(envars: Vec<(String, String)>) -> Result { trace!("{:?}", envars); envars.into_iter().for_each(|(k, v)| env::set_var(k, v)); diff --git a/rash_core/src/facts/mod.rs b/rash_core/src/facts/mod.rs index 96b629f1..0be328bb 100644 --- a/rash_core/src/facts/mod.rs +++ b/rash_core/src/facts/mod.rs @@ -2,6 +2,9 @@ pub mod env; use tera::Context; +/// Variables stored and accessible during execution, based on [`tera::Context`] +/// +/// [`tera::Context`]: ../../tera/struct.Context.html pub type Facts = Context; #[cfg(test)] diff --git a/rash_core/src/logger.rs b/rash_core/src/logger.rs index 691780f9..8e7d759c 100644 --- a/rash_core/src/logger.rs +++ b/rash_core/src/logger.rs @@ -49,6 +49,7 @@ fn log_format(out: FormatCallback, message: &fmt::Arguments, record: &log::Recor )) } +/// Setup logging in function of verbosity. pub fn setup_logging(verbosity: u8) -> Result<()> { let mut base_config = fern::Dispatch::new(); diff --git a/rash_core/src/main.rs b/rash_core/src/main.rs index c6037277..ea5e508d 100644 --- a/rash_core/src/main.rs +++ b/rash_core/src/main.rs @@ -7,7 +7,7 @@ use rash_core::task::read_file; use std::path::PathBuf; use std::process::exit; -use clap::{crate_version, Clap}; +use clap::{crate_description, crate_version, Clap}; #[macro_use] extern crate log; @@ -26,9 +26,13 @@ where Ok((s[..pos].parse()?, s[pos + 1..].parse()?)) } -/// Declarative shell scripting using Rust native bindings #[derive(Clap)] -#[clap(name="rash", version = crate_version!(), author = "Alexander Gil ")] +#[clap( + name="rash", + about = crate_description!(), + version = crate_version!(), + author = "Alexander Gil ", +)] struct Opts { /// Script file to be executed script_file: String, @@ -40,10 +44,14 @@ struct Opts { environment: Vec<(String, String)>, } +/// Fail program printing [`Error`] and returning code associated if exists. +/// By default fail with `exit(1)` +/// +/// [`Error`]: ../rash_core/error/struct.Error.html fn crash_error(e: Error) { error!("{}", e); trace!(target: "error", "{:?}", e); - exit(1) + exit(e.raw_os_error().unwrap_or(1)) } fn main() { diff --git a/rash_core/src/modules/mod.rs b/rash_core/src/modules/mod.rs index 7e5c39a6..0139e5c0 100644 --- a/rash_core/src/modules/mod.rs +++ b/rash_core/src/modules/mod.rs @@ -7,6 +7,9 @@ use std::collections::HashMap; use serde_json::Value; use yaml_rust::Yaml; +/// Return values by [`Module`] execution. +/// +/// [`Module`]: struct.Module.html pub struct ModuleResult { changed: bool, extra: Option, @@ -14,20 +17,23 @@ pub struct ModuleResult { } impl ModuleResult { + /// Return changed pub fn get_changed(&self) -> bool { self.changed } + /// Return extra pub fn get_extra(&self) -> Option { self.extra.clone() } + /// Return output which is printed in log pub fn get_output(&self) -> Option { self.output.clone() } } -/// Module definition with exec function and input parameters +/// Basic execution structure. Build with module name and module exec function #[derive(Debug, Clone, PartialEq)] pub struct Module { name: &'static str, @@ -35,10 +41,12 @@ pub struct Module { } impl Module { + /// Return name pub fn get_name(&self) -> &str { self.name } + /// Execute `self.exec_fn` pub fn exec(&self, params: Yaml) -> Result { (self.exec_fn)(params) } diff --git a/rash_core/src/task.rs b/rash_core/src/task.rs index d51fddd1..e4670f5d 100644 --- a/rash_core/src/task.rs +++ b/rash_core/src/task.rs @@ -11,8 +11,12 @@ use std::path::PathBuf; use tera::Tera; use yaml_rust::{Yaml, YamlLoader}; -/// Task is composed of Module and parameters to be executed in a concrete context -/// Admits attributes to modify task behaviour as log output, execution or changes in context +/// Main structure at definition level which prepare [`Module`] executions. +/// +/// It implements a state machine using Rust Generics to enforce well done definitions. +/// Inspired by [Kanidm Entries](https://fy.blackhats.net.au/blog/html/2019/04/13/using_rust_generics_to_enforce_db_record_state.html). +/// +/// [`Module`]: ../modules/struct.Module.html #[derive(Debug, Clone, PartialEq, FieldNames)] pub struct Task { module: Module, @@ -20,6 +24,9 @@ pub struct Task { name: Option, } +/// A lists of [`Task`] +/// +/// [`Task`]: struct.Task.html pub type Tasks = Vec; #[inline(always)] @@ -28,6 +35,14 @@ fn is_module(module: &str) -> bool { } impl Task { + /// Create a new Task from [`Yaml`]. + /// Enforcing all key values are valid using TaskNew and TaskValid internal states. + /// + /// All final values must be convertible to String and all keys must contain one module and + /// [`Task`] fields. + /// + /// [`Task`]: struct.Task.html + /// [`Yaml`]: ../../yaml_rust/struct.Yaml.html pub fn new(yaml: &Yaml) -> Result { trace!("new task: {:?}", yaml); TaskNew::from(yaml).validate_attrs()?.get_task() @@ -76,6 +91,10 @@ impl Task { } } + /// Execute [`Module`] rendering `self.params` with [`Facts`]. + /// + /// [`Module`]: ../modules/struct.Module.html + /// [`Facts`]: ../facts/struct.Facts.html pub fn exec(&self, facts: Facts) -> Result { debug!("Module: {}", self.module.get_name()); debug!("Params: {:?}", self.params); @@ -87,10 +106,14 @@ impl Task { Ok(facts) } + /// Return name. pub fn get_name(&self) -> Option { self.name.clone() } + /// Return name rendered with [`Facts`]. + /// + /// [`Facts`]: ../facts/struct.Facts.html pub fn get_rendered_name(&self, facts: Facts) -> Result { Task::render_string( &self @@ -101,6 +124,9 @@ impl Task { ) } + /// Return [`Module`]. + /// + /// [`Module`]: ../modules/struct.Module.html pub fn get_module(&self) -> Module { self.module.clone() } @@ -119,7 +145,7 @@ impl Task { } } -/// TaskValid is a ProtoTask with attrs validated (valid modules or attrs) +/// TaskValid is a ProtoTask with attrs verified (one module and valid attrs) #[derive(Debug)] struct TaskValid { attrs: Yaml, @@ -180,7 +206,7 @@ impl TaskValid { } } -/// TaskNew is a new task without checking yaml validity +/// TaskNew is a new task without Yaml verified #[derive(Debug)] struct TaskNew { proto_attrs: Yaml, diff --git a/rash_derive/Cargo.toml b/rash_derive/Cargo.toml index 5efa7e06..82e23515 100644 --- a/rash_derive/Cargo.toml +++ b/rash_derive/Cargo.toml @@ -1,10 +1,13 @@ [package] name = "rash_derive" +description = "rash derive crate" version = "0.1.0" authors = ["Pando85 "] edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +license-file = "../LICENSE" +homepage = "https://github.com/pando85/rash" +repository = "https://github.com/pando85/rash" +readme = "../README.md" [lib] proc-macro = true diff --git a/rash_derive/src/lib.rs b/rash_derive/src/lib.rs index 628686f5..f4329441 100644 --- a/rash_derive/src/lib.rs +++ b/rash_derive/src/lib.rs @@ -6,6 +6,15 @@ extern crate syn; use proc_macro::TokenStream; /// Implementation of the `#[derive(FieldNames)]` derive macro. +/// +/// Add a new method which return field names +/// ``` +/// # use std::collections::HashSet; +/// pub fn get_field_names() -> HashSet +/// # { +/// # HashSet::new() +/// # } +/// ``` #[proc_macro_derive(FieldNames, attributes(field_names))] pub fn derive_field_names(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemStruct); @@ -19,6 +28,7 @@ pub fn derive_field_names(input: TokenStream) -> TokenStream { let expanded = quote::quote! { impl #name { + /// Return field names. pub fn get_field_names() -> std::collections::HashSet { [#(#field_names),*].iter().map(ToString::to_string).collect::>() }