Skip to content

Instantly share code, notes, and snippets.

@frgomes
Created January 29, 2022 18:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frgomes/95694f3494c940f1a5a78d09a071c101 to your computer and use it in GitHub Desktop.
Save frgomes/95694f3494c940f1a5a78d09a071c101 to your computer and use it in GitHub Desktop.
Rust - Render YAML resolving environment variables and YAML references.
#![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()?)
}
}
@frgomes
Copy link
Author

frgomes commented Jan 29, 2022

Cargo.toml:

[dependencies]
serde = { version = "^1.0", features = ["derive"] }
serde_yaml = "^0.8"
handlebars = { version = "^4.1", features = [] }
clap = { version = "^3.0", features = [] }
anyhow = "^1.0"

@frgomes
Copy link
Author

frgomes commented Jan 29, 2022

 $ cargo run -- vm nginx mysql -- examples/simple/*.yaml

@frgomes
Copy link
Author

frgomes commented Jan 29, 2022

examples/simple/controller.yaml:

controller:
 hostname: mars
 domain: example.com
 email: me@example.com
 pubkey: "~/.ssh/id_ed25519_{{controller.hostname}}.{{controller.domain}}"

@frgomes
Copy link
Author

frgomes commented Jan 29, 2022

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

@frgomes
Copy link
Author

frgomes commented Jan 29, 2022

It renders to:

controller:
  hostname: mars
  domain: example.com
  email: me@example.com
  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://rgomes@terra.example.com/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