Skip to content

Commit

Permalink
Merge pull request #620 from cgwalters/container-push
Browse files Browse the repository at this point in the history
cli: Add a new bootc image subcommand
  • Loading branch information
cgwalters authored Jun 28, 2024
2 parents 72f9013 + 83a6a15 commit 71b87ba
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 1 deletion.
2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ anstream = "0.6.13"
anstyle = "1.0.6"
anyhow = "1.0.82"
camino = { version = "1.1.6", features = ["serde1"] }
ostree-ext = { version = "0.14.0" }
ostree-ext = { version = "0.14.0" }
chrono = { version = "0.4.38", features = ["serde"] }
clap = { version= "4.5.4", features = ["derive","cargo"] }
clap_mangen = { version = "0.2.20", optional = true }
Expand Down
48 changes: 48 additions & 0 deletions lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use fn_error_context::context;
use ostree::gio;
use ostree_container::store::PrepareResult;
use ostree_ext::container as ostree_container;
use ostree_ext::container::Transport;
use ostree_ext::keyfileext::KeyFileExt;
use ostree_ext::ostree;

Expand Down Expand Up @@ -191,6 +192,41 @@ pub(crate) enum ContainerOpts {
Lint,
}

/// Subcommands which operate on images.
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum ImageOpts {
/// List fetched images stored in the bootc storage.
///
/// Note that these are distinct from images stored via e.g. `podman`.
List,
/// Copy a container image from the bootc storage to `containers-storage:`.
///
/// The source and target are both optional; if both are left unspecified,
/// via a simple invocation of `bootc image copy-to-storage`, then the default is to
/// push the currently booted image to `containers-storage` (as used by podman, etc.)
/// and tagged with the image name `localhost/bootc`,
///
/// ## Copying a non-default container image
///
/// It is also possible to copy an image other than the currently booted one by
/// specifying `--source`.
///
/// ## Pulling images
///
/// At the current time there is no explicit support for pulling images other than indirectly
/// via e.g. `bootc switch` or `bootc upgrade`.
CopyToStorage {
#[clap(long)]
/// The source image; if not specified, the booted image will be used.
source: Option<String>,

#[clap(long)]
/// The destination; if not specified, then the default is to push to `containers-storage:localhost/bootc`;
/// this will make the image accessible via e.g. `podman run localhost/bootc` and for builds.
target: Option<String>,
},
}

/// Hidden, internal only options
#[derive(Debug, clap::Subcommand, PartialEq, Eq)]
pub(crate) enum InternalsOpts {
Expand Down Expand Up @@ -321,6 +357,12 @@ pub(crate) enum Opt {
/// Operations which can be executed as part of a container build.
#[clap(subcommand)]
Container(ContainerOpts),
/// Operations on container images
///
/// Stability: This interface is not declared stable and may change or be removed
/// at any point in the future.
#[clap(subcommand, hide = true)]
Image(ImageOpts),
/// Execute the given command in the host mount namespace
#[cfg(feature = "install")]
#[clap(hide = true)]
Expand Down Expand Up @@ -732,6 +774,12 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
Ok(())
}
},
Opt::Image(opts) => match opts {
ImageOpts::List => crate::image::list_entrypoint().await,
ImageOpts::CopyToStorage { source, target } => {
crate::image::push_entrypoint(source.as_deref(), target.as_deref()).await
}
},
#[cfg(feature = "install")]
Opt::Install(opts) => match opts {
InstallOpts::ToDisk(opts) => crate::install::install_to_disk(opts).await,
Expand Down
66 changes: 66 additions & 0 deletions lib/src/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! # Controlling bootc-managed images
//!
//! APIs for operating on container images in the bootc storage.
use anyhow::{Context, Result};
use fn_error_context::context;
use ostree_ext::container::{ImageReference, Transport};

/// 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 repo = &sysroot.repo();

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

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

/// Implementation of `bootc image push-to-storage`.
#[context("Pushing image")]
pub(crate) async fn push_entrypoint(source: Option<&str>, target: Option<&str>) -> Result<()> {
let transport = Transport::ContainerStorage;
let sysroot = crate::cli::get_locked_sysroot().await?;

let repo = &sysroot.repo();

// If the target isn't specified, push to containers-storage + our default image
let target = if let Some(target) = target {
ImageReference {
transport,
name: target.to_owned(),
}
} else {
ImageReference {
transport: Transport::ContainerStorage,
name: IMAGE_DEFAULT.to_string(),
}
};

// If the source isn't specified, we use the booted image
let source = if let Some(source) = source {
ImageReference::try_from(source).context("Parsing source image")?
} else {
let status = crate::status::get_status_require_booted(&sysroot)?;
// SAFETY: We know it's booted
let booted = status.2.status.booted.unwrap();
let booted_image = booted.image.unwrap().image;
ImageReference {
transport: Transport::try_from(booted_image.transport.as_str()).unwrap(),
name: booted_image.image,
}
};
let mut opts = ostree_ext::container::store::ExportToOCIOpts::default();
opts.progress_to_stdout = true;
println!("Copying local image {source} to {target} ...");
let r = ostree_ext::container::store::export(repo, &source, &target, Some(opts)).await?;

println!("Pushed: {target} {r}");
Ok(())
}
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
pub mod cli;
pub(crate) mod deploy;
pub(crate) mod generator;
mod image;
pub(crate) mod journal;
pub(crate) mod kargs;
mod lints;
Expand Down
129 changes: 129 additions & 0 deletions tests/booted/002-test-image-pushpull-upgrade.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# This test does:
# bootc image copy-to-storage
# podman build <from that image>
# bootc switch <to the local image>
# <verify booted state>
# Then another build, and reboot into verifying that
use std assert
use tap.nu

const kargsv0 = ["testarg=foo", "othertestkarg", "thirdkarg=bar"]
const kargsv1 = ["testarg=foo", "thirdkarg=baz"]
let removed = ($kargsv0 | filter { not ($in in $kargsv1) })

# This code runs on *each* boot.
# Here we just capture information.
bootc status
let st = bootc status --json | from json
let booted = $st.status.booted.image

# Parse the kernel commandline into a list.
# This is not a proper parser, but good enough
# for what we need here.
def parse_cmdline [] {
open /proc/cmdline | str trim | split row " "
}

# Run on the first boot
def initial_build [] {
tap begin "local image push + pull + upgrade"

let td = mktemp -d
cd $td

do --ignore-errors { podman image rm localhost/bootc o+e>| ignore }
bootc image copy-to-storage
let img = podman image inspect localhost/bootc | from json

mkdir usr/lib/bootc/kargs.d
{ kargs: $kargsv0 } | to toml | save usr/lib/bootc/kargs.d/05-testkargs.toml
# A simple derived container that adds a file, but also injects some kargs
"FROM localhost/bootc
COPY usr/ /usr/
RUN echo test content > /usr/share/blah.txt
" | save Dockerfile
# Build it
podman build -t localhost/bootc-derived .
# Just sanity check it
let v = podman run --rm localhost/bootc-derived cat /usr/share/blah.txt | str trim
assert equal $v "test content"
# Now, fetch it back into the bootc storage!
bootc switch --transport containers-storage localhost/bootc-derived
# And reboot into it
tmt-reboot
}

# The second boot; verify we're in the derived image
def second_boot [] {
print "verifying second boot"
# booted from the local container storage and image
assert equal $booted.image.transport containers-storage
assert equal $booted.image.image localhost/bootc-derived
# We wrote this file
let t = open /usr/share/blah.txt | str trim
assert equal $t "test content"

# Verify we have updated kargs
let cmdline = parse_cmdline
print $"cmdline=($cmdline)"
for x in $kargsv0 {
print $"verifying karg: ($x)"
assert ($x in $cmdline)
}

# Now do another build where we drop one of the kargs
let td = mktemp -d
cd $td

mkdir usr/lib/bootc/kargs.d
{ kargs: $kargsv1 } | to toml | save usr/lib/bootc/kargs.d/05-testkargs.toml
"FROM localhost/bootc
COPY usr/ /usr/
RUN echo test content2 > /usr/share/blah.txt
" | save Dockerfile
# Build it
podman build -t localhost/bootc-derived .
let booted_digest = $booted.imageDigest
print booted_digest = $booted_digest
# We should already be fetching updates from container storage
bootc upgrade
# Verify we staged an update
let st = bootc status --json | from json
let staged_digest = $st.status.staged.image.imageDigest
assert ($booted_digest != $staged_digest)
# And reboot into the upgrade
tmt-reboot
}

# Check we have the updated kargs
def third_boot [] {
print "verifying third boot"
assert equal $booted.image.transport containers-storage
assert equal $booted.image.image localhost/bootc-derived
let t = open /usr/share/blah.txt | str trim
assert equal $t "test content2"

# Verify we have updated kargs
let cmdline = parse_cmdline
print $"cmdline=($cmdline)"
for x in $kargsv1 {
print $"Verifying karg ($x)"
assert ($x in $cmdline)
}
# And the kargs that should be removed are gone
for x in $removed {
assert not ($removed in $cmdline)
}

tap ok
}

def main [] {
# See https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test
match $env.TMT_REBOOT_COUNT? {
null | "0" => initial_build,
"1" => second_boot,
"2" => third_boot,
$o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } },
}
}

0 comments on commit 71b87ba

Please sign in to comment.