use crate as cindy;
#[crate::wire]
pub struct Inventory<V> {
pub hosts: Vec<Host<V>>,
}
pub const WHOLE_INVENTORY_KEY: &str = "__cindy:all";
pub fn collect_sealed_vaults(
value: &serde_json::Value,
out: &mut std::collections::BTreeSet<String>,
) {
match value {
serde_json::Value::Object(map) => {
if let (Some(serde_json::Value::String(vault)), Some(serde_json::Value::String(_))) =
(map.get("vault"), map.get("ciphertext"))
{
out.insert(vault.clone());
}
for v in map.values() {
collect_sealed_vaults(v, out);
}
}
serde_json::Value::Array(items) => {
for v in items {
collect_sealed_vaults(v, out);
}
}
_ => {}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(crate = "cindy::__reexports::serde")]
#[doc(hidden)]
pub struct InventoryDump {
pub json: serde_json::Value,
pub debug: std::collections::BTreeMap<String, String>,
#[serde(default)]
pub json_revealed: Option<Result<serde_json::Value, String>>,
}
#[doc(hidden)]
pub trait IntoInventoryDump {
fn into_inventory_dump(self) -> InventoryDump;
}
fn build_debug_map<V: std::fmt::Debug>(
inv: &Inventory<V>,
) -> std::collections::BTreeMap<String, String> {
let mut debug = std::collections::BTreeMap::new();
for host in &inv.hosts {
debug.insert(host.name.clone(), format!("{host:#?}"));
}
debug.insert(WHOLE_INVENTORY_KEY.to_owned(), format!("{inv:#?}"));
debug
}
impl<V: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug> IntoInventoryDump
for Inventory<V>
{
fn into_inventory_dump(self) -> InventoryDump {
let json = serde_json::to_value(&self).expect("Failed to serialise `Inventory<V>` to JSON");
if std::env::var_os("CINDY_REVEAL_SECRETS").is_some() {
crate::secret::with_revealed_secrets(|| {
let debug = build_debug_map(&self);
let json_revealed = serde_json::to_value(&self).map_err(|e| e.to_string());
InventoryDump {
json,
debug,
json_revealed: Some(json_revealed),
}
})
} else {
InventoryDump {
json,
debug: build_debug_map(&self),
json_revealed: None,
}
}
}
}
impl<V: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug, E: std::fmt::Debug>
IntoInventoryDump for ::std::result::Result<Inventory<V>, E>
{
fn into_inventory_dump(self) -> InventoryDump {
match self {
Ok(inv) => inv.into_inventory_dump(),
Err(e) => {
::std::eprintln!("`#[cindy::inventory]` failed: {e:?}");
::std::process::exit(1);
}
}
}
}
#[doc(hidden)]
pub struct RegisteredInventory {
pub dump: fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = InventoryDump> + Send>>,
}
inventory::collect!(RegisteredInventory);
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(crate = "cindy::__reexports::serde")]
pub struct Host<V = ()> {
pub name: String,
#[serde(default)]
pub tags: Vec<String>,
pub vars: V,
}
impl<V> Host<V> {
pub fn new(
name: impl Into<String>,
tags: impl IntoIterator<Item = impl Into<String>>,
vars: V,
) -> Self {
Self {
name: name.into(),
tags: tags.into_iter().map(Into::into).collect(),
vars,
}
}
}
impl<V> Inventory<V> {
pub fn new(hosts: impl IntoIterator<Item = Host<V>>) -> Self {
Self {
hosts: hosts.into_iter().collect(),
}
}
}
#[macro_export]
macro_rules! tags {
($($el:expr),* $(,)?) => {
vec![$(Into::<String>::into($el)),*]
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_and_inventory_constructors_accept_into() {
let a = Host::new("web-01", ["env:prod", "web"], 42u64);
assert_eq!(a.name, "web-01");
assert_eq!(a.tags, vec!["env:prod", "web"]);
assert_eq!(a.vars, 42);
let b = Host::new(String::from("web-02"), vec![String::from("db")], 7u64);
assert_eq!(b.name, "web-02");
let c = Host::new("web-03", std::iter::empty::<&str>(), 1u64);
assert!(c.tags.is_empty());
let d = Host {
name: "web-04".into(),
tags: crate::tags!["t"],
vars: 0u64,
};
let inv = Inventory::new([a, b, c, d]);
assert_eq!(inv.hosts.len(), 4);
}
}