use crate::core::types::{ForjarConfig, OutputEquivalence, Resource};
pub const NO_EXEC_SECRET_PROVIDER: &str = "unattended-no-exec";
#[must_use]
pub fn provider_executes(provider: Option<&str>) -> bool {
matches!(provider, Some("sops" | "op"))
}
#[must_use]
pub fn sanitize_config(config: &ForjarConfig) -> (ForjarConfig, Vec<String>) {
let mut out = config.clone();
let mut skipped = Vec::new();
strip_secret_provider(&mut out, &mut skipped);
for (id, resource) in out.resources.iter_mut() {
strip_ambient_inputs(id, resource, &mut skipped);
strip_output_normalisers(id, resource, &mut skipped);
}
(out, skipped)
}
fn strip_secret_provider(config: &mut ForjarConfig, skipped: &mut Vec<String>) {
if !provider_executes(config.secrets.provider.as_deref()) {
return;
}
let name = config.secrets.provider.take().unwrap_or_default();
config.secrets.provider = Some(NO_EXEC_SECRET_PROVIDER.to_string());
skipped.push(format!(
"secrets: provider '{name}' not invoked; `{{{{secrets.*}}}}` left unresolved"
));
}
fn strip_ambient_inputs(id: &str, resource: &mut Resource, skipped: &mut Vec<String>) {
if resource.ambient_inputs.is_empty() {
return;
}
let n = resource.ambient_inputs.len();
resource.ambient_inputs.clear();
skipped.push(format!(
"{id}: {n} ambient_inputs command(s) not executed; staleness from ambient state not checked"
));
}
fn strip_output_normalisers(id: &str, resource: &mut Resource, skipped: &mut Vec<String>) {
let named: Vec<String> = resource
.output_equivalence
.iter()
.filter(|(_, rule)| matches!(rule, OutputEquivalence::Command(_)))
.map(|(artifact, _)| artifact.clone())
.collect();
for artifact in named {
resource
.output_equivalence
.insert(artifact.clone(), OutputEquivalence::None);
skipped.push(format!(
"{id}: output_equivalence normaliser for '{artifact}' not executed; \
its content is not compared"
));
}
}
#[must_use]
pub fn disclosure(skipped: &[String]) -> Option<String> {
if skipped.is_empty() {
return None;
}
Some(format!(
"this surface never executes what a config declares, so {} declaration(s) \
were skipped ({}); the plan is lock-relative for those — run `forjar plan` \
from a checkout you trust, or `forjar drift`, for what the machines \
actually hold",
skipped.len(),
skipped.join("; ")
))
}
#[must_use]
pub fn merge_disclosures(scope: Option<String>, unattended: Option<String>) -> Option<String> {
match (scope, unattended) {
(Some(a), Some(b)) => Some(format!("{a} Also: {b}.")),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
#[cfg(test)]
mod tests;