Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions containers/anaconda-bootc/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Anaconda + Bootc Container Image
#
# This container provides anaconda installer capabilities combined with bootc
# for installing bootc container images to disk. Built on a bootc base image
# so it can be run as an ephemeral VM with the bcvk infrastructure.

FROM quay.io/fedora/fedora-bootc:42

LABEL org.opencontainers.image.title="Anaconda Bootc Installer"
LABEL org.opencontainers.image.description="Anaconda installer with bootc support for container-native installations"
LABEL org.opencontainers.image.source="https://github.com/bootc-dev/bcvk"
LABEL org.opencontainers.image.vendor="bootc project"

COPY packages.txt /tmp/
RUN grep -vE '^#|^$' /tmp/packages.txt | xargs dnf -y install && \
rm /tmp/packages.txt && \
dnf clean all

WORKDIR /var/tmp
160 changes: 160 additions & 0 deletions containers/anaconda-bootc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Anaconda + Bootc Container

This container provides anaconda installer capabilities combined with bootc for installing bootc container images to disk.

## Overview

The container is based on `quay.io/fedora/fedora-bootc:42` which includes a kernel, allowing it to be run as an ephemeral VM using bcvk's infrastructure. Anaconda-tui is installed on top for text-mode installation support.

## Building

```bash
cd containers/anaconda-bootc
podman build -t localhost/anaconda-bootc:latest .
```

## Usage with bcvk

The `bcvk anaconda install` command automates the installation process:

```bash
# Install a bootc image to a disk file
bcvk anaconda install \
--kickstart my-kickstart.ks \
quay.io/fedora/fedora-bootc:42 \
/path/to/disk.img

# With custom options
bcvk anaconda install \
--kickstart my-kickstart.ks \
--disk-size 20G \
--target-imgref registry.example.com/my-image:latest \
quay.io/fedora/fedora-bootc:42 \
/path/to/disk.img
```

## Writing a Kickstart File

You **must** provide a kickstart file with partitioning, locale, and other
system configuration. bcvk handles the container storage integration and
injects:

- `ostreecontainer --transport=containers-storage --url=<image>`
- `%pre` script to configure container storage with host overlay
- `%post` script to repoint the installed system to the registry (unless `--no-repoint`)

### Example Kickstart

Create a file called `my-kickstart.ks`:

```kickstart
# Locale and keyboard
lang en_US.UTF-8
keyboard us
timezone UTC --utc

# Network
network --bootproto=dhcp --activate

# Partitioning - user must define this
zerombr
clearpart --all --initlabel
autopart --type=plain --fstype=xfs

# Bootloader
bootloader --location=mbr

# Security
rootpw --lock

# Post-install action
poweroff
```

**Do NOT include** `ostreecontainer` in your kickstart - bcvk injects it
automatically with the correct transport for containers-storage.

### Customization Examples

**LVM partitioning:**
```kickstart
clearpart --all --initlabel
autopart --type=lvm --fstype=xfs
```

**Btrfs partitioning:**
```kickstart
clearpart --all --initlabel
autopart --type=btrfs
```

**Encrypted root:**
```kickstart
clearpart --all --initlabel
autopart --type=lvm --fstype=xfs --encrypted --passphrase=changeme
```

**Static network:**
```kickstart
network --bootproto=static --ip=192.168.1.100 --netmask=255.255.255.0 \
--gateway=192.168.1.1 --nameserver=8.8.8.8 --activate
```

**Custom users:**
```kickstart
rootpw --iscrypted $6$rounds=...
user --name=admin --groups=wheel --iscrypted --password=$6$rounds=...
```

## Target Image Reference

By default, bcvk injects a `%post` script that runs:

```bash
bootc switch --mutate-in-place --transport registry <target-imgref>
```

This repoints the installed system's bootc origin from `containers-storage:`
to `registry:`, so that `bootc upgrade` pulls updates from the registry.

- By default, `<target-imgref>` is the image you're installing
- Use `--target-imgref` to specify a different registry image
- Use `--no-repoint` to skip this and handle it yourself

## What's Installed

- **anaconda-tui**: Text-mode anaconda installer
- **python3-kickstart / pykickstart**: Kickstart file processing
- **Disk tools**: parted, gdisk, lvm2, cryptsetup
- **Filesystem tools**: e2fsprogs, xfsprogs, btrfs-progs, dosfstools
- **Container tools**: skopeo (bootc is already in the base image)

## Architecture

```mermaid
flowchart TB
subgraph Host["Host System"]
bcvk["bcvk anaconda install"]
storage["Container Storage<br/>(images)"]
disk["Target Disk Image"]
kickstart["User Kickstart"]
end

subgraph VM["Ephemeral VM (anaconda-bootc)"]
anaconda["anaconda --text --kickstart"]
virtiofs["virtiofs mount<br/>(host storage)"]
virtioblk["virtio-blk<br/>(target disk)"]
end

kickstart -->|"1. Read & inject directives"| bcvk
bcvk -->|"2. Create disk"| disk
bcvk -->|"3. Start VM with kickstart"| VM
storage -->|"read-only"| virtiofs
virtiofs --> anaconda
anaconda -->|"install to"| virtioblk
virtioblk --> disk
```

## License

Same as the parent bcvk project.
22 changes: 22 additions & 0 deletions containers/anaconda-bootc/packages.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Anaconda text-mode installer (includes anaconda-core)
anaconda-tui
# Kickstart support (explicit, though anaconda-core depends on it)
python3-kickstart
pykickstart
# System libraries required by anaconda/blivet
systemd-libs
# Disk management tools
parted
gdisk
lvm2
cryptsetup
# Filesystem tools
e2fsprogs
xfsprogs
btrfs-progs
dosfstools
# Container tools (skopeo for image operations)
skopeo
# Network tools
wget
curl
1 change: 1 addition & 0 deletions crates/integration-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub(crate) use integration_tests::{
};

mod tests {
pub mod anaconda_install;
pub mod libvirt_base_disks;
pub mod libvirt_port_forward;
pub mod libvirt_upload_disk;
Expand Down
137 changes: 137 additions & 0 deletions crates/integration-tests/src/tests/anaconda_install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Integration tests for anaconda install command

use std::io::Write;

use camino::Utf8PathBuf;
use color_eyre::Result;
use integration_tests::integration_test;
use tempfile::TempDir;
use xshell::cmd;

use crate::{get_bck_command, shell, CapturedOutput};

fn test_anaconda_help() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;

let output = sh.cmd(&bck).args(&["anaconda", "--help"]).output()?;
assert!(output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Install bootc images using anaconda"));
assert!(stdout.contains("install"));

Ok(())
}
integration_test!(test_anaconda_help);

fn test_anaconda_install_help() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;

let output = sh
.cmd(&bck)
.args(&["anaconda", "install", "--help"])
.output()?;
assert!(output.status.success());

let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("<IMAGE>"));
assert!(stdout.contains("--anaconda-image"));
assert!(stdout.contains("--kickstart"));

Ok(())
}
integration_test!(test_anaconda_install_help);

/// Test actual anaconda installation to a disk image
///
/// This test verifies that anaconda can successfully install a bootc container
/// to a disk image using the ostreecontainer kickstart verb.
fn test_anaconda_install() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;

// Use fedora-bootc for anaconda tests since that's what anaconda-bootc is based on
let image = "quay.io/fedora/fedora-bootc:42";

let temp_dir = TempDir::new().expect("Failed to create temp directory");
let disk_path = Utf8PathBuf::try_from(temp_dir.path().join("anaconda-disk.img"))
.expect("temp path is not UTF-8");

// Create a kickstart file with partitioning and locale settings
let kickstart_path = temp_dir.path().join("test.ks");
let kickstart_content = r#"
lang en_US.UTF-8
keyboard us
timezone UTC --utc
network --bootproto=dhcp --activate

zerombr
clearpart --all --initlabel
autopart --type=plain --fstype=xfs
bootloader --location=mbr
rootpw --lock

poweroff
"#;
let mut ks_file = std::fs::File::create(&kickstart_path)?;
ks_file.write_all(kickstart_content.as_bytes())?;
ks_file.sync_all()?;
let kickstart = Utf8PathBuf::try_from(kickstart_path).expect("temp path is not UTF-8");

let raw_output = cmd!(
sh,
"{bck} anaconda install --kickstart {kickstart} --disk-size 10G {image} {disk_path}"
)
.ignore_status()
.output()?;

let output = CapturedOutput::new(std::process::Output {
status: raw_output.status,
stdout: raw_output.stdout,
stderr: raw_output.stderr,
});

assert!(
output.success(),
"anaconda install failed with exit code: {:?}. stdout: {}, stderr: {}",
output.exit_code(),
output.stdout,
output.stderr
);

// Verify disk was created
let metadata = std::fs::metadata(&disk_path).expect("Failed to get disk metadata");
assert!(metadata.len() > 0, "Disk image is empty");

// Verify anaconda completed successfully
assert!(
output
.stdout
.contains("Installation completed successfully")
|| output.stdout.contains("Complete!"),
"Anaconda installation did not complete successfully. stdout: {}, stderr: {}",
output.stdout,
output.stderr
);

// Verify the disk has partitions using sfdisk
let sfdisk_stdout = cmd!(sh, "sfdisk -l {disk_path}").read()?;
assert!(
sfdisk_stdout.contains("Disk "),
"sfdisk output doesn't show expected disk information"
);

let has_partitions = sfdisk_stdout
.lines()
.any(|line| line.contains(disk_path.as_str()) && line.contains("Linux"));
assert!(
has_partitions,
"No Linux partitions found in sfdisk output. Output was:\n{}",
sfdisk_stdout
);

Ok(())
}
integration_test!(test_anaconda_install);
Loading
Loading