Merge branch 'custom-base-target' into 'main'

Draft: Add bootc-base-imagectl onto the main branch

See merge request fedora/bootc/base-images!98
This commit is contained in:
Colin Walters (Red Hat) 2025-02-21 23:19:59 +00:00
commit 80e0aa085a
40 changed files with 326 additions and 80 deletions

View File

@ -1,14 +1,32 @@
--- ---
include: stages:
- remote: https://gitlab.com/platform-engineering-org/gitlab-ci/-/raw/main/templates/build-image.gitlab-ci.yml - build
variables:
IMAGE: ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHA}
CONTAINERFILE: Containerfile
CONTEXT: .
EXTRA_ARGS: ""
.build-image:
stage: build
image: quay.io/buildah/stable:v1.38.0
needs: []
script: buildah bud -f ${CONTAINERFILE} --no-cache -t ${IMAGE} ${EXTRA_ARGS} ${CONTEXT}
rules:
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
build-image: build-image:
extends: .build-image extends: .build-image
parallel: parallel:
matrix: matrix:
- TIER: [tier-0, tier-1, tier-x] - TIER: [minimal, standard, tier-x]
variables: variables:
EXTRA_ARGS: "--security-opt=label=disable --cap-add=all --build-arg MANIFEST=fedora-$TIER.yaml" EXTRA_ARGS: "--security-opt=label=disable --cap-add=all --build-arg MANIFEST=fedora-$TIER"
rules: rules:
- if: $CI_PROJECT_NAMESPACE != "fedora/bootc" - if: $CI_PROJECT_NAMESPACE != "fedora/bootc"
when: never when: never

View File

@ -1,36 +1,17 @@
# This container build uses some special features of podman that allow # In order to make a base image as part of a Dockerfile, this container build uses
# a process executing as part of a container build to generate a new container # nested containerization, so you must build with e.g.
# image "from scratch".
#
# This container build uses nested containerization, so you must build with e.g.
# podman build --security-opt=label=disable --cap-add=all --device /dev/fuse <...> # podman build --security-opt=label=disable --cap-add=all --device /dev/fuse <...>
#
# # Why are we doing this? # NOTE: This container build will output a single giant layer. It is strongly recommended
# # to run the "rechunker" on the output of this build, see
# Today this base image build process uses rpm-ostree. There is a lot of things that # https://coreos.github.io/rpm-ostree/experimental-build-chunked-oci/
# rpm-ostree does when generating a container image...but important parts include:
#
# - auto-updating labels in the container metadata
# - Generating "chunked" content-addressed reproducible image layers (notice
# how there are ~60 layers in the generated image)
#
# The latter bit in particular is currently impossible to do from Containerfile.
# A future goal is adding some support for this in a way that can be honored by
# buildah (xref https://github.com/containers/podman/discussions/12605)
#
# # Why does this build process require additional privileges?
#
# Because it's generating a base image and uses containerization features itself.
# In the future some of this can be lifted.
FROM quay.io/fedora/fedora:rawhide as repos FROM quay.io/fedora/fedora:rawhide as repos
# BOOTSTRAPPING: This can be any image that has rpm-ostree and selinux-policy-targeted. # BOOTSTRAPPING: This can be any image that has rpm-ostree and selinux-policy-targeted.
FROM quay.io/fedora/fedora:rawhide as builder FROM quay.io/fedora/fedora:rawhide as builder
RUN dnf -y install rpm-ostree selinux-policy-targeted RUN dnf -y install rpm-ostree selinux-policy-targeted
ARG MANIFEST=fedora-bootc.yaml ARG MANIFEST=fedora-standard
COPY --from=repos /etc/dnf/vars /etc/dnf/vars
COPY --from=repos /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-* /etc/pki/rpm-gpg
# The input git repository has .repo files committed to git rpm-ostree has historically # The input git repository has .repo files committed to git rpm-ostree has historically
# emphasized that. But here, we are fetching the repos from the container base image. # emphasized that. But here, we are fetching the repos from the container base image.
# So copy the source, and delete the hardcoded ones in git, and use the container base # So copy the source, and delete the hardcoded ones in git, and use the container base
@ -38,17 +19,29 @@ COPY --from=repos /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-* /etc/pki/rpm-gpg
COPY . /src COPY . /src
WORKDIR /src WORKDIR /src
RUN rm -vf /src/*.repo RUN rm -vf /src/*.repo
COPY --from=repos /etc/yum.repos.d/*.repo /src
RUN --mount=type=cache,target=/workdir \ RUN --mount=type=cache,target=/workdir \
--mount=type=bind,rw=true,src=.,dst=/buildcontext,bind-propagation=shared \ --mount=type=bind,rw,from=repos,src=/,dst=/repos <<EORUN
--mount=type=bind,from=repos,src=/,dst=/repos \ set -xeuo pipefail
rpm-ostree compose image --image-config fedora-bootc-config.json \ # Put our manifests into the builder image in the same location they'll be in the
--cachedir=/workdir --format=ociarchive --initialize ${MANIFEST} \ # final image.
--source-root=/repos /buildcontext/out.ociarchive ./install-manifests
# Verify that listing works
/usr/libexec/bootc-base-imagectl list >/dev/null
# Run the build script in the same way we expect custom images to do, and also
# "re-inject" the manifests into the target, so secondary container builds can use it.
/usr/libexec/bootc-base-imagectl build-rootfs --reinject --manifest=${MANIFEST} /repos /target-rootfs
EORUN
FROM oci-archive:./out.ociarchive # This pulls in the rootfs generated in the previous step
# Need to reference builder here to force ordering. But since we have to run FROM scratch
# something anyway, we might as well cleanup after ourselves. COPY --from=builder /target-rootfs/ /
RUN --mount=type=bind,from=builder,src=.,target=/var/tmp \ LABEL containers.bootc 1
--mount=type=bind,rw=true,src=.,dst=/buildcontext,bind-propagation=shared \ # This is an ad-hoc way for us to reference bootc-image-builder in
rm /buildcontext/out.ociarchive # a way that in theory client tooling can inspect and find. Today
# it isn't widely used.
LABEL bootc.diskimage-builder quay.io/centos-bootc/bootc-image-builder
# https://pagure.io/fedora-kiwi-descriptions/pull-request/52
ENV container=oci
# Make systemd the default
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

View File

@ -30,7 +30,7 @@ podman build --security-opt=label=disable --cap-add=all \
--device /dev/fuse -t localhost/fedora-bootc . --device /dev/fuse -t localhost/fedora-bootc .
``` ```
See the `Containerfile` for more details. This builds the default `tier-1` image. See the `Containerfile` for more details. This builds the default `standard` image.
## Fedora versions ## Fedora versions
@ -58,21 +58,20 @@ It is planned to rework and improve this in the future, especially
to support smaller custom images. For more on this, see to support smaller custom images. For more on this, see
[this tracker issue](https://gitlab.com/fedora/bootc/tracker/-/issues/32). [this tracker issue](https://gitlab.com/fedora/bootc/tracker/-/issues/32).
- **tier-1**: This image is the default, what is published as - **standard**: This image is the default, what is published as
https://quay.io/repository/fedora/fedora-bootc https://quay.io/repository/fedora/fedora-bootc
- **tier-0**: This content set is more of a convenient centralization point for CI - **minimal**: This content set is more of a convenient centralization point for CI
and curation around a package set that we can all agree is the rough minimum and curation around a package set that is intended as a starting point fror
necessary for a usable system. It's not meant to be used as is, but layered a container base image.
upon.
- **tier-x**: This content set is the shared base used by all image-based - **tier-x**: This content set is the shared base used by all image-based
Fedora variants (IoT, Atomic Desktops, and CoreOS). Fedora variants (IoT, Atomic Desktops, and CoreOS).
Changes to this tier may be done without accounting for external users. Changes to this tier may be done without accounting for external users.
To build this, pass `--build-arg=MANIFEST=fedora-tier-x.yaml` to the build To build this, pass `--build-arg=MANIFEST=fedora-tier-x.yaml` to the build
command above. command above.
**tier-1** inherits from **tier-x** and **tier-x** in turn inherit from **tier-0**. **standard** inherits from **tier-x** and **tier-x** in turn inherit from **minimal**.
All non-trivial changes to **tier-0** and **tier-x** should be ACKed by at least All non-trivial changes to **minimal** and **tier-x** should be ACKed by at least
one stakeholder of each Fedora variant WGs. one stakeholder of each Fedora variant WGs.
## More information ## More information

87
bootc-base-imagectl Executable file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
import os
import os.path as path
import subprocess
import shutil
import json
import argparse
import sys
MANIFESTDIR = 'usr/share/doc/bootc-base-imagectl/manifests'
def run_build_rootfs(args):
"""
Regenerates a base image using a build configuration.
"""
target = args.target
if os.path.isdir(args.manifest):
manifest_path = os.path.join(args.manifest, 'manifest.yaml')
else:
manifest_path = args.manifest + '.yaml'
try:
# Perform the build
subprocess.run([
'rpm-ostree',
'experimental',
'compose',
'rootfs',
f'--source-root-rw={args.source_root}',
f'/{MANIFESTDIR}/{manifest_path}',
target,
], check=True)
# And run the bootc linter for good measure
subprocess.run([
'bootc',
'container',
'lint',
f'--rootfs={target}',
], check=True)
except subprocess.CalledProcessError as e:
print(f"Error executing command: {e}")
sys.exit(1)
# Copy our own build configuration into the target if configured;
# this is used for the first stage build. But by default *secondary*
# builds don't get this.
if args.reinject:
for d in [MANIFESTDIR]:
dst = path.join(target, d)
print(f"Copying /{d} to {dst}")
shutil.copytree('/' + d, dst)
for f in ['usr/libexec/bootc-base-imagectl']:
dst = path.join(target, f)
print(f"Copying /{f} to {dst}")
shutil.copy('/' + f, dst)
def run_list(args):
d = '/' + MANIFESTDIR
for ent in sorted(os.listdir(d)):
name, ext = os.path.splitext(ent)
if ext != '.yaml':
continue
fullpath = os.path.join(d, ent)
if os.path.islink(fullpath):
continue
o = subprocess.check_output(['rpm-ostree', 'compose', 'tree', '--print-only', fullpath])
manifest = json.loads(o)
description = manifest['metadata']['summary']
print(f"{name}: {description}")
print("---")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Operate on the build configuration for this container")
subparsers = parser.add_subparsers(help='Subcommands', required=True)
build_rootfs = subparsers.add_parser('build-rootfs', help='Generate a container root filesystem')
build_rootfs.add_argument("--reinject", help="Also reinject the build configurations into the target", action='store_true')
build_rootfs.add_argument("--manifest", help="Use the specified manifest", action='store', default='default')
build_rootfs.add_argument("source_root", help="Path to the source root directory used for dnf configuration.")
build_rootfs.add_argument("target", help="Path to the target root directory that will be generated.")
build_rootfs.set_defaults(func=run_build_rootfs)
cmd_list = subparsers.add_parser('list', help='List available manifests')
cmd_list.set_defaults(func=run_list)
args = parser.parse_args()
args.func(args)

99
bootc-base-imagectl.md Normal file
View File

@ -0,0 +1,99 @@
# bootc-base-imagectl
A core premise of the bootc model is that rich
control over Linux system customization can be accomplished
with a "default" container build:
```
FROM <base image>
RUN ...
```
As of recently, it is possible to e.g. swap the kernel
and other fundamental components as part of default derivation.
However, some use cases want even more control - for example,
as an organization deploying a bootc system, I may want to ensure
the base image version carries a set of packages at
exactly specific versions (perhaps defined by a lockfile,
or an rpm-md repository). There are many tools which
manage snapshots of yum (rpm-md) repositories.
There are currently issues where it won't quite work to e.g.
`dnf -y upgrade selinux-policy-targeted`.
The `/usr/libexec/bootc-base-imagectl` tool which is
included in the base image is designed to enable building
a root filesystem in ostree-container format from a set
of RPMs controlled by the user.
## Understanding the base image content
Most, but not all content from the base image comes from RPMs.
There is some additional non-RPM content, as well as postprocessing
that operates on the filesystem root. At the current time the
implementation of the base image build uses `rpm-ostree`,
but this is considered an implementation detail subject to change.
## Using bootc-base-imagectl build-rootfs
The core operation is `bootc-base-imagectl build-rootfs`.
This command takes just two arguments:
- A "source root" which should have an `/etc/yum.repos.d`
that defines the input RPM content. This source root is also used
to control things like the `$releasever`.
- A path to the target root filesystem which will be generated as
a directory. The target should not already exist (but its parent must exist).
### Other options
`bootc-base-imagectl list` will enumerate available configurations that
can be selected by passing `--manifest` to `build-rootfs`.
### Implementation
The current implementation uses `rpm-ostree` on a manifest (treefile)
embedded in the container image itself. These manifests are not intended
to be editable directly.
To emphasize: the implementation of this command (especially the configuration
files that it reads) are subject to change.
### Cross builds and the builder image
The build tooling is designed to support "cross builds"; the
repository root could e.g. be CentOS Stream 10, while the
builder root is Fedora or RHEL, etc.
In other words, one given base image can be used as a "builder" to produce another
using different RPMs.
### Example: Generate a new image using CentOS Stream 10 content from RHEL
FROM quay.io/centos/centos:stream10 as repos
FROM registry.redhat.io/rhel10/rhel-bootc:10 as builder
RUN --mount=type=bind,from=repos,src=/,dst=/repos,rw /usr/libexec/bootc-base-imagectl build-rootfs --manifest=minimal /repos /target-rootfs
# This container image uses the "artifact pattern"; it has some
# basic configuration we expect to apply to multiple container images.
FROM quay.io/exampleos/baseconfig@sha256:.... as baseconfig
FROM scratch
COPY --from=builder /target-rootfs/ /
# Now we make other arbitrary changes. Copy our systemd units and
# other tweaks from the baseconfig container image.
COPY --from=baseconfig /usr/ /usr/
RUN <<EORUN
set -xeuo pipefail
# Install critical components
dnf -y install linux-firmware NetworkManager cloud-init cowsay
dnf clean all
bootc container lint
EORUN
LABEL containers.bootc 1
ENV container=oci
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

View File

@ -1,8 +0,0 @@
metadata:
name: fedora-boot-tier1
summary: Fedora Bootable Tier 1
include:
- fedora-generic.yaml
- tier-1/manifest.yaml
- tier-1/kernel.yaml

1
fedora-bootc.yaml Symbolic link
View File

@ -0,0 +1 @@
fedora-standard.yaml

3
fedora-minimal.yaml Normal file
View File

@ -0,0 +1,3 @@
include:
- fedora-includes/generic.yaml
- minimal/manifest.yaml

3
fedora-standard.yaml Normal file
View File

@ -0,0 +1,3 @@
include:
- fedora-includes/generic.yaml
- standard/manifest.yaml

View File

@ -1,8 +0,0 @@
metadata:
name: fedora-boot-tier0
summary: Fedora Bootable Tier 0
include:
- fedora-generic.yaml
- tier-0/manifest.yaml
- tier-0/kernel.yaml

View File

@ -1 +0,0 @@
fedora-bootc.yaml

View File

@ -1,8 +1,3 @@
metadata:
name: fedora-boot-tier-x
summary: Fedora Bootable Tier X
include: include:
- fedora-generic.yaml - fedora-includes/generic.yaml
- tier-x/manifest.yaml - tier-x/manifest.yaml
- tier-x/kernel.yaml

18
install-manifests Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -xeuo pipefail
# This script copies the manifests from the current directory
# into their installed location.
manifestdir=/usr/share/doc/bootc-base-imagectl/manifests
mkdir -p "$manifestdir/"
for image in minimal standard tier-x; do
# Embed the generic defaults
cp -a $image $manifestdir/
# And the Fedora-specific tweaks
cp -a fedora-$image.yaml $manifestdir/
done
# Set the default
ln -s fedora-standard.yaml $manifestdir/default.yaml
# And install dependency manifests
cp -a fedora-includes $manifestdir
# And embed the rebuild script
install -m 0755 -t /usr/libexec ./bootc-base-imagectl

View File

@ -1,3 +1,6 @@
metadata:
summary: Effectively just bootc, systemd, kernel, and dnf as a starting point.
edition: "2024" edition: "2024"
# Be minimal # Be minimal
@ -15,6 +18,7 @@ remove-from-packages:
- [systemd-udev, /usr/lib/systemd/system-generators/systemd-gpt-auto-generator] - [systemd-udev, /usr/lib/systemd/system-generators/systemd-gpt-auto-generator]
include: include:
- kernel.yaml
- postprocess-conf.yaml - postprocess-conf.yaml
- bootc.yaml - bootc.yaml
- bootupd.yaml - bootupd.yaml
@ -30,7 +34,7 @@ packages:
# in dnf5. In CentOS/RHEL, this pulls in dnf(4). We can simplify this back to # in dnf5. In CentOS/RHEL, this pulls in dnf(4). We can simplify this back to
# just `dnf` once the `dnf` package is retired from Fedora. # just `dnf` once the `dnf` package is retired from Fedora.
- /usr/bin/dnf - /usr/bin/dnf
# Even in tier-0, we have this. If you don't want SELinux today, you'll need # Even in minimal, we have this. If you don't want SELinux today, you'll need
# to build a custom image. # to build a custom image.
- selinux-policy-targeted - selinux-policy-targeted
# And we want container-selinux because trying to layer it on later currently causes issues. # And we want container-selinux because trying to layer it on later currently causes issues.

View File

@ -7,7 +7,7 @@ opt-usrlocal: "root"
machineid-compat: true machineid-compat: true
# Note that the default for c9s+ is sqlite; we can't rely on rpm being # Note that the default for c9s+ is sqlite; we can't rely on rpm being
# in the target (it isn't in tier-0!) so turn this to host here. This # in the target (it isn't in minimal!) so turn this to host here. This
# does break the "hermetic build" aspect a bit. Maybe eventually # does break the "hermetic build" aspect a bit. Maybe eventually
# what we should do is special case this and actually install RPM temporarily # what we should do is special case this and actually install RPM temporarily
# and then remove it... # and then remove it...

View File

@ -1,8 +1,8 @@
# Configuration for the "tier-1" initramfs # Configuration for the initramfs
postprocess: postprocess:
- | - |
#!/usr/bin/env bash #!/usr/bin/env bash
mkdir -p /usr/lib/dracut/dracut.conf.d mkdir -p /usr/lib/dracut/dracut.conf.d
cat > /usr/lib/dracut/dracut.conf.d/30-bootc-tier-1.conf << 'EOF' cat > /usr/lib/dracut/dracut.conf.d/30-bootc-standard.conf << 'EOF'
add_dracutmodules+=" lvm crypt fips " add_dracutmodules+=" lvm crypt fips "
EOF EOF

View File

@ -1,3 +1,9 @@
metadata:
summary: |
A relatively full, but still generic base image. Roughly
similar to a headless server installation. Automatic updates
are on by default.
# Flip this back on, we're going to be a larger system # Flip this back on, we're going to be a larger system
recommends: true recommends: true

View File

@ -0,0 +1,31 @@
# This test case exercises using the fedora-bootc image as a builder
# to generate a minimal target image derived from CentOS Stream 10 content,
# and then further extends it in a secondary phase.
FROM quay.io/centos/centos:stream10 as repos
# This is intentionally a locally built image
FROM localhost/fedora-bootc as builder
RUN --mount=type=bind,from=repos,src=/,dst=/repos,rw /usr/libexec/bootc-base-imagectl build-rootfs --manifest=standard/manifest /repos /target-rootfs
# This pulls in the rootfs generated in the previous step
FROM scratch
COPY --from=builder /target-rootfs/ /
RUN <<EORUN
set -xeuo pipefail
# Verify we have CentOS content
. /usr/lib/os-release
test "$ID" = centos
# And install a package
dnf -y install strace
dnf clean all
# Cleanup and lint
rm /var/log /var/cache/* /var/lib/dnf
bootc container lint
EORUN
LABEL containers.bootc 1
ENV container=oci
STOPSIGNAL SIGRTMIN+3
CMD ["/sbin/init"]

View File

@ -1 +0,0 @@
../tier-0/kernel.yaml

View File

@ -1 +0,0 @@
../tier-0/kernel.yaml

View File

@ -1,5 +1,13 @@
metadata:
summary: |
A relatively full, but still generic base image. Roughly
similar to a smaller Fedora CoreOS. Includes NetworkManager,
openssh, various CLI tools, etc.
Automatic updates are not on by default.
include: include:
- ../tier-0/manifest.yaml - ../minimal/manifest.yaml
packages: packages:
# Used by admins interactively # Used by admins interactively