Skip to content

Commit

Permalink
Initialize a containers-storage: owned by bootc, use for bound images
Browse files Browse the repository at this point in the history
Initial work for: containers#721

- Initialize a containers-storage: instance at install time
  (that defaults to empty)
- "Open" it (but do nothing with it) as part of the core CLI
  operations

Further APIs and work will build on top of this.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Jul 29, 2024
1 parent 218ed0e commit 2fe0c9f
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 71 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ install:
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/bound-images.d
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/kargs.d
ln -s /sysroot/ostree/bootc/storage $(DESTDIR)$(prefix)/lib/bootc/storage
install -d -m 0755 $(DESTDIR)$(prefix)/lib/systemd/system-generators/
ln -f $(DESTDIR)$(prefix)/bin/bootc $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator
install -d $(DESTDIR)$(prefix)/lib/bootc/install
Expand Down
21 changes: 17 additions & 4 deletions docs/src/experimental-logically-bound-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ This experimental feature enables an association of container "app" images to a

## Using logically bound images

Each image is defined in a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) `.image` or `.container` file. An image is selected to be bound by creating a symlink in the `/usr/lib/bootc/bound-images.d` directory pointing to a `.image` or `.container` file. With these defined, during a `bootc upgrade` or `bootc switch` the bound images defined in the new bootc image will be automatically pulled via podman.
Each image is defined in a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) `.image` or `.container` file. An image is selected to be bound by creating a symlink in the `/usr/lib/bootc/bound-images.d` directory pointing to a `.image` or `.container` file.

With these defined, during a `bootc upgrade` or `bootc switch` the bound images defined in the new bootc image will be automatically pulled into the bootc image storage, and are available to container runtimes such as podman by explicitly configuring them to point to the bootc storage as an "additional image store", via e.g.:

`podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage run <image> ...`

An example Containerfile

Expand All @@ -28,8 +32,17 @@ RUN ln -s /usr/share/containers/systemd/my-app.image /usr/lib/bootc/bound-images
ln -s /usr/share/containers/systemd/my-app.image /usr/lib/bootc/bound-images.d/my-app.image
```

In the `.container` definition, you should use:

```
GlobalArgs=--storage-opt=additionalimagestore=/usr/lib/bootc/storage
```

## Pull secret

Images are fetched using the global bootc pull secret by default (`/etc/ostree/auth.json`). It is not yet supported to configure `PullSecret` in these image definitions.

## Limitations

- Currently, only the Image field of a `.image` or `.container` file is used to pull the image. Any other field is ignored.
- There is no cleanup during rollback.
- Images are subject to default garbage collection semantics; e.g. a background job pruning images without a running container may prune them. They can also be manually removed via e.g. podman rmi.
- Currently, only the Image field of a `.image` or `.container` file is used to pull the image; per above not even `PullSecret=` is supported.
- Images are not yet garbage collected
29 changes: 16 additions & 13 deletions lib/src/boundimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
//! pre-pulled (and in the future, pinned) before a new image root
//! is considered ready.

use crate::task::Task;
use anyhow::{Context, Result};
use camino::Utf8Path;
use cap_std_ext::cap_std::fs::Dir;
use cap_std_ext::dirext::CapStdExtDirExt;
use fn_error_context::context;
use ostree_ext::containers_image_proxy;
use ostree_ext::ostree::Deployment;
use ostree_ext::sysroot::SysrootLock;

use crate::imgstorage::PullMode;
use crate::store::Storage;

/// The path in a root for bound images; this directory should only contain
/// symbolic links to `.container` or `.image` files.
Expand All @@ -37,10 +38,10 @@ pub(crate) struct ResolvedBoundImage {
}

/// Given a deployment, pull all container images it references.
pub(crate) fn pull_bound_images(sysroot: &SysrootLock, deployment: &Deployment) -> Result<()> {
pub(crate) async fn pull_bound_images(sysroot: &Storage, deployment: &Deployment) -> Result<()> {
let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?;
let bound_images = query_bound_images(deployment_root)?;
pull_images(deployment_root, bound_images)
pull_images(sysroot, bound_images).await
}

#[context("Querying bound images")]
Expand Down Expand Up @@ -133,18 +134,20 @@ fn parse_container_file(file_contents: &tini::Ini) -> Result<BoundImage> {
Ok(bound_image)
}

#[context("pull bound images")]
pub(crate) fn pull_images(_deployment_root: &Dir, bound_images: Vec<BoundImage>) -> Result<()> {
#[context("Pulling bound images")]
pub(crate) async fn pull_images(sysroot: &Storage, bound_images: Vec<BoundImage>) -> Result<()> {
tracing::debug!("Pulling bound images: {}", bound_images.len());
//TODO: do this in parallel
for bound_image in bound_images {
let mut task = Task::new("Pulling bound image", "/usr/bin/podman")
.arg("pull")
.arg(&bound_image.image);
if let Some(auth_file) = &bound_image.auth_file {
task = task.arg("--authfile").arg(auth_file);
}
task.run()?;
let image = &bound_image.image;
let desc = format!("Updating bound image: {image}");
crate::utils::async_task_with_spinner(&desc, async move {
sysroot
.imgstore
.pull(&bound_image.image, PullMode::IfNotExists)
.await
})
.await?;
}

Ok(())
Expand Down
60 changes: 59 additions & 1 deletion lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,31 @@ pub(crate) enum ContainerOpts {
Lint,
}

/// Subcommands which operate on images.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ImageCmdOpts {
/// Wrapper for `podman image list` in bootc storage.
List {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Wrapper for `podman image build` in bootc storage.
Build {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Wrapper for `podman image pull` in bootc storage.
Pull {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
/// Wrapper for `podman image push` in bootc storage.
Push {
#[clap(allow_hyphen_values = true)]
args: Vec<OsString>,
},
}

/// Subcommands which operate on images.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ImageOpts {
Expand Down Expand Up @@ -232,6 +257,16 @@ pub(crate) enum ImageOpts {
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
target: Option<String>,
},
/// Copy a container image from the default `containers-storage:` to the bootc-owned container storage.
PullFromDefaultStorage {
/// The image to pull
image: String,
},
/// List fetched images stored in the bootc storage.
///
/// Note that these are distinct from images stored via e.g. `podman`.
#[clap(subcommand)]
Cmd(ImageCmdOpts),
}

/// Hidden, internal only options
Expand Down Expand Up @@ -430,10 +465,12 @@ pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootL
Ok(sysroot)
}

/// Load global storage state, expecting that we're booted into a bootc system.
#[context("Initializing storage")]
pub(crate) async fn get_storage() -> Result<crate::store::Storage> {
let global_run = Dir::open_ambient_dir("/run", cap_std::ambient_authority())?;
let sysroot = get_locked_sysroot().await?;
crate::store::Storage::new(sysroot)
crate::store::Storage::new(sysroot, &global_run)
}

#[context("Querying root privilege")]
Expand Down Expand Up @@ -798,6 +835,27 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
ImageOpts::CopyToStorage { source, target } => {
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
}
ImageOpts::PullFromDefaultStorage { image } => {
let sysroot = get_storage().await?;
sysroot.imgstore.pull_from_host_storage(&image).await
}
ImageOpts::Cmd(opt) => {
let sysroot = get_storage().await?;
match opt {
ImageCmdOpts::List { args } => {
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "list", &args).await
}
ImageCmdOpts::Build { args } => {
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "build", &args).await
}
ImageCmdOpts::Pull { args } => {
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "pull", &args).await
}
ImageCmdOpts::Push { args } => {
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "push", &args).await
}
}
}
},
#[cfg(feature = "install")]
Opt::Install(opts) => match opts {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ pub(crate) async fn stage(
)
.await?;

crate::boundimage::pull_bound_images(sysroot, &deployment)?;
crate::boundimage::pull_bound_images(sysroot, &deployment).await?;

crate::deploy::cleanup(sysroot).await?;
println!("Queued for next boot: {:#}", spec.image);
Expand Down
25 changes: 24 additions & 1 deletion lib/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,29 @@ use anyhow::{Context, Result};
use fn_error_context::context;
use ostree_ext::container::{ImageReference, Transport};

use crate::{imgstorage::Storage, utils::CommandRunExt};

/// The name of the image we push to containers-storage if nothing is specified.
const IMAGE_DEFAULT: &str = "localhost/bootc";

#[context("Listing images")]
pub(crate) async fn list_entrypoint() -> Result<()> {
let sysroot = crate::cli::get_locked_sysroot().await?;
let sysroot = crate::cli::get_storage().await?;
let repo = &sysroot.repo();

let images = ostree_ext::container::store::list_images(repo).context("Querying images")?;

println!("# Host images");
for image in images {
println!("{image}");
}
println!("");

println!("# Logically bound images");
let mut listcmd = sysroot.imgstore.new_image_cmd()?;
listcmd.arg("list");
listcmd.run()?;

Ok(())
}

Expand Down Expand Up @@ -64,3 +74,16 @@ pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>)
println!("Pushed: {target} {r}");
Ok(())
}

/// Thin wrapper for invoking `podman image <X>` but set up for our internal
/// image store (as distinct from /var/lib/containers default).
pub(crate) async fn imgcmd_entrypoint(
storage: &Storage,
arg: &str,
args: &[std::ffi::OsString],
) -> std::result::Result<(), anyhow::Error> {
let mut cmd = storage.new_image_cmd()?;
cmd.arg(arg);
cmd.args(args);
cmd.run()
}
Loading

0 comments on commit 2fe0c9f

Please sign in to comment.