Created
January 29, 2022 18:35
-
-
Save frgomes/95694f3494c940f1a5a78d09a071c101 to your computer and use it in GitHub Desktop.
Rust - Render YAML resolving environment variables and YAML references.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![allow(unused_parens)] | |
use anyhow::{Context,Result, anyhow}; | |
use clap::{arg, App, AppSettings}; | |
use std::ffi::OsStr; | |
fn main() -> Result<()> { | |
let matches = App::new("mkvm") | |
.about("Make virtual machines easily!") | |
.setting(AppSettings::SubcommandRequiredElseHelp) | |
.subcommand( | |
App::new("vm") | |
.about("start virtual machine(s)") | |
.setting(AppSettings::ArgRequiredElseHelp) | |
.arg(arg!(names: <NAME> ... "Virtual machines to start").allow_invalid_utf8(true)) | |
.arg( | |
arg!(files: [YAML]) | |
.multiple_occurrences(true) | |
.allow_invalid_utf8(true) | |
.last(true), | |
), | |
) | |
.get_matches(); | |
let args: Args = validate(&matches, "vm")?; | |
//XXX println!("Names: {:?}", args.names); | |
//XXX println!("Files: {:?}", args.files); | |
let yaml: String = args.render()?; | |
println!("{}", yaml); | |
Ok(()) | |
} | |
struct Args<'a> { | |
names: Vec<&'a OsStr>, | |
files: Vec<&'a OsStr>, | |
} | |
fn validate<'a>(matches: &'a clap::ArgMatches, _subcommand: &str) -> Result<Args<'a>> { | |
let mut stdin_seen = false; | |
let (names, files) = match matches.subcommand() { | |
Some((_subcommand, sub_matches)) => { | |
let names: Vec<&'a OsStr> = sub_matches | |
.values_of_os("names").context("names of virtual machines is requered")? | |
.collect::<Vec<_>>(); | |
let files: Vec<&'a OsStr> = sub_matches | |
.values_of_os("files").context("a list of file names is requered")? | |
.map(|path| if(path == "-" && stdin_seen) { Err(anyhow!("stdin specified multiple times")) } else { stdin_seen = true; Ok(path) }) | |
.map(|path| path.unwrap()) | |
.collect::<Vec<_>>(); | |
(names, files) | |
} | |
_ => unreachable!(), | |
}; | |
Ok(Args{ names, files, }) | |
} | |
///////////////////////////////////////////////////////////////////////////////////////////////// | |
#![recursion_limit = "256"] | |
use handlebars::Handlebars; | |
use serde_yaml::Value; | |
trait Render { | |
fn render(&self) -> Result<String>; | |
} | |
impl Render for String { | |
fn render(&self) -> Result<String> { | |
let mut tmpl: String = self.clone(); | |
let mut data: Value = serde_yaml::from_str(&tmpl)?; | |
let handlebars = Handlebars::new(); | |
let mut count = 0; | |
loop { | |
let rendered = handlebars.render_template(&tmpl, &data).unwrap(); | |
let exit = rendered == tmpl; | |
tmpl = rendered; | |
data = serde_yaml::from_str(&tmpl)?; | |
if exit { break; } | |
count = count +1; | |
if (count > 10) { break; } // some extra care to avoid infinite loop. | |
} | |
Ok(tmpl) | |
} | |
} | |
impl Render for Vec<&OsStr> { | |
fn render(&self) -> Result<String> { | |
Ok((*self) | |
.iter() | |
.map(|path| path.reader().unwrap()) | |
.fold(String::new(), |mut acc, item| { acc.push_str("\n"); acc.push_str(&item); acc } ) | |
.render()?) | |
} | |
} | |
impl<'a> Render for Args<'a> { | |
fn render(&self) -> Result<String> { | |
Ok((*self).files.render()?) | |
} | |
} |
$ cargo run -- vm nginx mysql -- examples/simple/*.yaml
examples/simple/controller.yaml:
controller:
hostname: mars
domain: example.com
email: [email protected]
pubkey: "~/.ssh/id_ed25519_{{controller.hostname}}.{{controller.domain}}"
examples/simple/hypervisor.yaml:
hypervisor:
hostname: terra
hypervisor: libvirt
network_bridge: br4300
network_mode: bridge
network_name: dmz
pool_name: volumes
storage_format: qcow2
url: "qemu+ssh://rgomes@{{hypervisor.hostname}}.{{controller.domain}}/system?keyfile={{controller.pubkey}}"
It renders to:
controller:
hostname: mars
domain: example.com
email: [email protected]
pubkey: "~/.ssh/id_ed25519_mars.example.com"
hypervisor:
hostname: terra
hypervisor: libvirt
network_bridge: br4300
network_mode: bridge
network_name: dmz
pool_name: volumes
storage_format: qcow2
url: qemu+ssh://[email protected]/system?keyfile=~/.ssh/id_ed25519_mars.example.com
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Cargo.toml: