use std::collections::BTreeMap;
use isoloom_manifest::{Delivery, NonEmpty, StepSelection, TfVarShape};
use super::Cx;
use crate::secrets::SecretValue;
pub(crate) fn step_env(cx: &Cx<'_>, id: &str) -> Vec<(String, String)> {
let valued = valued(cx);
let mut env = delivered(&valued, id);
env.extend(tf_vars(&valued));
env
}
pub(crate) fn env_deliveries(cx: &Cx<'_>, id: &str) -> Vec<(String, String)> {
delivered(&valued(cx), id)
}
fn delivered(valued: &[(&str, String, &NonEmpty<Delivery>)], id: &str) -> Vec<(String, String)> {
valued
.iter()
.flat_map(|(_, value, deliveries)| env_vars(deliveries.iter(), id).map(move |var| (var, value.clone())))
.collect()
}
pub(crate) fn every_delivery(manifest: &isoloom_manifest::Manifest) -> impl Iterator<Item = &Delivery> {
let params = manifest.params.iter().flat_map(|p| p.deliver.iter());
params.chain(manifest.secrets.iter().flat_map(|s| s.deliver.iter()))
}
pub(super) fn valued<'a>(cx: &'a Cx<'_>) -> Vec<(&'a str, String, &'a NonEmpty<Delivery>)> {
let manifest = cx.lab.manifest();
let params = manifest
.params
.iter()
.filter(|p| cx.scope.admits(&p.hosts, &p.providers))
.filter_map(|p| Some((p.name.as_str(), cx.vars.params().get(p.name.as_str())?.clone(), &p.deliver)));
let every_host = std::collections::BTreeSet::new();
let slots = manifest
.secrets
.iter()
.filter(move |s| cx.scope.admits(&every_host, &s.providers))
.filter_map(|s| {
let given = cx.secrets.get(s.name.as_str()).map(SecretValue::expose);
Some((s.name.as_str(), given.or(s.default.as_deref())?.to_owned(), &s.deliver))
});
params.chain(slots).collect()
}
fn tf_vars(valued: &[(&str, String, &NonEmpty<Delivery>)]) -> Vec<(String, String)> {
let mut plain: Vec<(String, String)> = Vec::new();
let mut maps: BTreeMap<String, serde_json::Map<String, serde_json::Value>> = BTreeMap::new();
for (name, value, deliveries) in valued {
for d in *deliveries {
match d {
Delivery::TfVar {
var,
shape: TfVarShape::String,
} => plain.push((format!("TF_VAR_{var}"), value.clone())),
Delivery::TfVar {
var,
shape: TfVarShape::MapEntry,
} => {
maps.entry(var.to_string())
.or_default()
.insert((*name).to_owned(), serde_json::Value::String(value.clone()));
}
_ => {}
}
}
}
let objects = maps
.into_iter()
.map(|(var, o)| (format!("TF_VAR_{var}"), serde_json::Value::Object(o).to_string()));
plain.into_iter().chain(objects).collect()
}
fn env_vars<'a>(deliveries: impl Iterator<Item = &'a Delivery> + 'a, id: &'a str) -> impl Iterator<Item = String> + 'a {
deliveries.filter_map(move |d| match d {
Delivery::Env { var, steps } if reaches(steps, id) => Some(var.to_string()),
_ => None,
})
}
pub(crate) fn reaches(steps: &StepSelection, id: &str) -> bool {
match steps {
StepSelection::Every => true,
StepSelection::Only(ids) => ids.iter().any(|s| s.as_str() == id),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn deliveries(json: &str) -> NonEmpty<Delivery> {
serde_json::from_str(json).unwrap()
}
#[test]
fn a_string_variable_is_its_value_and_the_map_entries_of_one_make_one_json_object() {
let token = deliveries(r#"[{"via": "tf_var", "var": "api_token"}]"#);
let flag = deliveries(r#"[{"via": "tf_var", "var": "inputs", "shape": "map_entry"}]"#);
let user = deliveries(r#"[{"via": "tf_var", "var": "inputs", "shape": "map_entry"}, {"via": "env", "var": "USER_NAME", "steps": ["*"]}]"#);
let valued = [
("token", "t0k".to_owned(), &token),
("CTF_FLAG", "flag{x}".to_owned(), &flag),
("user", "ada".to_owned(), &user),
];
assert_eq!(
tf_vars(&valued),
[
("TF_VAR_api_token".to_owned(), "t0k".to_owned()),
("TF_VAR_inputs".to_owned(), r#"{"CTF_FLAG":"flag{x}","user":"ada"}"#.to_owned())
]
);
}
#[test]
fn a_variable_reaches_the_steps_it_names_or_every_one() {
let every: StepSelection = serde_json::from_str(r#"["*"]"#).unwrap();
assert!(reaches(&every, "up"));
let only: StepSelection = serde_json::from_str(r#"["up"]"#).unwrap();
assert!(reaches(&only, "up") && !reaches(&only, "down"));
}
}