use anyhow::Context;
use trusty_agents_common::agent_content::REMEDY;
use trusty_mpm::content::bundle_cache::{
UpdateAction, UpdateOutcome, github_source, install_from_file, update,
};
use trusty_mpm::content::status::{ContentStatus, content_status};
use crate::cli::ContentAction;
pub(crate) async fn run(action: ContentAction) -> anyhow::Result<()> {
tokio::task::spawn_blocking(move || run_blocking(action))
.await
.context("the tm content task did not complete")?
}
fn run_blocking(action: ContentAction) -> anyhow::Result<()> {
let cache = trusty_common::content::default_cache_dir()
.context("no home directory resolves, so the content cache cannot be located")?;
match action {
ContentAction::Install { from } => {
print_outcome(&install_from_file(&cache, &from)?);
}
ContentAction::Update { content_ref } => {
let source = github_source()
.map_err(|e| anyhow::anyhow!("could not build an HTTP client: {}", e.reason))?;
print_outcome(&update(&cache, &source, content_ref.as_deref())?);
}
ContentAction::Status => {
let cwd = std::env::current_dir().ok();
let status = content_status(&cache, cwd.as_deref());
let (lines, verdict) = status_report(&status);
for line in lines {
println!("{line}");
}
verdict?;
}
}
Ok(())
}
fn status_report(status: &ContentStatus) -> (Vec<String>, anyhow::Result<()>) {
let lines = status.lines();
let verdict = if status.exits_ok() {
Ok(())
} else {
Err(anyhow::anyhow!("no verified content source; {REMEDY}"))
};
(lines, verdict)
}
fn print_outcome(outcome: &UpdateOutcome) {
let verb = match outcome.action {
UpdateAction::Installed => "pinned",
UpdateAction::Repaired => "restored",
UpdateAction::AlreadyCurrent => "already pinned and verified:",
};
println!("{verb} {} (sha256 {})", outcome.tag, outcome.sha256);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_report_exits_non_zero_with_nothing_installed() {
let cache = tempfile::tempdir().expect("tempdir");
let status = content_status(cache.path(), None);
let (lines, verdict) = status_report(&status);
let err = verdict.expect_err("no source serves");
assert!(err.to_string().contains(REMEDY), "{err}");
assert!(!lines.iter().any(|l| l.starts_with("info: ")), "{lines:?}");
let lock = cache.path().join(trusty_common::content::LOCK_FILE_NAME);
std::fs::write(&lock, "not = [valid").expect("write lock");
let broken = content_status(cache.path(), None);
let (lines, verdict) = status_report(&broken);
assert!(verdict.is_err(), "a broken lock must not exit 0: {lines:?}");
assert!(!lines.iter().any(|l| l.starts_with("info: ")), "{lines:?}");
}
#[test]
fn update_help_limits_the_pin_check_to_the_current_pin() {
use clap::CommandFactory as _;
let mut root = crate::cli::Cli::command();
let help = root
.find_subcommand_mut("content")
.and_then(|c| c.find_subcommand_mut("update"))
.expect("content update")
.render_long_help()
.to_string();
assert!(help.contains("currently pinned tag"), "{help}");
assert!(help.contains("Only the current pin is checked"), "{help}");
assert!(!help.contains("tag republished"), "{help}");
}
#[test]
fn update_help_names_the_offline_switch() {
use clap::CommandFactory as _;
let mut root = crate::cli::Cli::command();
let help = root
.find_subcommand_mut("content")
.and_then(|c| c.find_subcommand_mut("update"))
.expect("content update")
.render_long_help()
.to_string();
assert!(help.contains("TRUSTY_CONTENT_OFFLINE=1"), "{help}");
assert!(help.contains("first use"), "{help}");
}
}