Skip to content

Commit

Permalink
chunked-oci: Rework to support --from too
Browse files Browse the repository at this point in the history
Since containers/buildah#5952 broke
the Containerfile flow, let's add a workflow (that we wanted
to support anyways) to operate outside of a container build.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Jan 31, 2025
1 parent ef89de2 commit 73fe7d2
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 103 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,19 @@ jobs:
with:
name: install-c9s.tar
- name: Integration tests
run: cd tests/build-chunked-oci && tar -xzvf ../../install.tar && podman build -v $(pwd)/usr/bin:/ci -t localhost/test .
run: |
set -xeuo pipefail
cd tests/build-chunked-oci
# Something is confused in latest GHA here, I was getting: Error: database graph driver "" does not match our graph driver "overlay": database configuration mismatch
sudo rm /var/lib/containers -rf
sudo podman build -t localhost/base -f Containerfile.test
sudo tar -xzvf ../../install.tar
sudo podman build -v $(pwd)/usr/bin:/ci -t localhost/builder -f Containerfile.builder
sudo podman run --rm --privileged --security-opt=label=disable \
-v /var/lib/containers:/var/lib/containers \
-v /var/tmp:/var/tmp \
localhost/builder rpm-ostree experimental compose build-chunked-oci --bootc --format-version=1 --from localhost/base --output containers-storage:localhost/base-chunked
sudo podman inspect localhost/base-chunked
build-c9s:
name: "Build (c9s)"
runs-on: ubuntu-latest
Expand Down
70 changes: 11 additions & 59 deletions docs/experimental-build-chunked-oci.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,72 +11,24 @@ However, it does not support common container-native workflows such
as copying content from a distinct container image.

The `rpm-ostree experimental compose build-chunked-oci` command
accepts an arbitrary root filesystem, and synthesizes an OSTree-based
container from it.
accepts an arbitrary input container image, and synthesizes a
bootc (ostree container) ready image from it.

At the current time, it is recommended that the input
root filesystem be derived from a reference maintained base image,
container image be derived from a reference maintained base image,
such as the fedora-bootc ones. Especially if you are
targeting bootc systems with this, please trakc
targeting bootc systems with this, please follow
<https://gitlab.com/fedora/bootc/tracker/-/issues/32>.

## Example as part of a Containerfile
## Running

This relies on a podman-ecosystem specific feature: `FROM oci:`
which allows ingesting into the container build flow an OCI
archive built inside a stage. With this, we can generate
arbitrary container structure, in particular "chunked"
images. A bit more in [container.md](container).
This command expects a container image already fetched into a `containers-storage:`
instance, and can output to `containers-storage:` or `oci`.

In this example, we will dramatically trim out the current reference
base image, including especially the rpm-ostree and dnf stacks.

```Dockerfile
FROM quay.io/fedora/fedora-bootc:rawhide as rootfs
RUN <<EORUN
set -xeuo pipefail
# Remove some high level superfulous stuff
dnf -y remove sos NetworkManager-tui vim nano
# And this only targets VMs, so flush out all firmware
rpm -qa --queryformat=%{NAME} | grep -Fe '-firmware-' | xargs dnf -y remove
# We don't want any python, and we don't need rpm-ostree either.
dnf -y remove python3 rpm-ostree{,-libs}
bootc container lint
EORUN

# This builder image can be anything as long as it has a new enough
# rpm-ostree.
FROM quay.io/fedora/fedora-bootc:rawhide as builder
RUN --mount=type=bind,rw=true,src=.,dst=/buildcontext,bind-propagation=shared \
--mount=from=rootfs,dst=/rootfs <<EORUN
set -xeuo pipefail
rm /buildcontext/out.oci -rf
rpm-ostree experimental compose build-chunked-oci --bootc --format-version=1 \
--rootfs=/rootfs --output /buildcontext/out.oci
EORUN

# Finally, output the OCI archive back into our final container image. Here we
# can add labels and other metadata - note that no metadata was inherited from
# the source image - only the root filesystem!
FROM oci:./out.oci
# Need to reference builder here to force ordering. But since we have to run
# something anyway, we might as well cleanup after ourselves.
RUN --mount=type=bind,from=builder,src=.,target=/var/tmp \
--mount=type=bind,rw=true,src=.,dst=/buildcontext,bind-propagation=shared rm /buildcontext/out.oci -rf
```

## Using outside of container builds

There is no requirement to run as part of a container build, or even a container.
You can generate a root filesystem however you want, and get an OCI archive
out, which can be pushed directly to a registry using a tool such as `skopeo`.

```
mkdir -p rootfs
podman build -t quay.io/exampleos/exampleos:build ...
...
rpm-ostree experimental compose build-chunked-oci --bootc --format-version=1 \
--rootfs=rootfs --output out.oci
skopeo copy --authfile=/path/to/auth.json oci:out.oci docker://quay.io/exampleos/exampleos:latest
--from=quay.io/exampleos/exampleos:build --output containers-storage:quay.io/exampleos/exampleos:latest
podman push quay.io/exampleos/exampleos:latest
```

However as noted above, it is recommended to follow e.g. the
[fedora-bootc documentation](https://docs.fedoraproject.org/en-US/bootc/) around custom base images.
148 changes: 136 additions & 12 deletions rust/src/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::os::fd::{AsFd, AsRawFd};
use std::process::Command;
use std::process::{Command, Stdio};

use anyhow::{anyhow, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
Expand Down Expand Up @@ -157,8 +158,12 @@ struct Opt {
#[derive(Debug, Parser)]
pub(crate) struct BuildChunkedOCI {
/// Path to the source root filesystem tree.
#[clap(long, required = true)]
rootfs: Utf8PathBuf,
#[clap(long, required_unless_present = "from")]
rootfs: Option<Utf8PathBuf>,

/// Use the provided image (in containers-storage).
#[clap(long, required_unless_present = "rootfs")]
from: Option<String>,

/// If set, configure the output OCI image to be a bootc container.
/// At the current time this option is required.
Expand All @@ -175,16 +180,113 @@ pub(crate) struct BuildChunkedOCI {
#[clap(long, default_value = "latest")]
reference: String,

/// Path to an OCI directory. Will be created if nonexistent.
/// Output image reference, in TRANSPORT:TARGET syntax.
/// For example, `containers-storage:localhost/exampleos` or `oci:/path/to/ocidir`.
#[clap(long, required = true)]
output: Utf8PathBuf,
output: String,
}

struct PodmanMount {
path: Utf8PathBuf,
temp_cid: Option<String>,
mounted: bool,
}

impl PodmanMount {
#[context("Unmounting container")]
fn _impl_unmount(&mut self) -> Result<()> {
if self.mounted {
tracing::debug!("unmounting {}", self.path.as_str());
self.mounted = false;
Command::new("umount")
.args(["-l", self.path.as_str()])
.stdout(Stdio::null())
.run()
.context("umount")?;
tracing::trace!("umount ok");
}
if let Some(cid) = self.temp_cid.take() {
tracing::debug!("rm container {cid}");
Command::new("podman")
.args(["rm", cid.as_str()])
.stdout(Stdio::null())
.run()
.context("podman rm")?;
tracing::trace!("rm ok");
}
Ok(())
}

#[context("Mounting continer {container}")]
fn _impl_mount(container: &str) -> Result<Utf8PathBuf> {
let mut o = Command::new("podman")
.args(["mount", container])
.run_get_output()?;
let mut s = String::new();
o.read_to_string(&mut s)?;
while s.ends_with('\n') {
s.pop();
}
tracing::debug!("mounted container {container} at {s}");
Ok(s.into())
}

#[allow(dead_code)]
fn new_for_container(container: &str) -> Result<Self> {
let path = Self::_impl_mount(container)?;
Ok(Self {
path,
temp_cid: None,
mounted: true,
})
}

fn new_for_image(image: &str) -> Result<Self> {
let mut o = Command::new("podman")
.args(["create", image])
.run_get_output()?;
let mut s = String::new();
o.read_to_string(&mut s)?;
let cid = s.trim();
let path = Self::_impl_mount(cid)?;
tracing::debug!("created container {cid} from {image}");
Ok(Self {
path,
temp_cid: Some(cid.to_owned()),
mounted: true,
})
}

fn unmount(mut self) -> Result<()> {
self._impl_unmount()
}
}

impl Drop for PodmanMount {
fn drop(&mut self) {
tracing::trace!("In drop, mounted={}", self.mounted);
let _ = self._impl_unmount();
}
}

impl BuildChunkedOCI {
pub(crate) fn run(self) -> Result<()> {
anyhow::ensure!(self.rootfs.as_path() != "/");
let rootfs = &Dir::open_ambient_dir(self.rootfs.as_path(), cap_std::ambient_authority())
.with_context(|| format!("Opening {}", self.rootfs))?;
enum FileSource {
Rootfs(Utf8PathBuf),
Podman(PodmanMount),
}
let rootfs_source = if let Some(rootfs) = self.rootfs {
FileSource::Rootfs(rootfs)
} else {
let image = self.from.as_deref().unwrap();
FileSource::Podman(PodmanMount::new_for_image(image)?)
};
let rootfs = match &rootfs_source {
FileSource::Rootfs(p) => p.as_path(),
FileSource::Podman(mnt) => mnt.path.as_path(),
};
let rootfs = Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())
.with_context(|| format!("Opening {}", rootfs))?;
// These must be set to exactly this; the CLI parser requires it.
assert!(self.bootc);
assert_eq!(self.format_version, 1);
Expand All @@ -205,14 +307,23 @@ impl BuildChunkedOCI {
// It's only the tests that override
let modifier =
ostree::RepoCommitModifier::new(ostree::RepoCommitModifierFlags::empty(), None);
let commitid = generate_commit_from_rootfs(&repo, rootfs, modifier)?;

let imgref = format!("oci:{}:{}", self.output.as_str(), self.reference.as_str());
let commitid = generate_commit_from_rootfs(&repo, &rootfs, modifier)?;

let label_arg = self
.bootc
.then_some(["--label", "containers.bootc=1"].as_slice())
.unwrap_or_default();
let config_data = if let Some(image) = self.from.as_deref() {
let img_transport = format!("containers-storage:{image}");
let tmpf = tempfile::NamedTempFile::new()?;
Command::new("skopeo")
.args(["inspect", "--config", img_transport.as_str()])
.stdout(tmpf.as_file().try_clone()?)
.run()?;
Some(tmpf.into_temp_path())
} else {
None
};
crate::isolation::self_command()
.args([
"compose",
Expand All @@ -221,10 +332,23 @@ impl BuildChunkedOCI {
repo_path.as_str(),
])
.args(label_arg)
.args([commitid.as_str(), imgref.as_str()])
.args(
config_data
.iter()
.flat_map(|c| [OsStr::new("--image-config"), c.as_os_str()]),
)
.args([commitid.as_str(), self.output.as_str()])
.run()
.context("Invoking compose container-encapsulate")?;

drop(rootfs);
match rootfs_source {
FileSource::Rootfs(_) => {}
FileSource::Podman(mnt) => {
mnt.unmount().context("Final mount cleanup")?;
}
}

Ok(())
}
}
Expand Down
31 changes: 0 additions & 31 deletions tests/build-chunked-oci/Containerfile

This file was deleted.

8 changes: 8 additions & 0 deletions tests/build-chunked-oci/Containerfile.builder
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Note that the GHA flow in ci.yml injects a binary from C9S.
FROM quay.io/centos-bootc/centos-bootc:stream9
RUN <<EORUN
set -xeuo pipefail
# Pull in the binary we just built; if you're doing this locally you'll want
# to e.g. run `podman build -v target/release/rpm-ostree:/ci/rpm-ostree`.
install /ci/rpm-ostree /usr/bin/
EORUN
12 changes: 12 additions & 0 deletions tests/build-chunked-oci/Containerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM quay.io/fedora/fedora-bootc:41
RUN <<EORUN
set -xeuo pipefail
mkdir -p /var/lib/rpm-state
# Remove some high level superfulous stuff
dnf -y remove sos NetworkManager-tui vim nano
# We don't want any python, and we don't need rpm-ostree either.
dnf -y remove python3 rpm-ostree{,-libs}
dnf clean all
rm /var/lib/rpm -rf
bootc container lint
EORUN

0 comments on commit 73fe7d2

Please sign in to comment.