Compare commits

...

No commits in common. "b625498c54a61b242d7a7ac3fec44fc66d20b722" and "6d0bfeedaf08c2747944bb7c065e37c14ca5d78a" have entirely different histories.

8 changed files with 553 additions and 17 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
Cargo.lock -diff

203
Cargo.lock generated
View File

@ -44,6 +44,15 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -168,6 +177,33 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "derive-adhoc"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5283ac2881753c76c0892406705553f0d9ab30649f81e18964d3408f4501edb8"
dependencies = [
"derive-adhoc-macros",
"heck",
]
[[package]]
name = "derive-adhoc-macros"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c21b673a9b8c78c34908e6fcb42b922e11c4df2de5237f1c3f58d3285904a84b"
dependencies = [
"heck",
"itertools",
"proc-macro-crate",
"proc-macro2",
"quote",
"sha3",
"strum",
"syn 1.0.109",
"void",
]
[[package]] [[package]]
name = "deunicode" name = "deunicode"
version = "1.4.2" version = "1.4.2"
@ -182,8 +218,15 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -251,6 +294,12 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "humansize" name = "humansize"
version = "2.1.3" version = "2.1.3"
@ -309,6 +358,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.10" version = "1.0.10"
@ -324,6 +382,15 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "keccak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940"
dependencies = [
"cpufeatures",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -403,6 +470,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -440,7 +513,7 @@ dependencies = [
"pest_meta", "pest_meta",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -504,6 +577,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.76" version = "1.0.76"
@ -513,6 +596,18 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pwd-grp"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6955c41fd7e4283bdf6ff3e7218b7e3f8ef24c4236b31d22be050f4cfd5e2a2c"
dependencies = [
"derive-adhoc",
"libc",
"paste",
"thiserror",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.35"
@ -596,6 +691,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.16" version = "1.0.16"
@ -628,7 +729,7 @@ checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -666,6 +767,16 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -675,6 +786,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "0.3.11" version = "0.3.11"
@ -697,6 +814,45 @@ version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.48" version = "2.0.48"
@ -747,7 +903,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -765,15 +921,35 @@ name = "tmpl"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argparse", "argparse",
"blake2",
"file-mode", "file-mode",
"pwd-grp",
"serde", "serde",
"serde_yaml", "serde_yaml",
"shlex",
"tera", "tera",
"thiserror", "thiserror",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "toml_datetime"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.40"
@ -794,7 +970,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -922,6 +1098,12 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.4.0" version = "2.4.0"
@ -959,7 +1141,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -981,7 +1163,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.48",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -1145,3 +1327,12 @@ name = "windows_x86_64_msvc"
version = "0.52.0" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
dependencies = [
"memchr",
]

View File

@ -7,9 +7,12 @@ edition = "2021"
[dependencies] [dependencies]
argparse = "0.2.2" argparse = "0.2.2"
blake2 = "0.10.6"
file-mode = { version = "0.1.2", features = ["serde"] } file-mode = { version = "0.1.2", features = ["serde"] }
pwd-grp = "0.1.1"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_yaml = "0.9.30" serde_yaml = "0.9.30"
shlex = "1.2.0"
tera = "1.19.1" tera = "1.19.1"
thiserror = "1.0.56" thiserror = "1.0.56"
tracing = { version = "0.1.40", features = ["log"] } tracing = { version = "0.1.40", features = ["log"] }

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
max_width = 79

View File

@ -1,16 +1,27 @@
mod model; mod model;
mod templating;
use std::path::PathBuf; use std::collections::HashSet;
use std::io::Read; use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use argparse::{ArgumentParser, Store, StoreOption}; use argparse::{ArgumentParser, Store, StoreOption, StoreTrue};
use blake2::{Blake2b512, Digest};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_yaml::Value; use serde_yaml::Value;
use shlex::Shlex;
use tera::{Context, Tera}; use tera::{Context, Tera};
use tracing::error; use tracing::{debug, error, info, warn};
use model::Instructions; use model::Instructions;
macro_rules! joinargs {
($args:ident) => {
shlex::join($args.iter().map(|s| s.as_str()))
};
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
enum LoadError { enum LoadError {
#[error("{0}")] #[error("{0}")]
@ -19,6 +30,18 @@ enum LoadError {
Yaml(#[from] serde_yaml::Error), Yaml(#[from] serde_yaml::Error),
} }
#[derive(Debug, thiserror::Error)]
enum SetPermissionsError {
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("User not found: {0}")]
UserNotFound(String),
#[error("Group not found: {0}")]
GroupNotFound(String),
#[error("Bad mode string")]
BadMode,
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
enum ProcessInstructionsError { enum ProcessInstructionsError {
#[error("Error loading templates: {0}")] #[error("Error loading templates: {0}")]
@ -31,6 +54,7 @@ struct Args {
values: String, values: String,
templates: Option<String>, templates: Option<String>,
destdir: Option<PathBuf>, destdir: Option<PathBuf>,
skip_hooks: bool,
} }
fn parse_args(args: &mut Args) { fn parse_args(args: &mut Args) {
@ -53,6 +77,11 @@ fn parse_args(args: &mut Args) {
StoreOption, StoreOption,
"Destination directory", "Destination directory",
); );
parser.refer(&mut args.skip_hooks).add_option(
&["--skip-hooks", "-S"],
StoreTrue,
"Skip running hooks after rendering templates",
);
parser.parse_args_or_exit(); parser.parse_args_or_exit();
} }
@ -82,7 +111,14 @@ fn main() {
} }
}; };
let templates = args.templates.as_deref().unwrap_or("templates"); let templates = args.templates.as_deref().unwrap_or("templates");
if let Err(e) = process_instructions(templates, instructions, values) { let destdir = args.destdir.as_deref().unwrap_or(Path::new("/"));
if let Err(e) = process_instructions(
templates,
destdir,
instructions,
values,
args.skip_hooks,
) {
error!("Failed to process instructions: {}", e); error!("Failed to process instructions: {}", e);
std::process::exit(1); std::process::exit(1);
} }
@ -101,20 +137,223 @@ fn load_yaml<T: DeserializeOwned>(path: &str) -> Result<T, LoadError> {
fn process_instructions( fn process_instructions(
templates: &str, templates: &str,
destdir: impl AsRef<Path>,
instructions: Instructions, instructions: Instructions,
values: Value, values: Value,
skip_hooks: bool,
) -> Result<(), ProcessInstructionsError> { ) -> Result<(), ProcessInstructionsError> {
let tera = Tera::new(&format!("{}/**", templates))?; let mut tera = Tera::new(&format!("{}/**", templates))?;
tera.register_filter(
templating::decrypt::NAME,
templating::decrypt::DecryptFilter,
);
let ctx = Context::from_serialize(&values)?; let ctx = Context::from_serialize(&values)?;
let mut post_hooks = HashSet::new();
for i in instructions.render { for i in instructions.render {
match tera.render(&i.template, &ctx) { let out = match tera.render(&i.template, &ctx) {
Ok(o) => { Ok(o) => o,
print!("{}", o);
}
Err(e) => { Err(e) => {
error!("Failed to render template {}: {}", i.template, e); let msg = format_error(&e);
error!("Failed to render template {}: {}", i.template, msg);
continue;
} }
};
let mut dest = PathBuf::from(destdir.as_ref());
dest.push(i.dest.strip_prefix("/").unwrap_or(i.dest.as_path()));
let orig_cksm = checksum(&dest).ok();
if let Err(e) = write_file(&dest, out.as_bytes()) {
error!("Failed to write output file {}: {}", dest.display(), e);
continue;
}
let new_cksm = checksum(&dest).ok();
if orig_cksm != new_cksm {
info!("File {} was changed", dest.display());
if let Some(hooks) = i.hooks {
if let Some(changed) = hooks.changed {
for hook in changed {
if let Some(args) = parse_hook(&hook.run, &dest) {
if hook.immediate {
if !skip_hooks {
run_hook(args);
} else {
info!(
"Skipping hook: {}",
joinargs!(args)
);
}
} else {
post_hooks.insert(args);
}
}
}
}
}
} else {
debug!("File {} was NOT changed", dest.display());
}
if i.owner.is_some() || i.group.is_some() {
if let Err(e) =
chown(&dest, i.owner.as_deref(), i.group.as_deref())
{
error!("Failed to set ownership of {}: {}", dest.display(), e);
}
}
if let Some(mode) = i.mode {
if let Err(e) = chmod(&dest, &mode) {
error!("Failed to set mode of {}: {}", dest.display(), e);
}
}
}
for args in post_hooks {
if !skip_hooks {
run_hook(args);
} else {
info!("Skipping hook: {}", joinargs!(args));
} }
} }
Ok(()) Ok(())
} }
fn write_file(
dest: impl AsRef<Path>,
data: &[u8],
) -> Result<(), std::io::Error> {
if let Some(p) = dest.as_ref().parent() {
if !p.exists() {
info!("Creating directory {}", p.display());
std::fs::create_dir_all(p)?;
}
}
debug!("Writing output: {}", dest.as_ref().display());
let mut f = std::fs::File::create(&dest)?;
f.write_all(data)?;
let size = f.stream_position()?;
debug!("Wrote output: {} ({} bytes)", dest.as_ref().display(), size);
Ok(())
}
fn chown(
path: impl AsRef<Path>,
owner: Option<&str>,
group: Option<&str>,
) -> Result<(), SetPermissionsError> {
let uid = if let Some(owner) = owner {
debug!("Looking up UID for user {}", owner);
if let Some(pw) = pwd_grp::getpwnam(owner)? {
debug!("Found UID {} for user {}", pw.uid, owner);
Some(pw.uid)
} else {
return Err(SetPermissionsError::UserNotFound(owner.into()));
}
} else {
None
};
let gid = if let Some(group) = group {
debug!("Looking up GID for group {}", group);
if let Some(gr) = pwd_grp::getgrnam(group)? {
debug!("Found GID {} for group {}", gr.gid, group);
Some(gr.gid)
} else {
return Err(SetPermissionsError::GroupNotFound(group.into()));
}
} else {
None
};
debug!(
"Setting ownership of {} to {:?} / {:?}",
path.as_ref().display(),
uid,
gid
);
Ok(std::os::unix::fs::chown(path, uid, gid)?)
}
fn chmod(
path: impl AsRef<Path>,
mode: &str,
) -> Result<(), SetPermissionsError> {
let mut filemode = file_mode::Mode::empty();
filemode
.set_str(mode)
.map_err(|_| SetPermissionsError::BadMode)?;
debug!(
"Changing mode of {} to {:o}",
path.as_ref().display(),
filemode.mode()
);
let newmode = filemode.set_mode_path(&path)?;
info!("Set mode of {} to {:o}", path.as_ref().display(), newmode);
Ok(())
}
fn format_error(e: &dyn std::error::Error) -> String {
// TODO replace this with std::error::Error::sources when it is stablized.
// https://github.com/rust-lang/rust/issues/58520
let mut msg = e.to_string();
if let Some(e) = e.source() {
msg.push_str(&format!(": {}", format_error(e)));
}
msg
}
fn checksum(path: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
let mut f = std::fs::File::open(path)?;
let mut blake = Blake2b512::new();
loop {
let mut buf = vec![0u8; 16384];
match f.read_exact(&mut buf) {
Ok(_) => blake.update(buf),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
blake.update(buf);
break;
}
Err(e) => return Err(e),
}
}
Ok(blake.finalize().to_vec())
}
fn parse_hook(command: &str, path: &Path) -> Option<Vec<String>> {
let mut bad_path = false;
let args: Vec<_> = Shlex::new(command)
.map(|a| {
if a == "%s" {
if let Some(p) = path.as_os_str().to_str() {
p.into()
} else {
bad_path = true;
a
}
} else {
a
}
})
.collect();
if bad_path {
warn!("Cannot run hook: path is not valid UTF-8");
return None;
}
if args.is_empty() {
warn!(
"Invalid hook for {} ({}): empty argument list",
path.display(),
command
);
return None;
}
Some(args)
}
fn run_hook(args: Vec<String>) {
info!("Running hook: {}", joinargs!(args));
if let Err(e) = _run_hook(args) {
error!("Error running hook: {}", e);
}
}
fn _run_hook(args: Vec<String>) -> std::io::Result<()> {
Command::new(&args[0]).args(&args[1..]).spawn()?.wait()?;
Ok(())
}

View File

@ -2,6 +2,18 @@ use std::path::PathBuf;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Hook {
pub run: String,
#[serde(default)]
pub immediate: bool,
}
#[derive(Debug, Deserialize)]
pub struct Hooks {
pub changed: Option<Vec<Hook>>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct RenderInstruction { pub struct RenderInstruction {
pub template: String, pub template: String,
@ -9,6 +21,7 @@ pub struct RenderInstruction {
pub owner: Option<String>, pub owner: Option<String>,
pub group: Option<String>, pub group: Option<String>,
pub mode: Option<String>, pub mode: Option<String>,
pub hooks: Option<Hooks>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

87
src/templating/decrypt.rs Normal file
View File

@ -0,0 +1,87 @@
use std::collections::HashMap;
use std::io::{Read, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use tera::Value;
#[derive(Debug, thiserror::Error)]
pub(crate) enum DecryptError {
#[error("Decryption failed: {0}")]
Io(#[from] std::io::Error),
#[error("Decryption failed: {0}")]
DecryptFailed(String),
}
pub(crate) static NAME: &str = "decrypt";
pub(crate) struct DecryptFilter;
impl tera::Filter for DecryptFilter {
fn filter(
&self,
value: &Value,
args: &HashMap<String, Value>,
) -> tera::Result<Value> {
let data = match value {
Value::String(s) => s.clone().into_bytes(),
_ => {
return Err(tera::Error::msg(
"Can only decrypt string values",
));
}
};
let identity = match args.get("identity") {
Some(Value::String(s)) => s,
Some(_) => {
return Err(tera::Error::msg("identity must be string"));
}
None => "keys.txt",
};
let decrypted = Self::decrypt(&data, identity)
.map_err(|e| tera::Error::chain("Decryption failed", e))?;
Ok(Value::String(
String::from_utf8(decrypted)
.map_err(|_| tera::Error::msg("Invalid UTF-8 string"))?,
))
}
fn is_safe(&self) -> bool {
false
}
}
impl DecryptFilter {
fn decrypt(
data: &[u8],
identity: impl AsRef<Path>,
) -> Result<Vec<u8>, DecryptError> {
let mut cmd = Command::new("age")
.arg("-d")
.arg("-i")
.arg(identity.as_ref())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = cmd.stdin.take() {
stdin.write_all(data)?;
drop(stdin);
}
let mut decrypted = vec![];
if let Some(stdout) = cmd.stdout.as_mut() {
stdout.read_to_end(&mut decrypted)?;
}
let status = cmd.wait()?;
if status.success() {
Ok(decrypted)
} else {
let mut error = vec![];
if let Some(stderr) = cmd.stderr.as_mut() {
let _ = stderr.read_to_end(&mut error);
}
let msg = String::from_utf8_lossy(&error[..]).trim_end().into();
Err(DecryptError::DecryptFailed(msg))
}
}
}

1
src/templating/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod decrypt;