Skip to content

Commit 6a4bbf2

Browse files
feat(vz-cli): add vm patch apply --image workflow
1 parent 8135713 commit 6a4bbf2

3 files changed

Lines changed: 114 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ vz vm patch create \
149149
--signing-key /tmp/vz-patch-signing-key.pem
150150

151151
vz vm patch verify --bundle /tmp/patch-1.vzpatch
152-
sudo vz vm patch apply --bundle /tmp/patch-1.vzpatch --root /tmp/mounted-root
152+
sudo vz vm patch apply --bundle /tmp/patch-1.vzpatch --image ~/.vz/images/base.img
153153
```
154154

155155
For advanced CI workflows, `vz vm patch create` also supports `--operations <json>` + `--payload-dir <dir>`.

crates/vz-cli/src/commands/vm_patch.rs

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,12 @@ pub struct ApplyArgs {
138138
pub bundle: PathBuf,
139139

140140
/// Mounted root path to apply operations under.
141-
#[arg(long)]
142-
pub root: PathBuf,
141+
#[arg(long, conflicts_with = "image", required_unless_present = "image")]
142+
pub root: Option<PathBuf>,
143+
144+
/// Raw VM disk image path to mount/apply/detach automatically.
145+
#[arg(long, conflicts_with = "root", required_unless_present = "root")]
146+
pub image: Option<PathBuf>,
143147
}
144148

145149
/// Typed bundle manifest.
@@ -923,10 +927,62 @@ fn patch_state_path() -> PathBuf {
923927
}
924928

925929
fn apply_with_state_path(args: ApplyArgs, patch_state_path: &Path) -> anyhow::Result<()> {
926-
let manifest = verify_bundle(&args.bundle)?;
930+
match (args.root.as_ref(), args.image.as_ref()) {
931+
(Some(root), None) => apply_with_root(&args.bundle, root, patch_state_path),
932+
(None, Some(image)) => apply_with_image(&args.bundle, image, patch_state_path),
933+
_ => bail!("exactly one apply target is required: --root <path> or --image <path>"),
934+
}
935+
}
936+
937+
fn apply_with_image(bundle: &Path, image: &Path, patch_state_path: &Path) -> anyhow::Result<()> {
938+
let image = expand_home(image);
939+
if !image.exists() {
940+
bail!("disk image not found: {}", image.display());
941+
}
942+
943+
let manifest = verify_bundle(bundle)?;
927944
validate_patch_target_base_policy(&manifest)?;
928-
let root = fs::canonicalize(&args.root)
929-
.with_context(|| format!("failed to resolve apply root {}", args.root.display()))?;
945+
super::vm_base::verify_image_for_base_id(&image, &manifest.target_base_id).with_context(
946+
|| {
947+
format!(
948+
"pinned base verification failed before applying patch to image {}",
949+
image.display()
950+
)
951+
},
952+
)?;
953+
954+
let disk = crate::provision::attach_and_mount(&image).with_context(|| {
955+
format!(
956+
"failed to attach and mount image {} before patch apply",
957+
image.display()
958+
)
959+
})?;
960+
961+
let result =
962+
apply_verified_manifest_with_root(manifest, bundle, &disk.mount_point, patch_state_path);
963+
let detach_result = disk.detach();
964+
965+
result?;
966+
detach_result?;
967+
968+
println!("Patch apply completed for image {}", image.display());
969+
Ok(())
970+
}
971+
972+
fn apply_with_root(bundle: &Path, root: &Path, patch_state_path: &Path) -> anyhow::Result<()> {
973+
let manifest = verify_bundle(bundle)?;
974+
validate_patch_target_base_policy(&manifest)?;
975+
apply_verified_manifest_with_root(manifest, bundle, root, patch_state_path)
976+
}
977+
978+
fn apply_verified_manifest_with_root(
979+
manifest: PatchBundleManifest,
980+
bundle: &Path,
981+
root_arg: &Path,
982+
patch_state_path: &Path,
983+
) -> anyhow::Result<()> {
984+
let root = fs::canonicalize(root_arg)
985+
.with_context(|| format!("failed to resolve apply root {}", root_arg.display()))?;
930986
if !root.is_dir() {
931987
bail!("apply root {} is not a directory", root.display());
932988
}
@@ -953,7 +1009,7 @@ fn apply_with_state_path(args: ApplyArgs, patch_state_path: &Path) -> anyhow::Re
9531009
);
9541010
}
9551011

956-
let paths = BundlePaths::from_bundle_dir(&args.bundle);
1012+
let paths = BundlePaths::from_bundle_dir(bundle);
9571013
let payload_by_digest = load_payload_archive(&paths.payload)?;
9581014
validate_apply_preflight(&manifest, &payload_by_digest)?;
9591015
apply_operations_transactional(&root, &manifest, &payload_by_digest)?;
@@ -1233,6 +1289,16 @@ fn decode_pem_private_key(raw: &[u8]) -> anyhow::Result<Vec<u8>> {
12331289
.context("private key PEM body is not valid base64")
12341290
}
12351291

1292+
fn expand_home(path: &Path) -> PathBuf {
1293+
let s = path.to_string_lossy();
1294+
if s.starts_with("~/") {
1295+
if let Ok(home) = std::env::var("HOME") {
1296+
return PathBuf::from(format!("{}{}", home, &s[1..]));
1297+
}
1298+
}
1299+
path.to_path_buf()
1300+
}
1301+
12361302
fn verify_bundle(bundle_dir: &Path) -> anyhow::Result<PatchBundleManifest> {
12371303
let paths = BundlePaths::from_bundle_dir(bundle_dir);
12381304

@@ -2257,7 +2323,8 @@ mod tests {
22572323
apply_with_state_path(
22582324
ApplyArgs {
22592325
bundle: bundle.to_path_buf(),
2260-
root: root.to_path_buf(),
2326+
root: Some(root.to_path_buf()),
2327+
image: None,
22612328
},
22622329
patch_state_path,
22632330
)

crates/vz-cli/src/main.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,45 @@ mod tests {
702702
commands::vm::VmCommand::Patch(patch_args) => match patch_args.action {
703703
commands::vm_patch::VmPatchCommand::Apply(apply) => {
704704
assert_eq!(apply.bundle, PathBuf::from("/tmp/patch-bundle.vzpatch"));
705-
assert_eq!(apply.root, PathBuf::from("/tmp/mounted-root"));
705+
assert_eq!(
706+
apply.root.as_ref(),
707+
Some(&PathBuf::from("/tmp/mounted-root"))
708+
);
709+
assert!(apply.image.is_none());
710+
}
711+
other => panic!("unexpected vm patch action: {other:?}"),
712+
},
713+
other => panic!("unexpected vm command: {other:?}"),
714+
},
715+
other => panic!("unexpected command: {other:?}"),
716+
}
717+
}
718+
719+
#[cfg(target_os = "macos")]
720+
#[test]
721+
fn parse_vm_patch_apply_image_target() {
722+
let cli = Cli::try_parse_from([
723+
"vz",
724+
"vm",
725+
"patch",
726+
"apply",
727+
"--bundle",
728+
"/tmp/patch-bundle.vzpatch",
729+
"--image",
730+
"~/.vz/images/base.img",
731+
])
732+
.expect("parse");
733+
734+
match cli.command {
735+
Commands::Vm(args) => match args.action {
736+
commands::vm::VmCommand::Patch(patch_args) => match patch_args.action {
737+
commands::vm_patch::VmPatchCommand::Apply(apply) => {
738+
assert_eq!(apply.bundle, PathBuf::from("/tmp/patch-bundle.vzpatch"));
739+
assert_eq!(
740+
apply.image.as_ref(),
741+
Some(&PathBuf::from("~/.vz/images/base.img"))
742+
);
743+
assert!(apply.root.is_none());
706744
}
707745
other => panic!("unexpected vm patch action: {other:?}"),
708746
},

0 commit comments

Comments
 (0)