Skip to content

Commit 0527ca9

Browse files
committed
Initialize a containers-storage: owned by bootc, use for bound images
Closes: bootc-dev#721 - Initialize a containers-storage: instance at install time (that defaults to empty) - Open it at the same time we open the ostree repo/sysroot - Change bound images to use this We are *NOT* yet changing the base bootc image pull to use this. That's an obvious next step (xref bootc-dev#215 ) but will come later. Signed-off-by: Colin Walters <[email protected]>
1 parent 9e7bce8 commit 0527ca9

File tree

14 files changed

+550
-116
lines changed

14 files changed

+550
-116
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: 27 additions & 16 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.
@@ -26,8 +27,8 @@ const BOUND_IMAGE_DIR: &str = "usr/lib/bootc/bound-images.d";
2627
/// other pull options.
2728
#[derive(Debug, PartialEq, Eq)]
2829
pub(crate) struct BoundImage {
29-
image: String,
30-
auth_file: Option<String>,
30+
pub(crate) image: String,
31+
pub(crate) auth_file: Option<String>,
3132
}
3233

3334
#[derive(Debug, PartialEq, Eq)]
@@ -37,10 +38,18 @@ 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<()> {
42+
let bound_images = query_bound_images_for_deployment(sysroot, deployment)?;
43+
pull_images(sysroot, bound_images).await
44+
}
45+
46+
#[context("Querying bound images")]
47+
pub(crate) fn query_bound_images_for_deployment(
48+
sysroot: &Storage,
49+
deployment: &Deployment,
50+
) -> Result<Vec<BoundImage>> {
4151
let deployment_root = &crate::utils::deployment_fd(sysroot, deployment)?;
42-
let bound_images = query_bound_images(deployment_root)?;
43-
pull_images(deployment_root, bound_images)
52+
query_bound_images(deployment_root)
4453
}
4554

4655
#[context("Querying bound images")]
@@ -133,18 +142,20 @@ fn parse_container_file(file_contents: &tini::Ini) -> Result<BoundImage> {
133142
Ok(bound_image)
134143
}
135144

136-
#[context("pull bound images")]
137-
pub(crate) fn pull_images(_deployment_root: &Dir, bound_images: Vec<BoundImage>) -> Result<()> {
145+
#[context("Pulling bound images")]
146+
pub(crate) async fn pull_images(sysroot: &Storage, bound_images: Vec<BoundImage>) -> Result<()> {
138147
tracing::debug!("Pulling bound images: {}", bound_images.len());
139148
//TODO: do this in parallel
140149
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()?;
150+
let image = &bound_image.image;
151+
let desc = format!("Updating bound image: {image}");
152+
crate::utils::async_task_with_spinner(&desc, async move {
153+
sysroot
154+
.imgstore
155+
.pull(&bound_image.image, PullMode::IfNotExists)
156+
.await
157+
})
158+
.await?;
148159
}
149160

150161
Ok(())

lib/src/cli.rs

Lines changed: 65 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
@@ -247,6 +282,8 @@ pub(crate) enum InternalsOpts {
247282
FixupEtcFstab,
248283
/// Should only be used by `make update-generated`
249284
PrintJsonSchema,
285+
/// Perform cleanup actions
286+
Cleanup,
250287
}
251288

252289
impl InternalsOpts {
@@ -430,10 +467,12 @@ pub(crate) async fn get_locked_sysroot() -> Result<ostree_ext::sysroot::SysrootL
430467
Ok(sysroot)
431468
}
432469

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

439478
#[context("Querying root privilege")]
@@ -798,6 +837,27 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
798837
ImageOpts::CopyToStorage { source, target } => {
799838
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
800839
}
840+
ImageOpts::PullFromDefaultStorage { image } => {
841+
let sysroot = get_storage().await?;
842+
sysroot.imgstore.pull_from_host_storage(&image).await
843+
}
844+
ImageOpts::Cmd(opt) => {
845+
let sysroot = get_storage().await?;
846+
match opt {
847+
ImageCmdOpts::List { args } => {
848+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "list", &args).await
849+
}
850+
ImageCmdOpts::Build { args } => {
851+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "build", &args).await
852+
}
853+
ImageCmdOpts::Pull { args } => {
854+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "pull", &args).await
855+
}
856+
ImageCmdOpts::Push { args } => {
857+
crate::image::imgcmd_entrypoint(&sysroot.imgstore, "push", &args).await
858+
}
859+
}
860+
}
801861
},
802862
#[cfg(feature = "install")]
803863
Opt::Install(opts) => match opts {
@@ -831,6 +891,10 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
831891
serde_json::to_writer_pretty(&mut stdout, &schema)?;
832892
Ok(())
833893
}
894+
InternalsOpts::Cleanup => {
895+
let sysroot = get_storage().await?;
896+
crate::deploy::cleanup(&sysroot).await
897+
}
834898
},
835899
#[cfg(feature = "docgen")]
836900
Opt::Man(manopts) => crate::docgen::generate_manpages(&manopts.directory),

lib/src/deploy.rs

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//!
33
//! Create a merged filesystem tree with the image and mounted configmaps.
44
5+
use std::collections::HashSet;
56
use std::io::{BufRead, Write};
67

78
use anyhow::Ok;
@@ -268,53 +269,76 @@ pub(crate) async fn pull(
268269
Ok(Box::new((*import).into()))
269270
}
270271

272+
/// Gather all bound images in all deployments, then prune the image store,
273+
/// using the gathered images as the roots (that will not be GC'd).
274+
pub(crate) async fn prune_container_store(sysroot: &Storage) -> Result<()> {
275+
let deployments = sysroot.deployments();
276+
let mut all_bound_images = Vec::new();
277+
for deployment in deployments {
278+
let bound = crate::boundimage::query_bound_images_for_deployment(sysroot, &deployment)?;
279+
all_bound_images.extend(bound.into_iter());
280+
}
281+
// Convert to a hashset of just the image names
282+
let image_names = HashSet::from_iter(all_bound_images.iter().map(|img| img.image.as_str()));
283+
let pruned = sysroot.imgstore.prune_except_roots(&image_names).await?;
284+
tracing::debug!("Pruned images: {}", pruned.len());
285+
Ok(())
286+
}
287+
271288
pub(crate) async fn cleanup(sysroot: &Storage) -> Result<()> {
289+
let bound_prune = prune_container_store(sysroot);
290+
272291
// We create clones (just atomic reference bumps) here to move to the thread.
273292
let repo = sysroot.repo();
274293
let sysroot = sysroot.sysroot.clone();
275-
ostree_ext::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
276-
let locked_sysroot = &SysrootLock::from_assumed_locked(&sysroot);
277-
let cancellable = Some(cancellable);
278-
let repo = &repo;
279-
let txn = repo.auto_transaction(cancellable)?;
280-
let repo = txn.repo();
281-
282-
// Regenerate our base references. First, we delete the ones that exist
283-
for ref_entry in repo
284-
.list_refs_ext(
285-
Some(BASE_IMAGE_PREFIX),
286-
ostree::RepoListRefsExtFlags::NONE,
287-
cancellable,
288-
)
289-
.context("Listing refs")?
290-
.keys()
291-
{
292-
repo.transaction_set_refspec(ref_entry, None);
293-
}
294+
let repo_prune =
295+
ostree_ext::tokio_util::spawn_blocking_cancellable_flatten(move |cancellable| {
296+
let locked_sysroot = &SysrootLock::from_assumed_locked(&sysroot);
297+
let cancellable = Some(cancellable);
298+
let repo = &repo;
299+
let txn = repo.auto_transaction(cancellable)?;
300+
let repo = txn.repo();
301+
302+
// Regenerate our base references. First, we delete the ones that exist
303+
for ref_entry in repo
304+
.list_refs_ext(
305+
Some(BASE_IMAGE_PREFIX),
306+
ostree::RepoListRefsExtFlags::NONE,
307+
cancellable,
308+
)
309+
.context("Listing refs")?
310+
.keys()
311+
{
312+
repo.transaction_set_refspec(ref_entry, None);
313+
}
314+
315+
// Then, for each deployment which is derived (e.g. has configmaps) we synthesize
316+
// a base ref to ensure that it's not GC'd.
317+
for (i, deployment) in sysroot.deployments().into_iter().enumerate() {
318+
let commit = deployment.csum();
319+
if let Some(base) = get_base_commit(repo, &commit)? {
320+
repo.transaction_set_refspec(&format!("{BASE_IMAGE_PREFIX}/{i}"), Some(&base));
321+
}
322+
}
294323

295-
// Then, for each deployment which is derived (e.g. has configmaps) we synthesize
296-
// a base ref to ensure that it's not GC'd.
297-
for (i, deployment) in sysroot.deployments().into_iter().enumerate() {
298-
let commit = deployment.csum();
299-
if let Some(base) = get_base_commit(repo, &commit)? {
300-
repo.transaction_set_refspec(&format!("{BASE_IMAGE_PREFIX}/{i}"), Some(&base));
324+
let pruned =
325+
ostree_container::deploy::prune(locked_sysroot).context("Pruning images")?;
326+
if !pruned.is_empty() {
327+
let size = glib::format_size(pruned.objsize);
328+
println!(
329+
"Pruned images: {} (layers: {}, objsize: {})",
330+
pruned.n_images, pruned.n_layers, size
331+
);
332+
} else {
333+
tracing::debug!("Nothing to prune");
301334
}
302-
}
303335

304-
let pruned = ostree_container::deploy::prune(locked_sysroot).context("Pruning images")?;
305-
if !pruned.is_empty() {
306-
let size = glib::format_size(pruned.objsize);
307-
println!(
308-
"Pruned images: {} (layers: {}, objsize: {})",
309-
pruned.n_images, pruned.n_layers, size
310-
);
311-
} else {
312-
tracing::debug!("Nothing to prune");
313-
}
336+
Ok(())
337+
});
314338

315-
Ok(())
316-
})
317-
.await
339+
// We run these in parallel mostly because we can.
340+
tokio::try_join!(repo_prune, bound_prune)?;
341+
Ok(())
318342
}
319343

320344
/// If commit is a bootc-derived commit (e.g. has configmaps), return its base.
@@ -399,7 +423,7 @@ pub(crate) async fn stage(
399423
)
400424
.await?;
401425

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

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

0 commit comments

Comments
 (0)