Skip to content

Commit b75b647

Browse files
authored
Merge pull request #264 from cgwalters/switch-inplace
Switch inplace
2 parents e5b5970 + 426af69 commit b75b647

File tree

3 files changed

+174
-16
lines changed

3 files changed

+174
-16
lines changed

lib/src/cli.rs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use anyhow::{Context, Result};
66
use camino::Utf8PathBuf;
7+
use cap_std_ext::cap_std;
78
use clap::Parser;
89
use fn_error_context::context;
910
use ostree::gio;
@@ -71,6 +72,12 @@ pub(crate) struct SwitchOpts {
7172
#[clap(long)]
7273
pub(crate) ostree_remote: Option<String>,
7374

75+
/// Don't create a new deployment, but directly mutate the booted state.
76+
/// This is hidden because it's not something we generally expect to be done,
77+
/// but this can be used in e.g. Anaconda %post to fixup
78+
#[clap(long, hide = true)]
79+
pub(crate) mutate_in_place: bool,
80+
7481
/// Retain reference to currently booted image
7582
#[clap(long)]
7683
pub(crate) retain: bool,
@@ -386,14 +393,6 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
386393
/// Implementation of the `bootc switch` CLI command.
387394
#[context("Switching")]
388395
async fn switch(opts: SwitchOpts) -> Result<()> {
389-
prepare_for_write().await?;
390-
let cancellable = gio::Cancellable::NONE;
391-
392-
let sysroot = &get_locked_sysroot().await?;
393-
let repo = &sysroot.repo();
394-
let (booted_deployment, _deployments, host) =
395-
crate::status::get_status_require_booted(sysroot)?;
396-
397396
let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
398397
let imgref = ostree_container::ImageReference {
399398
transport,
@@ -406,6 +405,29 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
406405
let target = ostree_container::OstreeImageReference { sigverify, imgref };
407406
let target = ImageReference::from(target);
408407

408+
// If we're doing an in-place mutation, we shortcut most of the rest of the work here
409+
if opts.mutate_in_place {
410+
let deployid = {
411+
// Clone to pass into helper thread
412+
let target = target.clone();
413+
let root = cap_std::fs::Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
414+
tokio::task::spawn_blocking(move || {
415+
crate::deploy::switch_origin_inplace(&root, &target)
416+
})
417+
.await??
418+
};
419+
println!("Updated {deployid} to pull from {target}");
420+
return Ok(());
421+
}
422+
423+
prepare_for_write().await?;
424+
let cancellable = gio::Cancellable::NONE;
425+
426+
let sysroot = &get_locked_sysroot().await?;
427+
let repo = &sysroot.repo();
428+
let (booted_deployment, _deployments, host) =
429+
crate::status::get_status_require_booted(sysroot)?;
430+
409431
let new_spec = {
410432
let mut new_spec = host.spec.clone();
411433
new_spec.image = Some(target.clone());

lib/src/deploy.rs

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
//!
33
//! Create a merged filesystem tree with the image and mounted configmaps.
44
5+
use anyhow::Ok;
56
use anyhow::{Context, Result};
67

8+
use cap_std::fs::Dir;
9+
use cap_std_ext::cap_std;
10+
use cap_std_ext::dirext::CapStdExtDirExt;
711
use fn_error_context::context;
812
use ostree::{gio, glib};
913
use ostree_container::OstreeImageReference;
@@ -12,6 +16,7 @@ use ostree_ext::container::store::PrepareResult;
1216
use ostree_ext::ostree;
1317
use ostree_ext::ostree::Deployment;
1418
use ostree_ext::sysroot::SysrootLock;
19+
use rustix::fs::MetadataExt;
1520

1621
use crate::spec::HostSpec;
1722
use crate::spec::ImageReference;
@@ -202,6 +207,18 @@ async fn deploy(
202207
Ok(())
203208
}
204209

210+
#[context("Generating origin")]
211+
fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
212+
let origin = glib::KeyFile::new();
213+
let imgref = OstreeImageReference::from(imgref.clone());
214+
origin.set_string(
215+
"origin",
216+
ostree_container::deploy::ORIGIN_CONTAINER,
217+
imgref.to_string().as_str(),
218+
);
219+
Ok(origin)
220+
}
221+
205222
/// Stage (queue deployment of) a fetched container image.
206223
#[context("Staging")]
207224
pub(crate) async fn stage(
@@ -211,13 +228,7 @@ pub(crate) async fn stage(
211228
spec: &RequiredHostSpec<'_>,
212229
) -> Result<()> {
213230
let merge_deployment = sysroot.merge_deployment(Some(stateroot));
214-
let origin = glib::KeyFile::new();
215-
let imgref = OstreeImageReference::from(spec.image.clone());
216-
origin.set_string(
217-
"origin",
218-
ostree_container::deploy::ORIGIN_CONTAINER,
219-
imgref.to_string().as_str(),
220-
);
231+
let origin = origin_from_imageref(spec.image)?;
221232
crate::deploy::deploy(
222233
sysroot,
223234
merge_deployment.as_ref(),
@@ -227,11 +238,115 @@ pub(crate) async fn stage(
227238
)
228239
.await?;
229240
crate::deploy::cleanup(sysroot).await?;
230-
println!("Queued for next boot: {imgref}");
241+
println!("Queued for next boot: {}", spec.image);
231242
if let Some(version) = image.version.as_deref() {
232243
println!(" Version: {version}");
233244
}
234245
println!(" Digest: {}", image.manifest_digest);
235246

236247
Ok(())
237248
}
249+
250+
fn find_newest_deployment_name(deploysdir: &Dir) -> Result<String> {
251+
let mut dirs = Vec::new();
252+
for ent in deploysdir.entries()? {
253+
let ent = ent?;
254+
if !ent.file_type()?.is_dir() {
255+
continue;
256+
}
257+
let name = ent.file_name();
258+
let name = if let Some(name) = name.to_str() {
259+
name
260+
} else {
261+
continue;
262+
};
263+
dirs.push((name.to_owned(), ent.metadata()?.mtime()));
264+
}
265+
dirs.sort_unstable_by(|a, b| a.1.cmp(&b.1));
266+
if let Some((name, _ts)) = dirs.pop() {
267+
Ok(name)
268+
} else {
269+
anyhow::bail!("No deployment directory found")
270+
}
271+
}
272+
273+
// Implementation of `bootc switch --in-place`
274+
pub(crate) fn switch_origin_inplace(root: &Dir, imgref: &ImageReference) -> Result<String> {
275+
// First, just create the new origin file
276+
let origin = origin_from_imageref(imgref)?;
277+
let serialized_origin = origin.to_data();
278+
279+
// Now, we can't rely on being officially booted (e.g. with the `ostree=` karg)
280+
// in a scenario like running in the anaconda %post.
281+
// Eventually, we should support a setup here where ostree-prepare-root
282+
// can officially be run to "enter" an ostree root in a supportable way.
283+
// Anyways for now, the brutal hack is to just scrape through the deployments
284+
// and find the newest one, which we will mutate. If there's more than one,
285+
// ultimately the calling tooling should be fixed to set things up correctly.
286+
287+
let mut ostree_deploys = root.open_dir("sysroot/ostree/deploy")?.entries()?;
288+
let deploydir = loop {
289+
if let Some(ent) = ostree_deploys.next() {
290+
let ent = ent?;
291+
if !ent.file_type()?.is_dir() {
292+
continue;
293+
}
294+
tracing::debug!("Checking {:?}", ent.file_name());
295+
let child_dir = ent
296+
.open_dir()
297+
.with_context(|| format!("Opening dir {:?}", ent.file_name()))?;
298+
if let Some(d) = child_dir.open_dir_optional("deploy")? {
299+
break d;
300+
}
301+
} else {
302+
anyhow::bail!("Failed to find a deployment");
303+
}
304+
};
305+
let newest_deployment = find_newest_deployment_name(&deploydir)?;
306+
let origin_path = format!("{newest_deployment}.origin");
307+
if !deploydir.try_exists(&origin_path)? {
308+
tracing::warn!("No extant origin for {newest_deployment}");
309+
}
310+
deploydir
311+
.atomic_write(&origin_path, serialized_origin.as_bytes())
312+
.context("Writing origin")?;
313+
return Ok(newest_deployment);
314+
}
315+
316+
#[test]
317+
fn test_switch_inplace() -> Result<()> {
318+
use std::os::unix::fs::DirBuilderExt;
319+
320+
let td = cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
321+
let mut builder = cap_std::fs::DirBuilder::new();
322+
let builder = builder.recursive(true).mode(0o755);
323+
let deploydir = "sysroot/ostree/deploy/default/deploy";
324+
let target_deployment = "af36eb0086bb55ac601600478c6168f834288013d60f8870b7851f44bf86c3c5.0";
325+
td.ensure_dir_with(
326+
format!("sysroot/ostree/deploy/default/deploy/{target_deployment}"),
327+
builder,
328+
)?;
329+
let deploydir = &td.open_dir(deploydir)?;
330+
let orig_imgref = ImageReference {
331+
image: "quay.io/exampleos/original:sometag".into(),
332+
transport: "registry".into(),
333+
signature: None,
334+
};
335+
{
336+
let origin = origin_from_imageref(&orig_imgref)?;
337+
deploydir.atomic_write(
338+
format!("{target_deployment}.origin"),
339+
origin.to_data().as_bytes(),
340+
)?;
341+
}
342+
343+
let target_imgref = ImageReference {
344+
image: "quay.io/someother/otherimage:latest".into(),
345+
transport: "registry".into(),
346+
signature: None,
347+
};
348+
349+
let replaced = switch_origin_inplace(&td, &target_imgref).unwrap();
350+
assert_eq!(replaced, target_deployment);
351+
Ok(())
352+
}

lib/src/spec.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! The definition for host system state.
22
3+
use std::fmt::Display;
4+
5+
use ostree_ext::container::OstreeImageReference;
36
use schemars::JsonSchema;
47
use serde::{Deserialize, Serialize};
58

@@ -149,8 +152,17 @@ impl Default for Host {
149152
}
150153
}
151154

155+
impl Display for ImageReference {
156+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157+
let ostree_imgref = OstreeImageReference::from(self.clone());
158+
ostree_imgref.fmt(f)
159+
}
160+
}
161+
152162
#[cfg(test)]
153163
mod tests {
164+
use std::str::FromStr;
165+
154166
use super::*;
155167

156168
#[test]
@@ -183,4 +195,13 @@ mod tests {
183195
Some(ImageSignature::OstreeRemote("fedora".into()))
184196
);
185197
}
198+
199+
#[test]
200+
fn test_display_imgref() {
201+
let src = "ostree-unverified-registry:quay.io/example/foo:sometag";
202+
let s = OstreeImageReference::from_str(src).unwrap();
203+
let s = ImageReference::from(s);
204+
let displayed = format!("{s}");
205+
assert_eq!(displayed.as_str(), src);
206+
}
186207
}

0 commit comments

Comments
 (0)