ktmpl 0.9.1

Parameterized templates for Kubernetes manifests.
Documentation
use std::collections::HashMap;
use std::fs::File;
use std::io::{stdin, ErrorKind, Read};
use std::process::exit;

use clap::{App, AppSettings, Arg, Values};

use ktmpl::{
    parameter_values_from_file,
    ParameterValue,
    ParameterValues,
    Secret,
    Secrets,
    Template,
};

fn main() {
    if let Err(error) = real_main() {
        println!("Error: {}", error);

        exit(1);
    }
}

fn real_main() -> Result<(), String> {
    let matches = App::new("ktmpl")
        .version(env!("CARGO_PKG_VERSION"))
        .about("Produces a Kubernetes manifest from a parameterized template")
        .setting(AppSettings::ArgRequiredElseHelp)
        .setting(AppSettings::AllowLeadingHyphen)
        .arg(
            Arg::with_name("template")
                .help("Path to the template file to be processed (use \"-\" to read from stdin)")
                .required(true)
                .index(1),
        )
        .arg(
            Arg::with_name("parameter")
                .help("Supplies a value for the named parameter")
                .next_line_help(true)
                .long("parameter")
                .short("p")
                .multiple(true)
                .takes_value(true)
                .number_of_values(2)
                .value_names(&["NAME", "VALUE"]),
        )
        .arg(
            Arg::with_name("base64-parameter")
                .help("Same as --parameter, but for values already encoded in Base64")
                .next_line_help(true)
                .long("base64-parameter")
                .short("b")
                .multiple(true)
                .takes_value(true)
                .number_of_values(2)
                .value_names(&["NAME", "VALUE"]),
        )
        .arg(
            Arg::with_name("secret")
                .help("A secret to Base64 encode after parameter interpolation")
                .next_line_help(true)
                .long("secret")
                .short("s")
                .multiple(true)
                .takes_value(true)
                .number_of_values(2)
                .value_names(&["NAME", "NAMESPACE"]),
        )
        .arg(
            Arg::with_name("parameter-file")
                .help("Path to a YAML file with parameter values")
                .next_line_help(true)
                .long("parameter-file")
                .short("f")
                .multiple(true)
                .takes_value(true)
                .number_of_values(1)
                .value_names(&["PARAMETER_FILE"]),
        )
        .get_matches();

    let mut values = HashMap::new();

    if let Some(files) = matches.values_of("parameter-file") {
        let params_from_file = parameter_files(files)?;

        values.extend(params_from_file);
    }

    if let Some(parameters) = matches.values_of("parameter") {
        values.extend(parameter_values(parameters, false));
    }

    if let Some(parameters) = matches.values_of("base64-parameter") {
        let encoded_values = parameter_values(parameters, true);

        values.extend(encoded_values);
    }

    let secrets = matches
        .values_of("secret")
        .and_then(|secrets| Some(secret_values(secrets)))
        .or(None);

    let filename = matches
        .value_of("template")
        .expect("template wasn't provided");
    let mut template_data = String::new();

    if filename == "-" {
        stdin()
            .read_to_string(&mut template_data)
            .map_err(|err| err.to_string())?;
    } else {
        let mut file = File::open(filename).map_err(|err| match err.kind() {
            ErrorKind::NotFound => format!("File not found: {}", filename),
            _ => err.to_string(),
        })?;
        file.read_to_string(&mut template_data)
            .map_err(|err| err.to_string())?;
    }

    let template = Template::new(template_data, values, secrets)?;

    match template.process() {
        Ok(manifests) => {
            println!("{}", manifests);

            Ok(())
        }
        Err(error) => Err(error),
    }
}

fn parameter_files(mut param_files: Values) -> Result<ParameterValues, String> {
    let mut parameter_values = ParameterValues::new();

    loop {
        if let Some(filename) = param_files.next() {
            let values = parameter_values_from_file(&filename)?;

            parameter_values.extend(values);
        } else {
            break;
        }
    }

    Ok(parameter_values)
}

fn parameter_values(mut parameters: Values, base64_encoded: bool) -> ParameterValues {
    let mut parameter_values = ParameterValues::new();

    loop {
        if let Some(name) = parameters.next() {
            let value = parameters.next().expect("Parameter was missing its value.");

            let parameter_value = if base64_encoded {
                ParameterValue::Encoded(value.to_string())
            } else {
                ParameterValue::Plain(value.to_string())
            };

            parameter_values.insert(name.to_string(), parameter_value);
        } else {
            break;
        }
    }

    parameter_values
}

fn secret_values(mut secret_parameters: Values) -> Secrets {
    let mut secrets = Secrets::new();

    loop {
        if let Some(name) = secret_parameters.next() {
            let namespace = secret_parameters
                .next()
                .expect("Secret was missing its namespace.");

            secrets.insert(Secret {
                name: name.to_string(),
                namespace: namespace.to_string(),
            });
        } else {
            break;
        }
    }

    secrets
}