diff --git a/lib/src/cli.rs b/lib/src/cli.rs index 76b4b88f..e289dd5d 100644 --- a/lib/src/cli.rs +++ b/lib/src/cli.rs @@ -99,6 +99,9 @@ pub(crate) struct SwitchOpts { /// Target image to use for the next boot. pub(crate) target: String, + + #[clap(long)] + pub(crate) stateroot: Option, } /// Options controlling rollback @@ -628,7 +631,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> { println!("No update available.") } else { let osname = booted_deployment.osname(); - crate::deploy::stage(sysroot, &osname, &fetched, &spec).await?; + crate::deploy::stage(sysroot, &osname, &osname, &fetched, &spec).await?; changed = true; if let Some(prev) = booted_image.as_ref() { if let Some(fetched_manifest) = fetched.get_manifest(repo)? { @@ -687,18 +690,42 @@ async fn switch(opts: SwitchOpts) -> Result<()> { let (booted_deployment, _deployments, host) = crate::status::get_status_require_booted(sysroot)?; + let (old_stateroot, stateroot) = { + let booted_osname = booted_deployment.osname(); + let stateroot = opts + .stateroot + .as_deref() + .unwrap_or_else(|| booted_osname.as_str()); + + (booted_osname.to_owned(), stateroot.to_owned()) + }; + let new_spec = { let mut new_spec = host.spec.clone(); new_spec.image = Some(target.clone()); new_spec }; - if new_spec == host.spec { - println!("Image specification is unchanged."); + if new_spec == host.spec && old_stateroot == stateroot { + // TODO: Should we really be confusing users with terms like "stateroot"? + println!( + "The currently running deployment in stateroot {stateroot} is already using this image" + ); return Ok(()); } let new_spec = RequiredHostSpec::from_spec(&new_spec)?; + if old_stateroot != stateroot { + let init_result = sysroot.init_osname(&stateroot, cancellable); + match init_result { + Ok(_) => {} + Err(err) => { + // TODO: Only ignore non already-exists errors + println!("Ignoring error creating new stateroot: {err}"); + } + } + } + let fetched = crate::deploy::pull(repo, &target, None, opts.quiet).await?; if !opts.retain { @@ -712,8 +739,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> { } } - let stateroot = booted_deployment.osname(); - crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec).await?; + crate::deploy::stage(sysroot, &old_stateroot, &stateroot, &fetched, &new_spec).await?; if opts.apply { crate::reboot::reboot()?; @@ -766,7 +792,7 @@ async fn edit(opts: EditOpts) -> Result<()> { // TODO gc old layers here let stateroot = booted_deployment.osname(); - crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec).await?; + crate::deploy::stage(sysroot, &stateroot, &stateroot, &fetched, &new_spec).await?; Ok(()) } diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs index cd1ef419..3c7a82bb 100644 --- a/lib/src/deploy.rs +++ b/lib/src/deploy.rs @@ -373,7 +373,6 @@ async fn deploy( image: &ImageState, origin: &glib::KeyFile, ) -> Result { - let stateroot = Some(stateroot); let mut opts = ostree::SysrootDeployTreeOpts::default(); // Compute the kernel argument overrides. In practice today this API is always expecting // a merge deployment. The kargs code also always looks at the booted root (which @@ -396,10 +395,12 @@ async fn deploy( let cancellable = gio::Cancellable::NONE; return sysroot .stage_tree_with_options( - stateroot, + Some(stateroot), image.ostree_commit.as_str(), Some(origin), - merge_deployment, + merge_deployment.filter(|merge_deployment| { + stateroot == ostree::Deployment::osname(merge_deployment) + }), &opts, cancellable, ) @@ -422,11 +423,12 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result { #[context("Staging")] pub(crate) async fn stage( sysroot: &Storage, + merge_stateroot: &str, stateroot: &str, image: &ImageState, spec: &RequiredHostSpec<'_>, ) -> Result<()> { - let merge_deployment = sysroot.merge_deployment(Some(stateroot)); + let merge_deployment = sysroot.merge_deployment(Some(merge_stateroot)); let origin = origin_from_imageref(spec.image)?; let deployment = crate::deploy::deploy( sysroot,