forjar 1.11.0

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
use super::template::resolve_template_with_secrets;
use crate::core::types::*;
use std::collections::HashMap;

/// Resolve a single optional string field through the template engine.
fn resolve_opt(
    field: &Option<String>,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> Result<Option<String>, String> {
    match field {
        Some(val) => Ok(Some(resolve_template_with_secrets(
            val, params, machines, secrets,
        )?)),
        None => Ok(None),
    }
}

/// Resolve a list of strings through the template engine.
fn resolve_list(
    items: &[String],
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> Result<Vec<String>, String> {
    items
        .iter()
        .map(|s| resolve_template_with_secrets(s, params, machines, secrets))
        .collect()
}

/// Resolve core string fields (path, content, ownership, etc.).
fn resolve_core_fields(
    r: &mut Resource,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> Result<(), String> {
    r.content = resolve_opt(&r.content, params, machines, secrets)?;
    r.source = resolve_opt(&r.source, params, machines, secrets)?;
    r.path = resolve_opt(&r.path, params, machines, secrets)?;
    r.target = resolve_opt(&r.target, params, machines, secrets)?;
    r.owner = resolve_opt(&r.owner, params, machines, secrets)?;
    r.group = resolve_opt(&r.group, params, machines, secrets)?;
    r.mode = resolve_opt(&r.mode, params, machines, secrets)?;
    r.name = resolve_opt(&r.name, params, machines, secrets)?;
    r.options = resolve_opt(&r.options, params, machines, secrets)?;
    r.command = resolve_opt(&r.command, params, machines, secrets)?;
    r.schedule = resolve_opt(&r.schedule, params, machines, secrets)?;
    r.port = resolve_opt(&r.port, params, machines, secrets)?;
    r.protocol = resolve_opt(&r.protocol, params, machines, secrets)?;
    r.action = resolve_opt(&r.action, params, machines, secrets)?;
    r.from_addr = resolve_opt(&r.from_addr, params, machines, secrets)?;
    r.image = resolve_opt(&r.image, params, machines, secrets)?;
    r.shell = resolve_opt(&r.shell, params, machines, secrets)?;
    r.home = resolve_opt(&r.home, params, machines, secrets)?;
    r.restart = resolve_opt(&r.restart, params, machines, secrets)?;
    r.version = resolve_opt(&r.version, params, machines, secrets)?;
    Ok(())
}

/// Resolve GPU, task, and extended string fields.
fn resolve_extended_fields(
    r: &mut Resource,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> Result<(), String> {
    // PMAT-039: GPU / ML model fields
    r.driver_version = resolve_opt(&r.driver_version, params, machines, secrets)?;
    r.cuda_version = resolve_opt(&r.cuda_version, params, machines, secrets)?;
    r.rocm_version = resolve_opt(&r.rocm_version, params, machines, secrets)?;
    r.gpu_backend = resolve_opt(&r.gpu_backend, params, machines, secrets)?;
    r.compute_mode = resolve_opt(&r.compute_mode, params, machines, secrets)?;

    // Task fields (ALB-027)
    r.working_dir = resolve_opt(&r.working_dir, params, machines, secrets)?;
    r.completion_check = resolve_opt(&r.completion_check, params, machines, secrets)?;
    r.pre_apply = resolve_opt(&r.pre_apply, params, machines, secrets)?;
    r.post_apply = resolve_opt(&r.post_apply, params, machines, secrets)?;
    r.script = resolve_opt(&r.script, params, machines, secrets)?;

    // github_release fields
    r.install_dir = resolve_opt(&r.install_dir, params, machines, secrets)?;
    r.repo = resolve_opt(&r.repo, params, machines, secrets)?;
    r.tag = resolve_opt(&r.tag, params, machines, secrets)?;
    r.asset_pattern = resolve_opt(&r.asset_pattern, params, machines, secrets)?;
    r.binary = resolve_opt(&r.binary, params, machines, secrets)?;
    r.build_machine = resolve_opt(&r.build_machine, params, machines, secrets)?;
    Ok(())
}

/// Resolve all templates in a resource's string fields.
pub fn resolve_resource_templates(
    resource: &Resource,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
) -> Result<Resource, String> {
    resolve_resource_templates_with_secrets(resource, params, machines, &SecretsConfig::default())
}

/// Resolve all templates with explicit secrets configuration.
pub fn resolve_resource_templates_with_secrets(
    resource: &Resource,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> Result<Resource, String> {
    let mut r = resource.clone();

    resolve_core_fields(&mut r, params, machines, secrets)?;
    resolve_extended_fields(&mut r, params, machines, secrets)?;

    r.ports = resolve_list(&r.ports, params, machines, secrets)?;
    r.environment = resolve_list(&r.environment, params, machines, secrets)?;
    r.volumes = resolve_list(&r.volumes, params, machines, secrets)?;
    r.packages = resolve_list(&r.packages, params, machines, secrets)?;
    r.output_artifacts = resolve_list(&r.output_artifacts, params, machines, secrets)?;

    Ok(r)
}

/// FJ-1500 (PMAT-197): Resolve a resource's templates, falling back to the
/// unresolved resource on error.
///
/// This is the ONE place that decision is made. It previously lived privately
/// in the planner, and the same bug was then reintroduced twice by callers who
/// passed raw `config.resources` straight through:
///
/// * the planner itself (FJ-154 / #19) — secret-bearing resources replanned as
///   a spurious `Update` forever, violating `f(f(x)) = f(x)`;
/// * `forjar drift` — ANY templated resource reported permanent false drift,
///   and because the apply-time drift gate is global, that blocked every
///   targeted apply fleet-wide (paiml/infra PMAT-196 had to work around it
///   with `--no-tripwire`);
/// * `forjar destroy` — generated destroy scripts containing literal
///   `{{params.*}}` paths.
///
/// Resolving with the SAME `SecretsConfig` the executor uses is load-bearing:
/// resolving with the default (env) provider makes the planner-computed
/// desired hash disagree with the executor-stored hash.
pub fn resolve_or_fallback(
    resource_id: &str,
    resource: &Resource,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> Resource {
    resolve_resource_templates_with_secrets(resource, params, machines, secrets).unwrap_or_else(
        |e| {
            eprintln!("warning: template resolution failed for {resource_id}: {e}");
            resource.clone()
        },
    )
}

/// Resolve every resource in a map. Use this anywhere a whole config is handed
/// to a consumer that compares against live machine state.
pub fn resolve_all(
    resources: &indexmap::IndexMap<String, Resource>,
    params: &HashMap<String, serde_yaml_ng::Value>,
    machines: &indexmap::IndexMap<String, Machine>,
    secrets: &SecretsConfig,
) -> indexmap::IndexMap<String, Resource> {
    resources
        .iter()
        .map(|(id, r)| {
            (
                id.clone(),
                resolve_or_fallback(id, r, params, machines, secrets),
            )
        })
        .collect()
}