From 7278d9923d23c7f6ae9c675a6211399066b52148 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Tue, 24 Feb 2026 11:46:16 +0100 Subject: [PATCH] cli: make snapshots output restorable The snapshots command printed a snapshot layer internal ID that restore cannot use. Compute and display the stored layer manifest hash so it can be copy/pasted into restore. JSON output now includes restore_hash. Add an integration test covering commit -> snapshots -> restore. --- crates/karapace-cli/src/commands/snapshots.rs | 25 +++--- crates/karapace-cli/tests/cli_integration.rs | 79 +++++++++++++++++++ 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/crates/karapace-cli/src/commands/snapshots.rs b/crates/karapace-cli/src/commands/snapshots.rs index b741b49..460a230 100644 --- a/crates/karapace-cli/src/commands/snapshots.rs +++ b/crates/karapace-cli/src/commands/snapshots.rs @@ -1,6 +1,6 @@ use super::{json_pretty, resolve_env_id, resolve_env_id_pretty, EXIT_SUCCESS}; use karapace_core::Engine; -use karapace_store::StoreLayout; +use karapace_store::{LayerStore, StoreLayout}; use std::path::Path; pub fn run(engine: &Engine, store_path: &Path, env_id: &str, json: bool) -> Result { @@ -16,16 +16,16 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str, json: bool) -> Resu .map_err(|e| e.to_string())?; if json { - let entries: Vec<_> = snapshots - .iter() - .map(|s| { - serde_json::json!({ - "hash": s.hash, - "tar_hash": s.tar_hash, - "parent": s.parent, - }) - }) - .collect(); + let mut entries = Vec::new(); + for s in &snapshots { + let restore_hash = LayerStore::compute_hash(s).map_err(|e| e.to_string())?; + entries.push(serde_json::json!({ + "hash": s.hash, + "restore_hash": restore_hash, + "tar_hash": s.tar_hash, + "parent": s.parent, + })); + } let payload = serde_json::json!({ "env_id": resolved, "snapshots": entries, @@ -36,7 +36,8 @@ pub fn run(engine: &Engine, store_path: &Path, env_id: &str, json: bool) -> Resu } else { println!("snapshots for {env_id}:"); for s in &snapshots { - println!(" {} (tar: {})", &s.hash[..12], &s.tar_hash[..12]); + let restore_hash = LayerStore::compute_hash(s).map_err(|e| e.to_string())?; + println!(" {} (tar: {})", restore_hash, &s.tar_hash[..12]); } } Ok(EXIT_SUCCESS) diff --git a/crates/karapace-cli/tests/cli_integration.rs b/crates/karapace-cli/tests/cli_integration.rs index abe5f80..c620dea 100644 --- a/crates/karapace-cli/tests/cli_integration.rs +++ b/crates/karapace-cli/tests/cli_integration.rs @@ -178,6 +178,85 @@ fn cli_build_offline_fails_fast_with_packages() { ); } +#[test] +fn cli_snapshots_restore_hash_matches_commit() { + let store = temp_store(); + let project = tempfile::tempdir().unwrap(); + let manifest = write_minimal_manifest(project.path(), "rolling"); + + let build_out = karapace_bin() + .args([ + "--store", + &store.path().to_string_lossy(), + "--json", + "build", + &manifest.to_string_lossy(), + "--name", + "demo", + ]) + .output() + .unwrap(); + assert!(build_out.status.success()); + + let commit_out = karapace_bin() + .args([ + "--store", + &store.path().to_string_lossy(), + "--json", + "commit", + "demo", + ]) + .output() + .unwrap(); + assert!( + commit_out.status.success(), + "commit must exit 0. stderr: {}", + String::from_utf8_lossy(&commit_out.stderr) + ); + let commit_stdout = String::from_utf8_lossy(&commit_out.stdout); + let commit_json: serde_json::Value = serde_json::from_str(&commit_stdout) + .unwrap_or_else(|e| panic!("commit --json must produce valid JSON: {e}\n{commit_stdout}")); + let commit_hash = commit_json["snapshot_hash"].as_str().unwrap().to_owned(); + + let snaps_out = karapace_bin() + .args([ + "--store", + &store.path().to_string_lossy(), + "--json", + "snapshots", + "demo", + ]) + .output() + .unwrap(); + assert!( + snaps_out.status.success(), + "snapshots must exit 0. stderr: {}", + String::from_utf8_lossy(&snaps_out.stderr) + ); + let snaps_stdout = String::from_utf8_lossy(&snaps_out.stdout); + let snaps_json: serde_json::Value = serde_json::from_str(&snaps_stdout).unwrap_or_else(|e| { + panic!("snapshots --json must produce valid JSON: {e}\nstdout: {snaps_stdout}") + }); + let restore_hash = snaps_json["snapshots"][0]["restore_hash"].as_str().unwrap(); + assert_eq!(restore_hash, commit_hash); + + let restore_out = karapace_bin() + .args([ + "--store", + &store.path().to_string_lossy(), + "restore", + "demo", + restore_hash, + ]) + .output() + .unwrap(); + assert!( + restore_out.status.success(), + "restore must exit 0. stderr: {}", + String::from_utf8_lossy(&restore_out.stderr) + ); +} + // A5: CLI Validation — list with JSON output #[test] fn cli_list_json_output_stable() {