Televiska / viska

SIP framework built in Rust
Other
88 stars 9 forks source link

add config from yaml/env #34

Open frgomes opened 2 years ago

frgomes commented 2 years ago

Hi! I saw this item in your notes:

Then I've decided to contribute some code which hopefully can be useful. It basically aggregates .yaml files and resolves references. The idea is simple: render a .yaml file in a loop until the result is equal to the input, meaning that all references are resolved.

#![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: me@example.com
  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: 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

I'm looking forward to give Viska a try soon! Thanks a lot :-)

vasilakisfil commented 2 years ago

Hi! Thanks for your time! I am indeed going to take a look on adding a proper config in viska soon, and you code will be very useful even as inspiration.