diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 86919ef..5c82de0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/Containerfile b/Containerfile index d01755b..4f08153 100644 --- a/Containerfile +++ b/Containerfile @@ -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 </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 diff --git a/bootc-base-imagectl b/bootc-base-imagectl new file mode 100755 index 0000000..05b880b --- /dev/null +++ b/bootc-base-imagectl @@ -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) \ No newline at end of file diff --git a/bootc-base-imagectl.md b/bootc-base-imagectl.md new file mode 100644 index 0000000..9a90b34 --- /dev/null +++ b/bootc-base-imagectl.md @@ -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 +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 <