use crate::core::types::{ResourceRunStatus, ResourceType, RunLogEntry, RunMeta};
use crate::transport::ExecOutput;
use std::path::{Path, PathBuf};
pub fn run_dir(state_dir: &Path, machine_name: &str, run_id: &str) -> PathBuf {
state_dir.join(machine_name).join("runs").join(run_id)
}
pub fn ensure_run_dir(dir: &Path, run_id: &str, machine_name: &str, command: &str) {
if dir.exists() {
return;
}
let _ = std::fs::create_dir_all(dir);
let mut meta = RunMeta::new(
run_id.to_string(),
machine_name.to_string(),
command.to_string(),
);
meta.started_at = Some(crate::tripwire::eventlog::now_iso8601());
let _ = serde_yaml_ng::to_string(&meta).map(|yaml| std::fs::write(dir.join("meta.yaml"), yaml));
}
#[derive(Debug, Clone, Copy)]
pub struct LogHeader<'a> {
pub resource_id: &'a str,
pub resource_type: &'a str,
pub action: &'a str,
pub machine_name: &'a str,
pub transport_type: &'a str,
}
pub fn capture_output(
run_dir: &Path,
header: &LogHeader<'_>,
script: &str,
output: &ExecOutput,
duration_secs: f64,
) {
let LogHeader {
resource_id,
resource_type,
action,
machine_name,
transport_type,
} = *header;
if !run_dir.exists() {
return;
}
let now = crate::tripwire::eventlog::now_iso8601();
let script_hash = if script.is_empty() {
String::new()
} else {
crate::tripwire::hasher::hash_string(script)
};
let entry = RunLogEntry {
resource_id: resource_id.to_string(),
resource_type: resource_type.to_string(),
action: action.to_string(),
machine: machine_name.to_string(),
transport: transport_type.to_string(),
script: script.to_string(),
script_hash,
stdout: output.stdout.clone(),
stderr: output.stderr.clone(),
exit_code: output.exit_code,
duration_secs,
started_at: now.clone(),
finished_at: now,
};
let log_content = entry.format_log();
let log_path = run_dir.join(format!("{resource_id}.{action}.log"));
let _ = std::fs::write(log_path, log_content);
let json_path = run_dir.join(format!("{resource_id}.{action}.json"));
let _ = std::fs::write(json_path, entry.format_json());
let script_path = run_dir.join(format!("{resource_id}.script"));
let _ = std::fs::write(script_path, script);
}
pub fn update_meta_resource(run_dir: &Path, resource_id: &str, status: ResourceRunStatus) {
let meta_path = run_dir.join("meta.yaml");
let mut meta = match std::fs::read_to_string(&meta_path) {
Ok(content) => serde_yaml_ng::from_str::<RunMeta>(&content)
.unwrap_or_else(|_| RunMeta::new("unknown".into(), "unknown".into(), "apply".into())),
Err(_) => return,
};
meta.record_resource(resource_id, status);
let _ = serde_yaml_ng::to_string(&meta).map(|yaml| std::fs::write(&meta_path, yaml));
let _ = serde_json::to_string_pretty(&meta)
.map(|json| std::fs::write(run_dir.join("meta.json"), json));
}
pub struct RunSlot<'a> {
pub state_dir: &'a Path,
pub machine_name: &'a str,
pub run_id: Option<&'a str>,
}
impl<'a> RunSlot<'a> {
pub fn new(state_dir: &'a Path, machine_name: &'a str, run_id: Option<&'a str>) -> Self {
Self {
state_dir,
machine_name,
run_id,
}
}
}
pub struct Transcript {
pub secrets: Vec<String>,
pub suppress: bool,
}
impl Transcript {
pub fn for_resource(
resource: &crate::core::types::Resource,
secrets: &crate::core::types::SecretsConfig,
) -> Self {
Self {
secrets: crate::core::resolver::collect_secret_values(resource, secrets),
suppress: resource.sensitive || crate::core::resolver::carries_ciphertext(resource),
}
}
pub fn unrestricted() -> Self {
Self {
secrets: Vec::new(),
suppress: false,
}
}
fn redact(&self, text: &str) -> String {
crate::core::resolver::redact_transcript(text, &self.secrets)
}
}
pub struct Executed<'a> {
pub resource_id: &'a str,
pub resource_type: &'a ResourceType,
pub action: &'a str,
pub script: &'a str,
pub transcript: Transcript,
}
pub fn capture_exec_output(
slot: &RunSlot,
executed: &Executed,
output: &ExecOutput,
duration_secs: f64,
) -> Option<PathBuf> {
let (state_dir, machine_name, run_id) = (slot.state_dir, slot.machine_name, slot.run_id);
let (resource_id, resource_type, action) = (
executed.resource_id,
executed.resource_type,
executed.action,
);
let script = executed.script;
let rid = run_id.unwrap_or("run-adhoc");
let dir = run_dir(state_dir, machine_name, rid);
ensure_run_dir(&dir, rid, machine_name, "apply");
let transcript = &executed.transcript;
if transcript.suppress {
return None;
}
let script = transcript.redact(script);
let redacted;
let output = if transcript.secrets.is_empty() {
output
} else {
redacted = ExecOutput {
exit_code: output.exit_code,
stdout: transcript.redact(&output.stdout),
stderr: transcript.redact(&output.stderr),
};
&redacted
};
let rt = format!("{resource_type:?}").to_lowercase();
capture_output(
&dir,
&LogHeader {
resource_id,
resource_type: &rt,
action,
machine_name,
transport_type: "transport",
},
&script,
output,
duration_secs,
);
let log = dir.join(format!("{resource_id}.{action}.log"));
log.exists().then_some(log)
}