# 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. ## Architecture Overview The `bootc-base-imagectl` tool is a Python script that provides three main operations: 1. **`build-rootfs`** - Generate a container root filesystem from RPM packages 2. **`rechunk`** - Split container images into reproducible, chunked layers 3. **`list`** - Enumerate available build configurations The tool uses `rpm-ostree` under the hood to compose root filesystems from RPM packages, with support for custom manifests, package locking, and reproducible builds. ## 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`. ### Basic Usage ```bash bootc-base-imagectl build-rootfs [OPTIONS] ``` **Required Arguments:** - `source_root` - Path to source root with `/etc/yum.repos.d` configuration - `target` - Path where the root filesystem will be generated (must not exist) ### Advanced Options ```bash bootc-base-imagectl build-rootfs \ --manifest=minimal \ --install=package1 --install=package2 \ --add-dir=/path/to/overlay1 --add-dir=/path/to/overlay2 \ --lock=package-1.0.0-1.x86_64 \ --no-docs \ --sysusers \ --cachedir=/var/cache/dnf \ --repo=fedora --repo=updates \ /source/root /target/rootfs ``` **Key Options:** - `--manifest` - Select build configuration (default: "default") - `--install` - Add additional packages to install - `--add-dir` - Copy directory contents as overlay layers - `--lock` - Lock packages to specific versions (NEVRA or NEVR format) - `--no-docs` - Skip documentation packages - `--sysusers` - Use systemd-sysusers instead of hardcoded passwd/group - `--cachedir` - Cache repository metadata and RPMs - `--repo` - Enable specific repositories only - `--reinject` - Copy build configurations into target ### Technical Implementation The tool performs these steps: 1. **Repository Setup**: Runs `dnf repolist` to refresh repository metadata 2. **Manifest Processing**: Loads YAML manifest from `/usr/share/doc/bootc-base-imagectl/manifests/` 3. **Override Generation**: Creates temporary JSON with user overrides 4. **OSTree Compose**: Executes `rpm-ostree compose rootfs` with the manifest 5. **Post-processing**: Fixes permissions and runs `bootc container lint` 6. **Cleanup**: Removes temporary files and repositories ## Using bootc-base-imagectl rechunk This operation is strongly related to `build-rootfs` but is also orthogonal; it can be used on a "regular" container build as well. ### Basic Usage ```bash bootc-base-imagectl rechunk [OPTIONS] ``` **Required Arguments:** - `from_image` - Source image in container storage (e.g., `quay.io/exampleos:build`) - `to_image` - Output image in container storage (e.g., `quay.io/exampleos:latest`) ### Advanced Options ```bash bootc-base-imagectl rechunk \ --max-layers=10 \ quay.io/exampleos:build \ quay.io/exampleos:latest ``` **Options:** - `--max-layers` - Configure maximum number of output layers ### Container Usage This command assumes it will be run as a container image, and defaults to wanting write access to the container storage. ```bash podman run --rm --privileged \ -v /var/lib/containers:/var/lib/containers \ quay.io/fedora/fedora-bootc:rawhide \ bootc-base-imagectl rechunk \ quay.io/exampleos/exampleos:build \ quay.io/exampleos/exampleos:latest ``` ### Technical Implementation The rechunk operation uses `rpm-ostree experimental compose build-chunked-oci`: 1. **Input Processing**: Reads the source container image 2. **Layer Analysis**: Analyzes RPM database to determine optimal layer splits 3. **Chunking**: Splits content into separate, reproducible layers 4. **Timestamp Canonicalization**: Sets all timestamps to zero for reproducibility 5. **Output Generation**: Creates new container image with chunked layers ### Rationale When performing a complex container derivation, there are several issues: #### Replaced duplicate content When e.g. upgrading or replacing the kernel or other large packages as part of a container build (without squashing all layers) then the old replaced content will still be present. #### Removed content still present Similarly, `RUN dnf -y remove` etc. will still retain that removed content in prior layers. #### Timestamp drift By default, many tools will use the current timestamp when writing files. `rpm` will do this (unless `SOURCE_DATE_EPOCH` is set), and other tools like `cp` and `curl` will as well. This means that every build of the image will produce a new tar stream (with new timestamps) - that will get pushed to a registry and downloaded by clients, even if the content didn't actually change. ### What rechunk does: split reproducible chunked images The `bootc-base-imagectl rechunk` command fixes all of these issues by taking an input container, operates on its final merged filesystem tree (hence removed/overridden files are handled), and then splits it up (currently based on the RPM database) into separate layers (tarballs). Further, because bootc uses OSTree today, and OSTree canonializes all timestamps to zero on the client side, this tool does that at build time. ## Using bootc-base-imagectl list The `list` command enumerates available build configurations that can be selected by passing `--manifest` to `build-rootfs`. ### Usage ```bash bootc-base-imagectl list ``` ### Output Format ``` minimal: Minimal bootc base image default: Standard bootc base image with common packages server: Server-focused bootc base image --- ``` ### Technical Implementation The list command: 1. **Manifest Discovery**: Scans `/usr/share/doc/bootc-base-imagectl/manifests/` for `.yaml` files 2. **Filtering**: Excludes `.hidden.yaml` files and symbolic links 3. **Metadata Extraction**: Uses `rpm-ostree compose tree --print-only` to read manifest metadata 4. **Summary Display**: Shows manifest name and description from metadata ## Technical Implementation Details ### Core Architecture The `bootc-base-imagectl` tool is implemented as a Python 3 script that orchestrates `rpm-ostree` operations. The tool provides a high-level interface for: - **Manifest Management**: Loading and processing YAML treefiles - **Package Resolution**: Handling RPM dependencies and version locking - **Overlay Support**: Managing additional content layers - **Reproducible Builds**: Ensuring consistent output across builds ### Key Components #### 1. Manifest System - **Location**: `/usr/share/doc/bootc-base-imagectl/manifests/` - **Format**: YAML treefiles compatible with `rpm-ostree` - **Discovery**: Automatic scanning for `.yaml` files (excluding `.hidden.yaml`) - **Override Support**: JSON overrides for user customizations #### 2. Package Management - **Repository Handling**: Uses `/etc/yum.repos.d` configuration from source root - **Version Locking**: Supports NEVRA (Name-Epoch-Version-Release-Architecture) format - **Dependency Resolution**: Leverages `rpm-ostree`'s advanced dependency solver - **Caching**: Optional repository metadata and RPM caching #### 3. Overlay System - **Directory Overlays**: Copy additional content as OSTree overlay layers - **Temporary Repositories**: Create temporary OSTree repos for overlay content - **Content Integration**: Merge overlay content into final rootfs #### 4. Build Process ```python # Simplified build flow def run_build_rootfs(args): # 1. Repository refresh subprocess.check_call(['dnf', 'repolist'], stdout=subprocess.DEVNULL) # 2. Manifest processing manifest_path = find_manifest(args.manifest) # 3. Override generation if user_overrides: create_temp_override_manifest(overrides) # 4. OSTree compose subprocess.run(['rpm-ostree', 'compose', 'rootfs', ...]) # 5. Post-processing fix_permissions(target) subprocess.run(['bootc', 'container', 'lint', f'--rootfs={target}']) ``` ### External Dependencies The tool requires these external commands: - `rpm-ostree` - Core OSTree composition engine - `dnf` - Package manager for repository operations - `ostree` - OSTree repository management - `bootc` - Container validation and linting ### Error Handling The implementation includes comprehensive error handling: - **Subprocess Failures**: Captures and reports command execution errors - **File Operations**: Handles temporary file creation and cleanup - **Repository Issues**: Manages OSTree repository lifecycle - **Validation**: Runs `bootc container lint` to ensure output validity ### Security Considerations - **Temporary Files**: All temporary files are created with secure permissions - **Repository Isolation**: Uses separate temporary OSTree repositories for overlays - **Content Validation**: Validates all input manifests and configurations - **Cleanup**: Ensures all temporary resources are properly cleaned up ### 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. ## Practical Examples ### Example 1: Generate a new image using CentOS Stream 10 content from RHEL ```dockerfile 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 < /overlay/usr/local/bin/custom-script RUN chmod +x /overlay/usr/local/bin/custom-script # Build rootfs with overlay RUN /usr/libexec/bootc-base-imagectl build-rootfs \ --manifest=default \ --add-dir=/overlay \ --install=cowsay \ / /target-rootfs FROM scratch COPY --from=builder /target-rootfs/ / LABEL containers.bootc 1 ENV container=oci STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] ``` ### Example 4: Reproducible build with rechunking ```dockerfile # Build stage FROM quay.io/fedora/fedora-bootc:rawhide as builder RUN /usr/libexec/bootc-base-imagectl build-rootfs \ --manifest=minimal \ --install=kernel \ --install=systemd \ / /target-rootfs # Create intermediate image FROM scratch as intermediate COPY --from=builder /target-rootfs/ / LABEL containers.bootc 1 ENV container=oci STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] # Rechunk for reproducibility FROM quay.io/fedora/fedora-bootc:rawhide as rechunker COPY --from=intermediate / / RUN /usr/libexec/bootc-base-imagectl rechunk \ --max-layers=5 \ localhost/intermediate:latest \ localhost/final:latest # Final image FROM localhost/final:latest