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.
This commit is contained in:
Marco Allegretti 2026-02-24 11:46:16 +01:00
parent b73fd9eaec
commit 7278d9923d
2 changed files with 92 additions and 12 deletions

View file

@ -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<u8, String> {
@ -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)

View file

@ -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() {