Skip to content

Commit 0415f12

Browse files
committed
WIP: Use podman pull to fetch containers
See #147 (comment) With this bootc starts to really gain support for a different backend than ostree. Here we basically just fork off `podman pull` to fetch container images into an *alternative root* in `/ostree/container-storage`, (Because otherwise basic things like `podman image prune` would delete the OS image) This is quite distinct from our use of `skopeo` in the ostree-ext project because suddenly now we gain support for things implemented in the containers/storage library like `zstd:chunked` and OCI crypt. *However*...today we still need to generate a final flattened filesystem tree (and an ostree commit) in order to maintain compatibilty with stuff in rpm-ostree. (A corrollary to this is we're not booting into a `podman mount` overlayfs stack) Related to this, we also need to handle SELinux labeling. Hence, we implement "layer squashing", and then do some final "postprocessing" on the resulting image matching the same logic that's done in ostree-ext such as `etc -> usr/etc` and handling `/var`. Note this also really wants ostreedev/ostree#3106 to avoid duplicating disk space. Signed-off-by: Colin Walters <[email protected]>
1 parent 0bffcce commit 0415f12

File tree

9 files changed

+760
-31
lines changed

9 files changed

+760
-31
lines changed

lib/src/cli.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ pub(crate) struct SwitchOpts {
6969

7070
/// Target image to use for the next boot.
7171
pub(crate) target: String,
72+
73+
/// The storage backend
74+
#[clap(long, hide = true)]
75+
pub(crate) backend: Option<crate::spec::Backend>,
7276
}
7377

7478
/// Perform an edit operation
@@ -119,6 +123,15 @@ pub(crate) enum TestingOpts {
119123
},
120124
}
121125

126+
/// Options for internal testing
127+
#[derive(Debug, clap::Parser)]
128+
pub(crate) struct InternalPodmanOpts {
129+
#[clap(long, value_parser, default_value = "/")]
130+
root: Utf8PathBuf,
131+
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
132+
args: Vec<std::ffi::OsString>,
133+
}
134+
122135
/// Deploy and transactionally in-place with bootable container images.
123136
///
124137
/// The `bootc` project currently uses ostree-containers as a backend
@@ -158,6 +171,9 @@ pub(crate) enum Opt {
158171
#[clap(hide = true)]
159172
#[command(external_subcommand)]
160173
ExecInHostMountNamespace(Vec<OsString>),
174+
/// Execute podman in our internal configuration
175+
#[clap(hide = true)]
176+
InternalPodman(InternalPodmanOpts),
161177
/// Install to the target filesystem.
162178
#[cfg(feature = "install")]
163179
InstallToFilesystem(crate::install::InstallToFilesystemOpts),
@@ -294,7 +310,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
294310
}
295311
}
296312
} else {
297-
let fetched = crate::deploy::pull(&sysroot, imgref, opts.quiet).await?;
313+
let fetched = crate::deploy::pull(&sysroot, spec.backend, imgref, opts.quiet).await?;
298314
let staged_digest = staged_image.as_ref().map(|s| s.image_digest.as_str());
299315
let fetched_digest = fetched.manifest_digest.as_str();
300316
tracing::debug!("staged: {staged_digest:?}");
@@ -366,6 +382,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
366382
let new_spec = {
367383
let mut new_spec = host.spec.clone();
368384
new_spec.image = Some(target.clone());
385+
new_spec.backend = opts.backend.unwrap_or_default();
369386
new_spec
370387
};
371388

@@ -374,7 +391,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
374391
}
375392
let new_spec = RequiredHostSpec::from_spec(&new_spec)?;
376393

377-
let fetched = crate::deploy::pull(sysroot, &target, opts.quiet).await?;
394+
let fetched = crate::deploy::pull(sysroot, new_spec.backend, &target, opts.quiet).await?;
378395

379396
if !opts.retain {
380397
// By default, we prune the previous ostree ref so it will go away after later upgrades
@@ -416,7 +433,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
416433
return Ok(());
417434
}
418435
let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
419-
let fetched = crate::deploy::pull(sysroot, new_spec.image, opts.quiet).await?;
436+
let fetched =
437+
crate::deploy::pull(sysroot, new_spec.backend, new_spec.image, opts.quiet).await?;
420438

421439
// TODO gc old layers here
422440

@@ -462,6 +480,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
462480
crate::hostexec::exec_in_host_mountns(args.as_slice())
463481
}
464482
Opt::Status(opts) => super::status::status(opts).await,
483+
Opt::InternalPodman(args) => {
484+
prepare_for_write().await?;
485+
// This also remounts writable
486+
let _sysroot = get_locked_sysroot().await?;
487+
crate::podman::exec(args.root.as_path(), args.args.as_slice())
488+
}
465489
#[cfg(feature = "internal-testing-api")]
466490
Opt::InternalTests(opts) => crate::privtests::run(opts).await,
467491
#[cfg(feature = "docgen")]

lib/src/deploy.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
55
use anyhow::{Context, Result};
66

7+
use cap_std_ext::cap_std::fs::Dir;
8+
use chrono::DateTime;
79
use fn_error_context::context;
810
use ostree::{gio, glib};
911
use ostree_container::OstreeImageReference;
1012
use ostree_ext::container as ostree_container;
1113
use ostree_ext::container::store::PrepareResult;
14+
use ostree_ext::oci_spec;
1215
use ostree_ext::ostree;
1316
use ostree_ext::ostree::Deployment;
1417
use ostree_ext::sysroot::SysrootLock;
1518

19+
use crate::spec::Backend;
1620
use crate::spec::HostSpec;
1721
use crate::spec::ImageReference;
1822

@@ -25,11 +29,14 @@ const BOOTC_DERIVED_KEY: &str = "bootc.derived";
2529
/// Variant of HostSpec but required to be filled out
2630
pub(crate) struct RequiredHostSpec<'a> {
2731
pub(crate) image: &'a ImageReference,
32+
pub(crate) backend: Backend,
2833
}
2934

3035
/// State of a locally fetched image
3136
pub(crate) struct ImageState {
37+
pub(crate) backend: Backend,
3238
pub(crate) manifest_digest: String,
39+
pub(crate) created: Option<DateTime<chrono::Utc>>,
3340
pub(crate) version: Option<String>,
3441
pub(crate) ostree_commit: String,
3542
}
@@ -42,16 +49,29 @@ impl<'a> RequiredHostSpec<'a> {
4249
.image
4350
.as_ref()
4451
.ok_or_else(|| anyhow::anyhow!("Missing image in specification"))?;
45-
Ok(Self { image })
52+
Ok(Self {
53+
image,
54+
backend: spec.backend,
55+
})
4656
}
4757
}
4858

4959
impl From<ostree_container::store::LayeredImageState> for ImageState {
5060
fn from(value: ostree_container::store::LayeredImageState) -> Self {
5161
let version = value.version().map(|v| v.to_owned());
5262
let ostree_commit = value.get_commit().to_owned();
63+
let config = value.configuration.as_ref();
64+
let labels = config.and_then(crate::status::labels_of_config);
65+
let created = labels
66+
.and_then(|l| {
67+
l.get(oci_spec::image::ANNOTATION_CREATED)
68+
.map(|s| s.as_str())
69+
})
70+
.and_then(crate::status::try_deserialize_timestamp);
5371
Self {
72+
backend: Backend::OstreeContainer,
5473
manifest_digest: value.manifest_digest,
74+
created,
5575
version,
5676
ostree_commit,
5777
}
@@ -64,8 +84,14 @@ impl ImageState {
6484
&self,
6585
repo: &ostree::Repo,
6686
) -> Result<Option<ostree_ext::oci_spec::image::ImageManifest>> {
67-
ostree_container::store::query_image_commit(repo, &self.ostree_commit)
68-
.map(|v| Some(v.manifest))
87+
match self.backend {
88+
Backend::OstreeContainer => {
89+
ostree_container::store::query_image_commit(repo, &self.ostree_commit)
90+
.map(|v| Some(v.manifest))
91+
}
92+
// TODO: Figure out if we can get the OCI manifest from podman
93+
Backend::Container => Ok(None),
94+
}
6995
}
7096
}
7197

@@ -83,6 +109,31 @@ pub(crate) async fn new_importer(
83109
/// Wrapper for pulling a container image, wiring up status output.
84110
#[context("Pulling")]
85111
pub(crate) async fn pull(
112+
sysroot: &SysrootLock,
113+
backend: Backend,
114+
imgref: &ImageReference,
115+
quiet: bool,
116+
) -> Result<Box<ImageState>> {
117+
match backend {
118+
Backend::OstreeContainer => pull_via_ostree(sysroot, imgref, quiet).await,
119+
Backend::Container => pull_via_podman(sysroot, imgref, quiet).await,
120+
}
121+
}
122+
123+
/// Wrapper for pulling a container image, wiring up status output.
124+
async fn pull_via_podman(
125+
sysroot: &SysrootLock,
126+
imgref: &ImageReference,
127+
quiet: bool,
128+
) -> Result<Box<ImageState>> {
129+
let rootfs = &Dir::reopen_dir(&crate::utils::sysroot_fd_borrowed(sysroot))?;
130+
let fetched_imageid = crate::podman::podman_pull(rootfs, imgref, quiet).await?;
131+
crate::podman_ostree::commit_image_to_ostree(sysroot, &fetched_imageid)
132+
.await
133+
.map(Box::new)
134+
}
135+
136+
async fn pull_via_ostree(
86137
sysroot: &SysrootLock,
87138
imgref: &ImageReference,
88139
quiet: bool,

lib/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub mod cli;
1717
pub(crate) mod deploy;
1818
pub(crate) mod hostexec;
1919
mod lsm;
20+
mod ostree_authfile;
21+
mod podman;
22+
mod podman_ostree;
2023
mod reboot;
2124
mod reexec;
2225
mod status;
@@ -36,8 +39,6 @@ mod install;
3639
mod k8sapitypes;
3740
#[cfg(feature = "install")]
3841
pub(crate) mod mount;
39-
#[cfg(feature = "install")]
40-
mod podman;
4142
pub mod spec;
4243
#[cfg(feature = "install")]
4344
mod task;

lib/src/ostree_authfile.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//! # Copy of the ostree authfile bits as they're not public
2+
3+
use anyhow::Result;
4+
use once_cell::sync::OnceCell;
5+
use ostree_ext::glib;
6+
use std::fs::File;
7+
use std::path::{Path, PathBuf};
8+
9+
// https://docs.rs/openat-ext/0.1.10/openat_ext/trait.OpenatDirExt.html#tymethod.open_file_optional
10+
// https://users.rust-lang.org/t/why-i-use-anyhow-error-even-in-libraries/68592
11+
pub(crate) fn open_optional(path: impl AsRef<Path>) -> std::io::Result<Option<std::fs::File>> {
12+
match std::fs::File::open(path.as_ref()) {
13+
Ok(r) => Ok(Some(r)),
14+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
15+
Err(e) => Err(e),
16+
}
17+
}
18+
19+
struct ConfigPaths {
20+
persistent: PathBuf,
21+
runtime: PathBuf,
22+
}
23+
24+
/// Get the runtime and persistent config directories. In the system (root) case, these
25+
/// system(root) case: /run/ostree /etc/ostree
26+
/// user(nonroot) case: /run/user/$uid/ostree ~/.config/ostree
27+
fn get_config_paths() -> &'static ConfigPaths {
28+
static PATHS: OnceCell<ConfigPaths> = OnceCell::new();
29+
PATHS.get_or_init(|| {
30+
let mut r = if rustix::process::getuid() == rustix::process::Uid::ROOT {
31+
ConfigPaths {
32+
persistent: PathBuf::from("/etc"),
33+
runtime: PathBuf::from("/run"),
34+
}
35+
} else {
36+
ConfigPaths {
37+
persistent: glib::user_config_dir(),
38+
runtime: glib::user_runtime_dir(),
39+
}
40+
};
41+
let path = "ostree";
42+
r.persistent.push(path);
43+
r.runtime.push(path);
44+
r
45+
})
46+
}
47+
48+
impl ConfigPaths {
49+
/// Return the path and an open fd for a config file, if it exists.
50+
pub(crate) fn open_file(&self, p: impl AsRef<Path>) -> Result<Option<(PathBuf, File)>> {
51+
let p = p.as_ref();
52+
let mut runtime = self.runtime.clone();
53+
runtime.push(p);
54+
if let Some(f) = open_optional(&runtime)? {
55+
return Ok(Some((runtime, f)));
56+
}
57+
let mut persistent = self.persistent.clone();
58+
persistent.push(p);
59+
if let Some(f) = open_optional(&persistent)? {
60+
return Ok(Some((persistent, f)));
61+
}
62+
Ok(None)
63+
}
64+
}
65+
66+
/// Return the path to the global container authentication file, if it exists.
67+
pub(crate) fn get_global_authfile_path() -> Result<Option<PathBuf>> {
68+
let paths = get_config_paths();
69+
let r = paths.open_file("auth.json")?;
70+
// TODO pass the file descriptor to the proxy, not a global path
71+
Ok(r.map(|v| v.0))
72+
}

0 commit comments

Comments
 (0)