Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: Add a new bootc image subcommand #620

Merged
merged 3 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)" } },
}
}
Loading