Skip to content

Commit

Permalink
WIP: Use podman pull to fetch containers
Browse files Browse the repository at this point in the history
See containers#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]>
  • Loading branch information
cgwalters authored and jeckersb committed Jan 23, 2024
1 parent 619d4ac commit e0734bb
Show file tree
Hide file tree
Showing 9 changed files with 773 additions and 38 deletions.
30 changes: 27 additions & 3 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ pub(crate) struct SwitchOpts {

/// Target image to use for the next boot.
pub(crate) target: String,

/// The storage backend
#[clap(long, hide = true)]
pub(crate) backend: Option<crate::spec::Backend>,
}

/// Perform an edit operation
Expand Down Expand Up @@ -151,6 +155,15 @@ pub(crate) enum TestingOpts {
},
}

/// Options for internal testing
#[derive(Debug, clap::Parser)]
pub(crate) struct InternalPodmanOpts {
#[clap(long, value_parser, default_value = "/")]
root: Utf8PathBuf,
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<std::ffi::OsString>,
}

/// Deploy and transactionally in-place with bootable container images.
///
/// The `bootc` project currently uses ostree-containers as a backend
Expand Down Expand Up @@ -219,6 +232,9 @@ pub(crate) enum Opt {
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Execute podman in our internal configuration
#[clap(hide = true)]
InternalPodman(InternalPodmanOpts),
/// Internal integration testing helpers.
#[clap(hide(true), subcommand)]
#[cfg(feature = "internal-testing-api")]
Expand Down Expand Up @@ -353,7 +369,7 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
}
}
} else {
let fetched = crate::deploy::pull(sysroot, imgref, opts.quiet).await?;
let fetched = crate::deploy::pull(sysroot, spec.backend, imgref, opts.quiet).await?;
let staged_digest = staged_image.as_ref().map(|s| s.image_digest.as_str());
let fetched_digest = fetched.manifest_digest.as_str();
tracing::debug!("staged: {staged_digest:?}");
Expand Down Expand Up @@ -438,6 +454,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
let new_spec = {
let mut new_spec = host.spec.clone();
new_spec.image = Some(target.clone());
new_spec.backend = opts.backend.unwrap_or_default();
new_spec
};

Expand All @@ -447,7 +464,7 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
}
let new_spec = RequiredHostSpec::from_spec(&new_spec)?;

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

if !opts.retain {
// By default, we prune the previous ostree ref so it will go away after later upgrades
Expand Down Expand Up @@ -489,7 +506,8 @@ async fn edit(opts: EditOpts) -> Result<()> {
return Ok(());
}
let new_spec = RequiredHostSpec::from_spec(&new_host.spec)?;
let fetched = crate::deploy::pull(sysroot, new_spec.image, opts.quiet).await?;
let fetched =
crate::deploy::pull(sysroot, new_spec.backend, new_spec.image, opts.quiet).await?;

// TODO gc old layers here

Expand Down Expand Up @@ -537,6 +555,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
crate::hostexec::exec_in_host_mountns(args.as_slice())
}
Opt::Status(opts) => super::status::status(opts).await,
Opt::InternalPodman(args) => {
prepare_for_write().await?;
// This also remounts writable
let _sysroot = get_locked_sysroot().await?;
crate::podman::exec(args.root.as_path(), args.args.as_slice())
}
#[cfg(feature = "internal-testing-api")]
Opt::InternalTests(opts) => crate::privtests::run(opts).await,
#[cfg(feature = "docgen")]
Expand Down
56 changes: 53 additions & 3 deletions lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ use anyhow::{Context, Result};
use cap_std::fs::Dir;
use cap_std_ext::cap_std;
use cap_std_ext::dirext::CapStdExtDirExt;
use chrono::DateTime;
use fn_error_context::context;
use ostree::{gio, glib};
use ostree_container::OstreeImageReference;
use ostree_ext::container as ostree_container;
use ostree_ext::container::store::PrepareResult;
use ostree_ext::oci_spec;
use ostree_ext::ostree;
use ostree_ext::ostree::Deployment;
use ostree_ext::sysroot::SysrootLock;
use rustix::fs::MetadataExt;

use crate::spec::Backend;
use crate::spec::HostSpec;
use crate::spec::ImageReference;

Expand All @@ -30,11 +33,14 @@ const BOOTC_DERIVED_KEY: &str = "bootc.derived";
/// Variant of HostSpec but required to be filled out
pub(crate) struct RequiredHostSpec<'a> {
pub(crate) image: &'a ImageReference,
pub(crate) backend: Backend,
}

/// State of a locally fetched image
pub(crate) struct ImageState {
pub(crate) backend: Backend,
pub(crate) manifest_digest: String,
pub(crate) created: Option<DateTime<chrono::Utc>>,
pub(crate) version: Option<String>,
pub(crate) ostree_commit: String,
}
Expand All @@ -47,16 +53,29 @@ impl<'a> RequiredHostSpec<'a> {
.image
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing image in specification"))?;
Ok(Self { image })
Ok(Self {
image,
backend: spec.backend,
})
}
}

impl From<ostree_container::store::LayeredImageState> for ImageState {
fn from(value: ostree_container::store::LayeredImageState) -> Self {
let version = value.version().map(|v| v.to_owned());
let ostree_commit = value.get_commit().to_owned();
let config = value.configuration.as_ref();
let labels = config.and_then(crate::status::labels_of_config);
let created = labels
.and_then(|l| {
l.get(oci_spec::image::ANNOTATION_CREATED)
.map(|s| s.as_str())
})
.and_then(crate::status::try_deserialize_timestamp);
Self {
backend: Backend::OstreeContainer,
manifest_digest: value.manifest_digest,
created,
version,
ostree_commit,
}
Expand All @@ -69,8 +88,14 @@ impl ImageState {
&self,
repo: &ostree::Repo,
) -> Result<Option<ostree_ext::oci_spec::image::ImageManifest>> {
ostree_container::store::query_image_commit(repo, &self.ostree_commit)
.map(|v| Some(v.manifest))
match self.backend {
Backend::OstreeContainer => {
ostree_container::store::query_image_commit(repo, &self.ostree_commit)
.map(|v| Some(v.manifest))
}
// TODO: Figure out if we can get the OCI manifest from podman
Backend::Container => Ok(None),
}
}
}

Expand All @@ -88,6 +113,31 @@ pub(crate) async fn new_importer(
/// Wrapper for pulling a container image, wiring up status output.
#[context("Pulling")]
pub(crate) async fn pull(
sysroot: &SysrootLock,
backend: Backend,
imgref: &ImageReference,
quiet: bool,
) -> Result<Box<ImageState>> {
match backend {
Backend::OstreeContainer => pull_via_ostree(sysroot, imgref, quiet).await,
Backend::Container => pull_via_podman(sysroot, imgref, quiet).await,
}
}

/// Wrapper for pulling a container image, wiring up status output.
async fn pull_via_podman(
sysroot: &SysrootLock,
imgref: &ImageReference,
quiet: bool,
) -> Result<Box<ImageState>> {
let rootfs = &Dir::reopen_dir(&crate::utils::sysroot_fd_borrowed(sysroot))?;
let fetched_imageid = crate::podman::podman_pull(rootfs, imgref, quiet).await?;
crate::podman_ostree::commit_image_to_ostree(sysroot, &fetched_imageid)
.await
.map(Box::new)
}

async fn pull_via_ostree(
sysroot: &SysrootLock,
imgref: &ImageReference,
quiet: bool,
Expand Down
5 changes: 3 additions & 2 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub mod cli;
pub(crate) mod deploy;
pub(crate) mod hostexec;
mod lsm;
mod ostree_authfile;
mod podman;
mod podman_ostree;
mod reboot;
mod reexec;
mod status;
Expand All @@ -41,8 +44,6 @@ mod install;
mod k8sapitypes;
#[cfg(feature = "install")]
pub(crate) mod mount;
#[cfg(feature = "install")]
mod podman;
pub mod spec;

#[cfg(feature = "docgen")]
Expand Down
72 changes: 72 additions & 0 deletions lib/src/ostree_authfile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! # Copy of the ostree authfile bits as they're not public
use anyhow::Result;
use once_cell::sync::OnceCell;
use ostree_ext::glib;
use std::fs::File;
use std::path::{Path, PathBuf};

// https://docs.rs/openat-ext/0.1.10/openat_ext/trait.OpenatDirExt.html#tymethod.open_file_optional
// https://users.rust-lang.org/t/why-i-use-anyhow-error-even-in-libraries/68592
pub(crate) fn open_optional(path: impl AsRef<Path>) -> std::io::Result<Option<std::fs::File>> {
match std::fs::File::open(path.as_ref()) {
Ok(r) => Ok(Some(r)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}

struct ConfigPaths {
persistent: PathBuf,
runtime: PathBuf,
}

/// Get the runtime and persistent config directories. In the system (root) case, these
/// system(root) case: /run/ostree /etc/ostree
/// user(nonroot) case: /run/user/$uid/ostree ~/.config/ostree
fn get_config_paths() -> &'static ConfigPaths {
static PATHS: OnceCell<ConfigPaths> = OnceCell::new();
PATHS.get_or_init(|| {
let mut r = if rustix::process::getuid() == rustix::process::Uid::ROOT {
ConfigPaths {
persistent: PathBuf::from("/etc"),
runtime: PathBuf::from("/run"),
}
} else {
ConfigPaths {
persistent: glib::user_config_dir(),
runtime: glib::user_runtime_dir(),
}
};
let path = "ostree";
r.persistent.push(path);
r.runtime.push(path);
r
})
}

impl ConfigPaths {
/// Return the path and an open fd for a config file, if it exists.
pub(crate) fn open_file(&self, p: impl AsRef<Path>) -> Result<Option<(PathBuf, File)>> {
let p = p.as_ref();
let mut runtime = self.runtime.clone();
runtime.push(p);
if let Some(f) = open_optional(&runtime)? {
return Ok(Some((runtime, f)));
}
let mut persistent = self.persistent.clone();
persistent.push(p);
if let Some(f) = open_optional(&persistent)? {
return Ok(Some((persistent, f)));
}
Ok(None)
}
}

/// Return the path to the global container authentication file, if it exists.
pub(crate) fn get_global_authfile_path() -> Result<Option<PathBuf>> {
let paths = get_config_paths();
let r = paths.open_file("auth.json")?;
// TODO pass the file descriptor to the proxy, not a global path
Ok(r.map(|v| v.0))
}
Loading

0 comments on commit e0734bb

Please sign in to comment.