Skip to content

Commit 2fe0c9f

Browse files
committed
Initialize a containers-storage: owned by bootc, use for bound images
Initial work for: #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]>
1 parent 218ed0e commit 2fe0c9f

File tree

12 files changed

+406
-71
lines changed

12 files changed

+406
-71
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ install:
1111
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bootc
1212
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/bound-images.d
1313
install -d -m 0755 $(DESTDIR)$(prefix)/lib/bootc/kargs.d
14+
ln -s /sysroot/ostree/bootc/storage $(DESTDIR)$(prefix)/lib/bootc/storage
1415
install -d -m 0755 $(DESTDIR)$(prefix)/lib/systemd/system-generators/
1516
ln -f $(DESTDIR)$(prefix)/bin/bootc $(DESTDIR)$(prefix)/lib/systemd/system-generators/bootc-systemd-generator
1617
install -d $(DESTDIR)$(prefix)/lib/bootc/install

docs/src/experimental-logically-bound-images.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ This experimental feature enables an association of container "app" images to a
1414

1515
## Using logically bound images
1616

17-
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.
17+
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.
18+
19+
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.:
20+
21+
`podman --storage-opt=additionalimagestore=/usr/lib/bootc/storage run <image> ...`
1822

1923
An example Containerfile
2024

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

35+
In the `.container` definition, you should use:
36+
37+
```
38+
GlobalArgs=--storage-opt=additionalimagestore=/usr/lib/bootc/storage
39+
```
40+
41+
## Pull secret
42+
43+
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.
44+
3145
## Limitations
3246

33-
- Currently, only the Image field of a `.image` or `.container` file is used to pull the image. Any other field is ignored.
34-
- There is no cleanup during rollback.
35-
- 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.
47+
- Currently, only the Image field of a `.image` or `.container` file is used to pull the image; per above not even `PullSecret=` is supported.
48+
- Images are not yet garbage collected

lib/src/boundimage.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@
55
//! pre-pulled (and in the future, pinned) before a new image root
66
//! is considered ready.
77
8-
use crate::task::Task;
98
use anyhow::{Context, Result};
109
use camino::Utf8Path;
1110
use cap_std_ext::cap_std::fs::Dir;
1211
use cap_std_ext::dirext::CapStdExtDirExt;
1312
use fn_error_context::context;
1413
use ostree_ext::containers_image_proxy;
1514
use ostree_ext::ostree::Deployment;
16-
use ostree_ext::sysroot::SysrootLock;
15+
16+
use crate::imgstorage::PullMode;
17+
use crate::store::Storage;
1718

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

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

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

136-
#[context("pull bound images")]
137-
pub(crate) fn pull_images(_deployment_root: &Dir, bound_images: Vec<BoundImage>) -> Result<()> {
137+
#[context("Pulling bound images")]
138+
pub(crate) async fn pull_images(sysroot: &Storage, bound_images: Vec<BoundImage>) -> Result<()> {
138139
tracing::debug!("Pulling bound images: {}", bound_images.len());
139140
//TODO: do this in parallel
140141
for bound_image in bound_images {
141-
let mut task = Task::new("Pulling bound image", "/usr/bin/podman")
142-
.arg("pull")
143-
.arg(&bound_image.image);
144-
if let Some(auth_file) = &bound_image.auth_file {
145-
task = task.arg("--authfile").arg(auth_file);
146-
}
147-
task.run()?;
142+
let image = &bound_image.image;
143+
let desc = format!("Updating bound image: {image}");
144+
crate::utils::async_task_with_spinner(&desc, async move {
145+
sysroot
146+
.imgstore
147+
.pull(&bound_image.image, PullMode::IfNotExists)
148+
.await
149+
})
150+
.await?;
148151
}
149152

150153
Ok(())

lib/src/cli.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,31 @@ pub(crate) enum ContainerOpts {
199199
Lint,
200200
}
201201

202+
/// Subcommands which operate on images.
203+
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
204+
pub(crate) enum ImageCmdOpts {
205+
/// Wrapper for `podman image list` in bootc storage.
206+
List {
207+
#[clap(allow_hyphen_values = true)]
208+
args: Vec<OsString>,
209+
},
210+
/// Wrapper for `podman image build` in bootc storage.
211+
Build {
212+
#[clap(allow_hyphen_values = true)]
213+
args: Vec<OsString>,
214+
},
215+
/// Wrapper for `podman image pull` in bootc storage.
216+
Pull {
217+
#[clap(allow_hyphen_values = true)]
218+
args: Vec<OsString>,
219+
},
220+
/// Wrapper for `podman image push` in bootc storage.
221+
Push {
222+
#[clap(allow_hyphen_values = true)]
223+
args: Vec<OsString>,
224+
},
225+
}
226+
202227
/// Subcommands which operate on images.
203228
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
204229
pub(crate) enum ImageOpts {
@@ -232,6 +257,16 @@ pub(crate) enum ImageOpts {
232257
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
233258
target: Option<String>,
234259
},
260+
/// Copy a container image from the default `containers-storage:` to the bootc-owned container storage.
261+
PullFromDefaultStorage {
262+
/// The image to pull
263+
image: String,
264+
},
265+
/// List fetched images stored in the bootc storage.
266+
///
267+
/// Note that these are distinct from images stored via e.g. `podman`.
268+
#[clap(subcommand)]
269+
Cmd(ImageCmdOpts),
235270
}
236271

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

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

439476
#[context("Querying root privilege")]
@@ -798,6 +835,27 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
798835
ImageOpts::CopyToStorage { source, target } => {
799836
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
800837
}
838+
ImageOpts::PullFromDefaultStorage { image } => {
839+
let sysroot = get_storage().await?;
840+
sysroot.imgstore.pull_from_host_storage(&image).await
841+
}
842+
ImageOpts::Cmd(opt) => {
843+
let sysroot = get_storage().await?;
844+
match opt {
845+
ImageCmdOpts::List { args } => {
846+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "list", &args).await
847+
}
848+
ImageCmdOpts::Build { args } => {
849+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "build", &args).await
850+
}
851+
ImageCmdOpts::Pull { args } => {
852+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "pull", &args).await
853+
}
854+
ImageCmdOpts::Push { args } => {
855+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "push", &args).await
856+
}
857+
}
858+
}
801859
},
802860
#[cfg(feature = "install")]
803861
Opt::Install(opts) => match opts {

lib/src/deploy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ pub(crate) async fn stage(
399399
)
400400
.await?;
401401

402-
crate::boundimage::pull_bound_images(sysroot, &deployment)?;
402+
crate::boundimage::pull_bound_images(sysroot, &deployment).await?;
403403

404404
crate::deploy::cleanup(sysroot).await?;
405405
println!("Queued for next boot: {:#}", spec.image);

lib/src/image.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,29 @@ use anyhow::{Context, Result};
66
use fn_error_context::context;
77
use ostree_ext::container::{ImageReference, Transport};
88

9+
use crate::{imgstorage::Storage, utils::CommandRunExt};
10+
911
/// The name of the image we push to containers-storage if nothing is specified.
1012
const IMAGE_DEFAULT: &str = "localhost/bootc";
1113

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

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

21+
println!("# Host images");
1922
for image in images {
2023
println!("{image}");
2124
}
25+
println!("");
26+
27+
println!("# Logically bound images");
28+
let mut listcmd = sysroot.imgstore.new_image_cmd()?;
29+
listcmd.arg("list");
30+
listcmd.run()?;
31+
2232
Ok(())
2333
}
2434

@@ -64,3 +74,16 @@ pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>)
6474
println!("Pushed: {target} {r}");
6575
Ok(())
6676
}
77+
78+
/// Thin wrapper for invoking `podman image <X>` but set up for our internal
79+
/// image store (as distinct from /var/lib/containers default).
80+
pub(crate) async fn imgcmd_entrypoint(
81+
storage: &Storage,
82+
arg: &str,
83+
args: &[std::ffi::OsString],
84+
) -> std::result::Result<(), anyhow::Error> {
85+
let mut cmd = storage.new_image_cmd()?;
86+
cmd.arg(arg);
87+
cmd.args(args);
88+
cmd.run()
89+
}

0 commit comments

Comments
 (0)