Introduce bootc-base-imagectl
- Embed the manifests into the container image - Add bootc-base-imagectl which is a tightly controlled frontend to execute on those manifests. For now, we don't attempt to rework how we build the standard image to actually look like `dnf install`, but we show that it can work. Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
parent
c3c8442827
commit
835852dc4f
|
|
@ -26,7 +26,7 @@ build-image:
|
|||
matrix:
|
||||
- TIER: [minimal, standard, tier-x]
|
||||
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:
|
||||
- if: $CI_PROJECT_NAMESPACE != "fedora/bootc"
|
||||
when: never
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ FROM quay.io/fedora/fedora:rawhide as repos
|
|||
# BOOTSTRAPPING: This can be any image that has rpm-ostree and selinux-policy-targeted.
|
||||
FROM quay.io/fedora/fedora:rawhide as builder
|
||||
RUN dnf -y install rpm-ostree selinux-policy-targeted
|
||||
ARG MANIFEST=fedora-bootc.yaml
|
||||
ARG MANIFEST=fedora-standard
|
||||
# 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.
|
||||
# So copy the source, and delete the hardcoded ones in git, and use the container base
|
||||
|
|
@ -20,8 +20,17 @@ COPY . /src
|
|||
WORKDIR /src
|
||||
RUN rm -vf /src/*.repo
|
||||
RUN --mount=type=cache,target=/workdir \
|
||||
--mount=type=bind,rw,from=repos,src=/,dst=/repos \
|
||||
rpm-ostree experimental compose rootfs --cachedir=/workdir --source-root-rw=/repos ${MANIFEST} /target-rootfs
|
||||
--mount=type=bind,rw,from=repos,src=/,dst=/repos <<EORUN
|
||||
set -xeuo pipefail
|
||||
# Put our manifests into the builder image in the same location they'll be in the
|
||||
# final image.
|
||||
./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
|
||||
|
||||
# This pulls in the rootfs generated in the previous step
|
||||
FROM scratch
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
||||
Loading…
Reference in New Issue