Skip to content

Commit f6363ce

Browse files
docs: add guide for building custom node images
Document how to build a disk image with a custom or patched package and test it with bink. Covers two approaches: - Runtime update via bootc switch (no disk rebuild) - Full disk image rebuild for boot-critical changes Includes concrete examples for COPR repos, local RPMs, binary replacement, and building bootc from source using the bootc repo's Dockerfile with the bink node image as base. Closes: #91 Signed-off-by: HarshwardhanPatil07 <harshpat@redhat.com>
1 parent 61899a6 commit f6363ce

2 files changed

Lines changed: 341 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,7 @@ bink cluster stop --remove-data
122122
```bash
123123
source <(bink completion bash)
124124
```
125+
126+
## Documentation
127+
128+
- [Building Custom Node Images](docs/custom-node-images.md) — How to build and test with custom or patched packages

docs/custom-node-images.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# Building Custom Node Images
2+
3+
## Overview
4+
5+
When debugging a bug in a node component (bootc, CRI-O, kubelet, kernel, etc.), you need to replace the package or binary with a custom build and verify the fix. There are two approaches:
6+
7+
1. **Runtime update** — Build a custom bootc OCI image and use `bootc switch` to deploy it on a running node. Faster, no disk rebuild needed.
8+
2. **Disk image rebuild** — Rebuild the entire qcow2 disk image. Required when the fix must be present from first boot (kernel, bootc itself, initrd).
9+
10+
**When to use which:**
11+
12+
| Situation | Approach |
13+
|-----------|----------|
14+
| Patching a userspace component (CRI-O, kubelet, a CLI tool) | Runtime update |
15+
| Patching bootc itself, the kernel, or initrd | Disk image rebuild |
16+
| Need the fix before the first boot completes | Disk image rebuild |
17+
18+
## Prerequisites
19+
20+
- Podman with socket running (`systemctl --user start podman.socket`)
21+
- `/dev/kvm` and `/dev/fuse` accessible
22+
- bink binary built (see [README](../README.md))
23+
- A running bink cluster, or you will create one
24+
25+
## Inspecting the Current Node State
26+
27+
Before making changes, inspect what is currently deployed. SSH into a node:
28+
29+
```bash
30+
bink node ssh node1 --cluster-name <name>
31+
```
32+
33+
Then run:
34+
35+
```bash
36+
# Check the ostree deployment
37+
ostree admin status
38+
39+
# Show ostree commit details
40+
ostree show --repo=/sysroot/ostree/repo <checksum>
41+
42+
# Check the origin file (which container image the node tracks)
43+
sudo cat /sysroot/ostree/deploy/default/deploy/*.origin
44+
45+
# Full bootc status
46+
sudo bootc status --json | jq
47+
48+
# Check specific package versions
49+
rpm -qa | grep -E 'bootc|cri-o|kubeadm'
50+
```
51+
52+
## Approach 1: Runtime Update via `bootc switch`
53+
54+
The simpler path when the patch does not need to be in the boot image. Build a custom bootc OCI image, push it to bink's local registry, and switch the node to it.
55+
56+
### Example: Building bootc from Source
57+
58+
The [bootc repository](https://github.com/bootc-dev/bootc) has a Dockerfile with `ARG base=quay.io/centos-bootc/centos-bootc:stream10`. Override this to use the bink node image as the base, producing a new bootable container image with bootc compiled from any git commit:
59+
60+
```bash
61+
git clone https://github.com/bootc-dev/bootc.git
62+
cd bootc
63+
git checkout <commit-or-branch>
64+
65+
podman build \
66+
--build-arg base=ghcr.io/bootc-dev/bink/node:v1.35-fedora-44 \
67+
-t localhost/custom-bootc-node:latest \
68+
.
69+
```
70+
71+
This compiles bootc from source and installs it into an image derived from the bink node image, preserving all the existing packages (kubernetes, CRI-O, etc.).
72+
73+
### Example: Layering a Custom Package
74+
75+
For simpler cases (installing a different package version), write a minimal Containerfile:
76+
77+
```dockerfile
78+
FROM ghcr.io/bootc-dev/bink/node:v1.35-fedora-44
79+
RUN dnf -y install <your-package>
80+
```
81+
82+
Build it:
83+
84+
```bash
85+
podman build -t localhost/custom-node:latest -f Containerfile.custom .
86+
```
87+
88+
### Push to the Local Registry and Switch
89+
90+
Bink runs a local OCI registry on port 5000. From the host, push to `localhost:5000`. Inside the VMs, the registry is reachable at `registry.cluster.local:5000`.
91+
92+
```bash
93+
podman push --tls-verify=false \
94+
localhost/custom-bootc-node:latest \
95+
localhost:5000/custom-bootc-node:latest
96+
```
97+
98+
SSH into the node and switch:
99+
100+
```bash
101+
bink node ssh node1 --cluster-name <name>
102+
103+
sudo bootc switch \
104+
registry.cluster.local:5000/custom-bootc-node:latest
105+
```
106+
107+
To switch by digest:
108+
109+
```bash
110+
sudo bootc switch \
111+
registry.cluster.local:5000/custom-bootc-node@sha256:<digest>
112+
```
113+
114+
Reboot to apply:
115+
116+
```bash
117+
sudo reboot
118+
```
119+
120+
### Verify After Reboot
121+
122+
```bash
123+
bink node ssh node1 --cluster-name <name>
124+
125+
ostree admin status
126+
sudo bootc status --json | jq '.status.booted.image'
127+
rpm -q bootc
128+
```
129+
130+
### Using `--target-imgref` for New Clusters
131+
132+
Instead of switching after boot, set the tracked image reference at cluster creation:
133+
134+
```bash
135+
bink cluster start \
136+
--cluster-name test \
137+
--target-imgref registry.cluster.local:5000/custom-bootc-node:latest
138+
```
139+
140+
This rewrites the ostree origin file during cloud-init so the node tracks your custom image from the start. Running `bootc upgrade` will pull updates from your custom image reference.
141+
142+
## Approach 2: Rebuild the Disk Image
143+
144+
Required when the fix must be present from first boot (kernel, bootc, initrd changes).
145+
146+
### Build Pipeline Overview
147+
148+
The node disk image is built in two stages from `node-images/fedora/`:
149+
150+
```
151+
Containerfile -> bootc OCI image (bootc-base-imagectl build-rootfs)
152+
Containerfile.disk -> qcow2 disk image (bcvk to-disk)
153+
```
154+
155+
- **Stage 1** (`Containerfile`): Builds a bootc OCI image from `quay.io/fedora/fedora-bootc:44`. Uses `bootc-base-imagectl build-rootfs` with `--install` flags for packages (kubernetes, CRI-O, etc.). Validated with `bootc container lint`.
156+
- **Stage 2** (`Containerfile.disk`): Converts the bootc OCI image to a qcow2 disk using `bcvk to-disk`. The final container image contains only `/disk.qcow2` and `/images.txt`.
157+
158+
> **Important:** Do not run `podman build` directly on `node-images/fedora/Containerfile`. The `build-rootfs` command requires elevated privileges. Always use the Makefile targets (`make build-bootc-image`, `make build-disk-image`) which pass the correct flags (`--cap-add=all --security-opt=label=disable --device /dev/fuse`).
159+
160+
### Scenario A: Custom Package from a COPR Repo
161+
162+
Modify `node-images/fedora/Containerfile` to add a COPR repo before the `build-rootfs` command:
163+
164+
```dockerfile
165+
RUN dnf -y install 'dnf5-command(copr)' && \
166+
dnf -y copr enable <owner>/<project> fedora-44-x86_64
167+
```
168+
169+
The COPR architecture string must match the base image (e.g., `fedora-44-x86_64` for Fedora 44).
170+
171+
Build and test:
172+
173+
```bash
174+
cd node-images/fedora
175+
176+
make build-bootc-image BOOTC_IMAGE=localhost/custom-node:latest
177+
make build-disk-image \
178+
BOOTC_IMAGE=localhost/custom-node:latest \
179+
NODE_IMAGE=localhost/custom-node:disk
180+
181+
cd ../..
182+
183+
bink cluster start \
184+
--node-image localhost/custom-node:disk \
185+
--cluster-name custom-test
186+
```
187+
188+
> **Note:** COPR repos should be used as an opt-in customization for debugging and testing, not as a permanent change to the default image. Upstream COPR updates can introduce unexpected changes that break CI.
189+
190+
### Scenario B: Local RPM File
191+
192+
Copy the RPM into the build context and install it into the target rootfs after `build-rootfs` completes:
193+
194+
```dockerfile
195+
COPY my-package.rpm /tmp/my-package.rpm
196+
197+
# After the build-rootfs RUN instruction:
198+
RUN dnf --installroot=/target-rootfs install -y /tmp/my-package.rpm
199+
```
200+
201+
The `--installroot=/target-rootfs` flag is required because `build-rootfs` assembles the filesystem at `/target-rootfs`, not in the builder's own root.
202+
203+
Build and test with the same `make` commands as Scenario A.
204+
205+
### Scenario C: Custom Binary Replacement
206+
207+
Use a multi-stage build to compile the binary and copy it into the node image:
208+
209+
```dockerfile
210+
FROM fedora:44 AS custom-build
211+
RUN dnf -y install git golang make
212+
COPY my-source/ /src
213+
WORKDIR /src
214+
RUN make build
215+
216+
FROM quay.io/fedora/fedora-bootc:44 AS builder
217+
ARG KUBE_MINOR=1.35
218+
ARG KERNEL_VERSION=6.19.14-300.fc44
219+
RUN /usr/libexec/bootc-base-imagectl build-rootfs \
220+
--manifest=minimal \
221+
--no-docs \
222+
--lock kernel-${KERNEL_VERSION} \
223+
--lock kernel-core-${KERNEL_VERSION} \
224+
--lock kernel-modules-${KERNEL_VERSION} \
225+
--lock kernel-modules-core-${KERNEL_VERSION} \
226+
--install NetworkManager \
227+
--install openssh-server \
228+
--install openssh-clients \
229+
--install kubernetes \
230+
--install kubernetes${KUBE_MINOR}-kubeadm \
231+
--install kubernetes${KUBE_MINOR}-client \
232+
--install cri-o${KUBE_MINOR} \
233+
--install qemu-guest-agent \
234+
--install bind-utils \
235+
--install iputils \
236+
--install cloud-init \
237+
--install dnsmasq \
238+
--install bubblewrap \
239+
--install sudo \
240+
--install vim-minimal \
241+
--install jq \
242+
--install less \
243+
/target-rootfs
244+
245+
FROM scratch AS root
246+
COPY --from=builder /target-rootfs/ /
247+
COPY --from=custom-build /src/my-binary /usr/bin/my-binary
248+
249+
RUN passwd -d root
250+
RUN sed -i 's|"/opt/cni/bin"|"/var/lib/cni/bin"|g' /etc/crio/crio.conf && \
251+
sed -i 's|"/opt/cni/net.d"|"/etc/cni/net.d"|g' /etc/crio/crio.conf
252+
RUN bootc container lint
253+
254+
LABEL containers.bootc 1
255+
LABEL ostree.bootable 1
256+
STOPSIGNAL SIGRTMIN+3
257+
CMD ["/sbin/init"]
258+
```
259+
260+
Build and test with the same `make` commands as Scenario A.
261+
262+
### Composefs Variant
263+
264+
To build the disk image with the composefs backend:
265+
266+
```bash
267+
make build-disk-image-composefs \
268+
BOOTC_IMAGE=localhost/custom-node:latest \
269+
NODE_IMAGE=localhost/custom-node:disk-composefs
270+
```
271+
272+
## Testing with Multiple Nodes
273+
274+
Use two nodes to compare stock vs. custom behavior:
275+
276+
```bash
277+
# Start a cluster with the stock image
278+
bink cluster start --cluster-name test
279+
280+
# Add a worker with the custom disk image
281+
bink node add node2 --cluster-name test \
282+
--node-image localhost/custom-node:disk
283+
284+
# SSH into each to compare
285+
bink node ssh node1 --cluster-name test
286+
bink node ssh node2 --cluster-name test
287+
```
288+
289+
Alternatively, switch a single worker at runtime:
290+
291+
```bash
292+
bink node add node2 --cluster-name test
293+
294+
# SSH into node2 and switch to the custom image
295+
bink node ssh node2 --cluster-name test
296+
sudo bootc switch \
297+
registry.cluster.local:5000/custom-node:latest
298+
sudo reboot
299+
```
300+
301+
## Makefile Variable Reference
302+
303+
Variables in `node-images/fedora/Makefile`:
304+
305+
| Variable | Default | Description |
306+
|----------|---------|-------------|
307+
| `KUBE_MINOR` | `1.35` | Kubernetes minor version |
308+
| `FEDORA_VERSION` | `44` | Fedora base version |
309+
| `DISK_SIZE` | `10G` | VM disk size |
310+
| `BUILD_MEMORY` | `4G` | Memory for bcvk build |
311+
| `BOOTC_IMAGE` | `ghcr.io/bootc-dev/bink/node:v1.35-fedora-44` | Bootc OCI image name |
312+
| `NODE_IMAGE` | `ghcr.io/bootc-dev/bink/node:v1.35-fedora-44-disk` | Disk image name |
313+
| `BCVK_EXTRA_ARGS` | (none) | Extra flags for `bcvk to-disk` |
314+
315+
## Troubleshooting
316+
317+
### KVM Permission Errors
318+
319+
```
320+
qemu-system-x86_64: Could not access KVM kernel module: Permission denied
321+
```
322+
323+
Add your user to the `kvm` group or set permissions: `sudo chmod 666 /dev/kvm`.
324+
325+
### `bootc container lint` Failures
326+
327+
The Containerfile runs `bootc container lint` at the end. Custom modifications must preserve:
328+
- `/sbin/init` as the entrypoint (`CMD ["/sbin/init"]`)
329+
- Labels: `containers.bootc 1` and `ostree.bootable 1`
330+
331+
### Disk Space
332+
333+
The qcow2 build via bcvk needs approximately 15 GB of temporary space. Free disk space or reduce the disk size with `DISK_SIZE=8G`.
334+
335+
### COPR Repo Not Found
336+
337+
Verify the Fedora version and architecture string matches the base image. For `quay.io/fedora/fedora-bootc:44`, use `fedora-44-x86_64`.

0 commit comments

Comments
 (0)