From 635dc2b1b46c71131bf027d885d6dff03e765035 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 20 Oct 2026 08:28:58 +0200 Subject: [PATCH 1/4] node-images: remove locking for kernel version In the kernel version 7.0 had a regression, and we pinned the kernel to 6.19.14-200.fc43. See this issue for more details: https://github.com/bootc-dev/bink/issues/8 Signed-off-by: Alice Frosi --- node-images/fedora/Containerfile | 7 ------- 1 file changed, 7 deletions(-) diff --git a/node-images/fedora/Containerfile b/node-images/fedora/Containerfile index b779db6..76de3c2 100644 --- a/node-images/fedora/Containerfile +++ b/node-images/fedora/Containerfile @@ -1,18 +1,11 @@ FROM quay.io/fedora/fedora-bootc:44 AS builder ARG KUBE_MINOR=1.36 -# Pin kernel to 6.19.x to work around a kernel 7.0 regression. -# https://github.com/bootc-dev/bink/issues/52 -ARG KERNEL_VERSION=6.19.14-300.fc44 RUN dnf -y install 'dnf5-command(copr)' && \ dnf -y copr enable rhcontainerbot/bootc fedora-44-x86_64 RUN /usr/libexec/bootc-base-imagectl build-rootfs \ --manifest=minimal \ --no-docs \ - --lock kernel-${KERNEL_VERSION} \ - --lock kernel-core-${KERNEL_VERSION} \ - --lock kernel-modules-${KERNEL_VERSION} \ - --lock kernel-modules-core-${KERNEL_VERSION} \ --install NetworkManager \ --install openssh-server \ --install openssh-clients \ From bf3446af71e3fdb4a3c690fe1dff47cdc6f94244 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 20 Oct 2026 10:11:01 +0000 Subject: [PATCH 2/4] cli: refactor runStart and runAdd to use option structs Reduce number of arguments passed to runAdd. Assisted-by: AI Signed-off-by: Alice Frosi --- internal/cli/cluster/start.go | 89 ++++++++++++++++++----------------- internal/cli/node/add.go | 61 ++++++++++++------------ 2 files changed, 79 insertions(+), 71 deletions(-) diff --git a/internal/cli/cluster/start.go b/internal/cli/cluster/start.go index 75af550..2c56a56 100644 --- a/internal/cli/cluster/start.go +++ b/internal/cli/cluster/start.go @@ -21,17 +21,21 @@ import ( "github.com/spf13/viper" ) +type startOptions struct { + nodeName string + nodeImage string + apiPort int + memory int + maxMemory int + exposePath string + hostNetworkPopulator bool + targetImgRef string + registryUser string + registryPassword string +} + func newStartCmd() *cobra.Command { - var nodeName string - var nodeImage string - var apiPort int - var memory int - var maxMemory int - var exposePath string - var hostNetworkPopulator bool - var targetImgRef string - var registryUser string - var registryPassword string + opts := &startOptions{} cmd := &cobra.Command{ Use: "start", @@ -47,26 +51,26 @@ func newStartCmd() *cobra.Command { bink cluster start --memory 4096 --expose ./kubeconfig`, RunE: func(cmd *cobra.Command, args []string) error { logger := logrus.New() - return runStart(cmd.Context(), logger, nodeName, nodeImage, apiPort, memory, maxMemory, exposePath, hostNetworkPopulator, targetImgRef, registryUser, registryPassword) + return runStart(cmd.Context(), logger, opts) }, } - cmd.Flags().StringVar(&nodeName, "node-name", "node1", "Name for the control plane node") - cmd.Flags().StringVar(&nodeImage, "node-image", config.DefaultNodeImage, "Container image containing base VM images") - cmd.Flags().IntVar(&apiPort, "api-port", 0, "API server port to expose (0 = auto-assign random port)") - cmd.Flags().IntVar(&memory, "memory", 0, "VM memory in MB (0 = use role default: 1900 for control-plane, 768 for worker)") - cmd.Flags().IntVar(&maxMemory, "max-memory", 0, "VM max memory in MB for balloon (0 = use role default: 4096 for control-plane, 2048 for worker)") - cmd.Flags().StringVar(&exposePath, "expose", "", "Expose API and save kubeconfig to PATH after cluster is up") - cmd.Flags().BoolVar(&hostNetworkPopulator, "host-network-populator", false, "Use host networking for the image populator container (fixes DNS in nested podman)") - cmd.Flags().StringVar(&targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)") - cmd.Flags().StringVar(®istryUser, "registry-user", "", "Username for the authenticated registry") - cmd.Flags().StringVar(®istryPassword, "registry-password", "", "Password for the authenticated registry") + cmd.Flags().StringVar(&opts.nodeName, "node-name", "node1", "Name for the control plane node") + cmd.Flags().StringVar(&opts.nodeImage, "node-image", config.DefaultNodeImage, "Container image containing base VM images") + cmd.Flags().IntVar(&opts.apiPort, "api-port", 0, "API server port to expose (0 = auto-assign random port)") + cmd.Flags().IntVar(&opts.memory, "memory", 0, "VM memory in MB (0 = use role default: 1900 for control-plane, 768 for worker)") + cmd.Flags().IntVar(&opts.maxMemory, "max-memory", 0, "VM max memory in MB for balloon (0 = use role default: 4096 for control-plane, 2048 for worker)") + cmd.Flags().StringVar(&opts.exposePath, "expose", "", "Expose API and save kubeconfig to PATH after cluster is up") + cmd.Flags().BoolVar(&opts.hostNetworkPopulator, "host-network-populator", false, "Use host networking for the image populator container (fixes DNS in nested podman)") + cmd.Flags().StringVar(&opts.targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)") + cmd.Flags().StringVar(&opts.registryUser, "registry-user", "", "Username for the authenticated registry") + cmd.Flags().StringVar(&opts.registryPassword, "registry-password", "", "Password for the authenticated registry") return cmd } -func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeImage string, apiPort int, memory int, maxMemory int, exposePath string, hostNetworkPopulator bool, targetImgRef string, registryUser string, registryPassword string) error { - authRegistryRequested, err := registry.AuthRegistryRequested(registryUser, registryPassword) +func runStart(ctx context.Context, logger *logrus.Logger, opts *startOptions) error { + authRegistryRequested, err := registry.AuthRegistryRequested(opts.registryUser, opts.registryPassword) if err != nil { return fmt.Errorf("invalid auth registry credentials: %w", err) } @@ -95,7 +99,7 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI return fmt.Errorf("ensuring registry: %w", err) } if authRegistryRequested { - if err := registryMgr.EnsureAuthRegistry(ctx, registryUser, registryPassword); err != nil { + if err := registryMgr.EnsureAuthRegistry(ctx, opts.registryUser, opts.registryPassword); err != nil { return fmt.Errorf("ensuring auth registry: %w", err) } } @@ -121,7 +125,7 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI if err := podmanClient.EnsureImage(ctx, config.DefaultClusterImage); err != nil { return fmt.Errorf("ensuring cluster image: %w", err) } - if err := podmanClient.EnsureImage(ctx, nodeImage); err != nil { + if err := podmanClient.EnsureImage(ctx, opts.nodeImage); err != nil { return fmt.Errorf("ensuring node image: %w", err) } logger.Info("") @@ -129,34 +133,35 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI logger.Info("Step 5: Preparing cluster images volume...") clusterMgr := cluster.New(cluster.Config{ Name: clusterName, - ControlPlane: nodeName, - HostNetworkPopulator: hostNetworkPopulator, + ControlPlane: opts.nodeName, + HostNetworkPopulator: opts.hostNetworkPopulator, Logger: logger, }) - clusterImagesVolume, err := clusterMgr.EnsureImagesVolume(ctx, nodeImage) + clusterImagesVolume, err := clusterMgr.EnsureImagesVolume(ctx, opts.nodeImage) if err != nil { return fmt.Errorf("ensuring images volume: %w", err) } logger.Info("") - logger.Infof("Step 6: Creating control plane node (%s)...", nodeName) - logger.Infof("Node image: %s", nodeImage) + logger.Infof("Step 6: Creating control plane node (%s)...", opts.nodeName) + logger.Infof("Node image: %s", opts.nodeImage) // Convert 0 to -1 for auto-assign (to distinguish from unset) + apiPort := opts.apiPort if apiPort == 0 { apiPort = -1 } - controlPlane, err := node.New(nodeName, true, - node.WithNodeImage(nodeImage), + controlPlane, err := node.New(opts.nodeName, true, + node.WithNodeImage(opts.nodeImage), node.WithClusterName(clusterName), node.WithAPIPort(apiPort), - node.WithMemory(memory), - node.WithMaxMemory(maxMemory), + node.WithMemory(opts.memory), + node.WithMaxMemory(opts.maxMemory), node.WithDNSIP(dnsIP), node.WithClusterImagesVolume(clusterImagesVolume), - node.WithTargetImgRef(targetImgRef), + node.WithTargetImgRef(opts.targetImgRef), ) if err != nil { return fmt.Errorf("creating node: %w", err) @@ -168,23 +173,23 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI } if exists { - return fmt.Errorf("%s already exists. Run 'bink cluster stop' first", nodeName) + return fmt.Errorf("%s already exists. Run 'bink cluster stop' first", opts.nodeName) } if err := controlPlane.Create(ctx); err != nil { return fmt.Errorf("creating control plane node: %w", err) } - logger.Infof("Adding %s DNS entry...", nodeName) - if err := dnsMgr.AddEntry(ctx, nodeName); err != nil { - return fmt.Errorf("adding %s DNS entry: %w", nodeName, err) + logger.Infof("Adding %s DNS entry...", opts.nodeName) + if err := dnsMgr.AddEntry(ctx, opts.nodeName); err != nil { + return fmt.Errorf("adding %s DNS entry: %w", opts.nodeName, err) } logger.Info("") logger.Info("Step 7: Initializing Kubernetes cluster...") if err := clusterMgr.Init(ctx, cluster.InitOptions{ - NodeName: nodeName, + NodeName: opts.nodeName, }); err != nil { return fmt.Errorf("initializing cluster: %w", err) } @@ -217,9 +222,9 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI logger.Info("") } - if exposePath != "" { + if opts.exposePath != "" { logger.Info("Step 9: Exposing API server...") - if err := api.RunExpose(ctx, logger, "", exposePath); err != nil { + if err := api.RunExpose(ctx, logger, "", opts.exposePath); err != nil { return fmt.Errorf("exposing API: %w", err) } } else { diff --git a/internal/cli/node/add.go b/internal/cli/node/add.go index 12bf9c1..db11456 100644 --- a/internal/cli/node/add.go +++ b/internal/cli/node/add.go @@ -20,14 +20,18 @@ import ( "github.com/bootc-dev/bink/internal/podman" ) +type addOptions struct { + nodeImage string + role string + memory int + maxMemory int + hostNetworkPopulator bool + labelFlags []string + targetImgRef string +} + func newAddCmd() *cobra.Command { - var nodeImage string - var role string - var memory int - var maxMemory int - var hostNetworkPopulator bool - var labelFlags []string - var targetImgRef string + opts := &addOptions{} cmd := &cobra.Command{ Use: "add ", @@ -43,22 +47,22 @@ func newAddCmd() *cobra.Command { bink node add node2 --role control-plane`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - labels, err := parseLabels(labelFlags) + labels, err := parseLabels(opts.labelFlags) if err != nil { return err } logger := logrus.New() - return runAdd(cmd.Context(), args[0], nodeImage, role, memory, maxMemory, hostNetworkPopulator, labels, targetImgRef, logger) + return runAdd(cmd.Context(), args[0], opts, labels, logger) }, } - cmd.Flags().StringVar(&nodeImage, "node-image", config.DefaultNodeImage, "Container image containing base VM images") - cmd.Flags().StringVarP(&role, "role", "r", "worker", "Node role: worker or control-plane") - cmd.Flags().IntVar(&memory, "memory", 0, "VM memory in MB (0 = use role default: 1900 for control-plane, 768 for worker)") - cmd.Flags().IntVar(&maxMemory, "max-memory", 0, "VM max memory in MB for balloon (0 = use role default: 4096 for control-plane, 2048 for worker)") - cmd.Flags().BoolVar(&hostNetworkPopulator, "host-network-populator", false, "Use host networking for the image populator container (fixes DNS in nested podman)") - cmd.Flags().StringArrayVarP(&labelFlags, "label", "l", nil, "Node label in key=value format (can be specified multiple times)") - cmd.Flags().StringVar(&targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)") + cmd.Flags().StringVar(&opts.nodeImage, "node-image", config.DefaultNodeImage, "Container image containing base VM images") + cmd.Flags().StringVarP(&opts.role, "role", "r", "worker", "Node role: worker or control-plane") + cmd.Flags().IntVar(&opts.memory, "memory", 0, "VM memory in MB (0 = use role default: 1900 for control-plane, 768 for worker)") + cmd.Flags().IntVar(&opts.maxMemory, "max-memory", 0, "VM max memory in MB for balloon (0 = use role default: 4096 for control-plane, 2048 for worker)") + cmd.Flags().BoolVar(&opts.hostNetworkPopulator, "host-network-populator", false, "Use host networking for the image populator container (fixes DNS in nested podman)") + cmd.Flags().StringArrayVarP(&opts.labelFlags, "label", "l", nil, "Node label in key=value format (can be specified multiple times)") + cmd.Flags().StringVar(&opts.targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)") cmd.RegisterFlagCompletionFunc("role", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"worker", "control-plane"}, cobra.ShellCompDirectiveNoFileComp @@ -86,19 +90,18 @@ func parseLabels(labelFlags []string) (map[string]string, error) { return labels, nil } -func runAdd(ctx context.Context, nodeName, nodeImage, role string, memory int, maxMemory int, hostNetworkPopulator bool, labels map[string]string, targetImgRef string, logger *logrus.Logger) error { - // Validate and convert role to boolean +func runAdd(ctx context.Context, nodeName string, opts *addOptions, labels map[string]string, logger *logrus.Logger) error { var isControlPlane bool - switch role { + switch opts.role { case "worker": isControlPlane = false case "control-plane": isControlPlane = true default: - return fmt.Errorf("invalid role %q: must be either 'worker' or 'control-plane'", role) + return fmt.Errorf("invalid role %q: must be either 'worker' or 'control-plane'", opts.role) } - logger.Infof("=== Creating %s node %s ===", role, nodeName) + logger.Infof("=== Creating %s node %s ===", opts.role, nodeName) logger.Info("") clusterName := viper.GetString("cluster.name") @@ -112,8 +115,8 @@ func runAdd(ctx context.Context, nodeName, nodeImage, role string, memory int, m } // Step 1: Create the new node - logger.Infof("Step 1: Creating %s node...", role) - logger.Infof("Node image: %s", nodeImage) + logger.Infof("Step 1: Creating %s node...", opts.role) + logger.Infof("Node image: %s", opts.nodeImage) // Collect IPs of existing nodes to avoid collisions existingContainers, err := podmanClient.ContainerList(ctx, config.LabelFilter(config.LabelClusterName, clusterName)) @@ -139,11 +142,11 @@ func runAdd(ctx context.Context, nodeName, nodeImage, role string, memory int, m clusterMgr := cluster.New(cluster.Config{ Name: clusterName, ControlPlane: controlPlane, - HostNetworkPopulator: hostNetworkPopulator, + HostNetworkPopulator: opts.hostNetworkPopulator, Logger: logger, }) - clusterImagesVolume, err := clusterMgr.EnsureImagesVolume(ctx, nodeImage) + clusterImagesVolume, err := clusterMgr.EnsureImagesVolume(ctx, opts.nodeImage) if err != nil { return fmt.Errorf("ensuring images volume: %w", err) } @@ -160,14 +163,14 @@ func runAdd(ctx context.Context, nodeName, nodeImage, role string, memory int, m } nodeOpts := []node.NodeOption{ - node.WithNodeImage(nodeImage), + node.WithNodeImage(opts.nodeImage), node.WithClusterName(clusterName), - node.WithMemory(memory), - node.WithMaxMemory(maxMemory), + node.WithMemory(opts.memory), + node.WithMaxMemory(opts.maxMemory), node.WithUsedIPs(usedIPs), node.WithDNSIP(dnsIP), node.WithClusterImagesVolume(clusterImagesVolume), - node.WithTargetImgRef(targetImgRef), + node.WithTargetImgRef(opts.targetImgRef), } if isControlPlane { nodeOpts = append(nodeOpts, node.WithAPIPort(-1)) From c29d550b5c0689f9c724c0b14d58ff58a898d292 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 20 Oct 2026 10:11:27 +0000 Subject: [PATCH 3/4] Add --boot-firmware flag for UEFI boot support The composefs backend in bootc requires UEFI boot because it reads the EFI variable LoaderDevicePartUUID to locate the ESP where boot entries are stored. Without UEFI firmware, bootc status fails with SystemNotUEFI. The ostree backend is unaffected since it reads boot entries from /sysroot/boot/loader/entries/ with no EFI dependency. This adds a --boot-firmware flag to cluster start and node add that passes --boot uefi to virt-install, switching from SeaBIOS to OVMF. The cluster container image now includes edk2-ovmf to provide the UEFI firmware files. Assisted-by: AI Signed-off-by: Alice Frosi --- containerfiles/cluster-image/Containerfile | 1 + internal/cli/cluster/start.go | 3 +++ internal/cli/node/add.go | 3 +++ internal/node/create.go | 11 ++++++----- internal/node/node.go | 8 ++++++++ internal/virsh/client.go | 4 ++++ internal/virsh/types.go | 1 + test/integration/cluster_test.go | 6 +++++- test/integration/helpers/cluster.go | 18 +++++++++++++++++- 9 files changed, 48 insertions(+), 7 deletions(-) diff --git a/containerfiles/cluster-image/Containerfile b/containerfiles/cluster-image/Containerfile index 8ef078b..166d617 100644 --- a/containerfiles/cluster-image/Containerfile +++ b/containerfiles/cluster-image/Containerfile @@ -16,6 +16,7 @@ RUN dnf install -y --setopt=install_weak_deps=0 \ openssh-clients \ genisoimage \ podman \ + edk2-ovmf \ && dnf clean all COPY qemu.conf /etc/libvirt/qemu.conf diff --git a/internal/cli/cluster/start.go b/internal/cli/cluster/start.go index 2c56a56..0c72d4d 100644 --- a/internal/cli/cluster/start.go +++ b/internal/cli/cluster/start.go @@ -32,6 +32,7 @@ type startOptions struct { targetImgRef string registryUser string registryPassword string + bootFirmware string } func newStartCmd() *cobra.Command { @@ -65,6 +66,7 @@ func newStartCmd() *cobra.Command { cmd.Flags().StringVar(&opts.targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)") cmd.Flags().StringVar(&opts.registryUser, "registry-user", "", "Username for the authenticated registry") cmd.Flags().StringVar(&opts.registryPassword, "registry-password", "", "Password for the authenticated registry") + cmd.Flags().StringVar(&opts.bootFirmware, "boot-firmware", "", "Boot firmware for the VM (uefi or bios, default: bios)") return cmd } @@ -162,6 +164,7 @@ func runStart(ctx context.Context, logger *logrus.Logger, opts *startOptions) er node.WithDNSIP(dnsIP), node.WithClusterImagesVolume(clusterImagesVolume), node.WithTargetImgRef(opts.targetImgRef), + node.WithBootFirmware(opts.bootFirmware), ) if err != nil { return fmt.Errorf("creating node: %w", err) diff --git a/internal/cli/node/add.go b/internal/cli/node/add.go index db11456..d2e7f99 100644 --- a/internal/cli/node/add.go +++ b/internal/cli/node/add.go @@ -28,6 +28,7 @@ type addOptions struct { hostNetworkPopulator bool labelFlags []string targetImgRef string + bootFirmware string } func newAddCmd() *cobra.Command { @@ -63,6 +64,7 @@ func newAddCmd() *cobra.Command { cmd.Flags().BoolVar(&opts.hostNetworkPopulator, "host-network-populator", false, "Use host networking for the image populator container (fixes DNS in nested podman)") cmd.Flags().StringArrayVarP(&opts.labelFlags, "label", "l", nil, "Node label in key=value format (can be specified multiple times)") cmd.Flags().StringVar(&opts.targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)") + cmd.Flags().StringVar(&opts.bootFirmware, "boot-firmware", "", "Boot firmware for the VM (uefi or bios, default: bios)") cmd.RegisterFlagCompletionFunc("role", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"worker", "control-plane"}, cobra.ShellCompDirectiveNoFileComp @@ -171,6 +173,7 @@ func runAdd(ctx context.Context, nodeName string, opts *addOptions, labels map[s node.WithDNSIP(dnsIP), node.WithClusterImagesVolume(clusterImagesVolume), node.WithTargetImgRef(opts.targetImgRef), + node.WithBootFirmware(opts.bootFirmware), } if isControlPlane { nodeOpts = append(nodeOpts, node.WithAPIPort(-1)) diff --git a/internal/node/create.go b/internal/node/create.go index 7aa4d05..b6a1ddf 100644 --- a/internal/node/create.go +++ b/internal/node/create.go @@ -218,11 +218,12 @@ func (n *Node) createVM(ctx context.Context) error { } opts := &virsh.VirtInstallOptions{ - Name: n.Name, - Memory: n.Memory, - MaxMemory: maxMemory, - VCPUs: n.VCPUs, - Disks: []string{overlayDisk, isoPath}, + Name: n.Name, + Memory: n.Memory, + MaxMemory: maxMemory, + VCPUs: n.VCPUs, + BootFirmware: n.BootFirmware, + Disks: []string{overlayDisk, isoPath}, Networks: []virsh.NetworkConfig{ { Type: "passt", diff --git a/internal/node/node.go b/internal/node/node.go index 2b5f027..c13d714 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -30,6 +30,7 @@ type Node struct { APIPort int // Configured API port (0 = auto-assign) AssignedAPIPort int // Actual assigned port after container creation TargetImgRef string + BootFirmware string usedIPs []string podman *podman.Client @@ -108,6 +109,13 @@ func WithClusterImagesVolume(volumeName string) NodeOption { } } +func WithBootFirmware(firmware string) NodeOption { + return func(n *Node) error { + n.BootFirmware = firmware + return nil + } +} + func New(name string, isControlPlane bool, opts ...NodeOption) (*Node, error) { var memory, maxMemory int switch { diff --git a/internal/virsh/client.go b/internal/virsh/client.go index 8dd61b9..42fb747 100644 --- a/internal/virsh/client.go +++ b/internal/virsh/client.go @@ -49,6 +49,10 @@ func (c *Client) VirtInstall(ctx context.Context, opts *VirtInstallOptions) erro "--noautoconsole", } + if opts.BootFirmware == "uefi" { + args = append(args, "--boot", "uefi") + } + // Add shared memory support if filesystems are present (required for virtiofs) if len(opts.Filesystems) > 0 { args = append(args, "--memorybacking", "source.type=memfd,access.mode=shared") diff --git a/internal/virsh/types.go b/internal/virsh/types.go index 1516c0a..eb9ffee 100644 --- a/internal/virsh/types.go +++ b/internal/virsh/types.go @@ -8,6 +8,7 @@ type VirtInstallOptions struct { Memory int MaxMemory int VCPUs int + BootFirmware string Disks []string Networks []NetworkConfig Filesystems []FilesystemConfig diff --git a/test/integration/cluster_test.go b/test/integration/cluster_test.go index 2ac0d61..f67df5c 100644 --- a/test/integration/cluster_test.go +++ b/test/integration/cluster_test.go @@ -42,7 +42,11 @@ var _ = Describe("Cluster Lifecycle", func() { targetImgRef := "registry.cluster.local:5000/node:latest" By("Creating cluster with --expose, custom node name, memory ballooning, and target-imgref") - cmd := helpers.BinkCmd("cluster", "start", "--cluster-name", clusterName, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-name", customNodeName, "--expose", kubeconfigPath, "--target-imgref", targetImgRef, "--node-image", helpers.NodeImage()) + args := []string{"cluster", "start", "--cluster-name", clusterName, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-name", customNodeName, "--expose", kubeconfigPath, "--target-imgref", targetImgRef, "--node-image", helpers.NodeImage()} + if fw := helpers.BootFirmware(); fw != "" { + args = append(args, "--boot-firmware", fw) + } + cmd := helpers.BinkCmd(args...) session := helpers.RunCommand(cmd) By("Verifying cluster creation command succeeded") diff --git a/test/integration/helpers/cluster.go b/test/integration/helpers/cluster.go index ba02373..f99ac9f 100644 --- a/test/integration/helpers/cluster.go +++ b/test/integration/helpers/cluster.go @@ -25,6 +25,12 @@ func NodeImage() string { return config.DefaultNodeImage } +// BootFirmware returns the boot firmware to use for tests. +// It reads BINK_BOOT_FIRMWARE env var, falling back to empty (bios default). +func BootFirmware() string { + return os.Getenv("BINK_BOOT_FIRMWARE") +} + // GenerateTestClusterName creates a unique cluster name for testing func GenerateTestClusterName() string { return fmt.Sprintf("test-bink-%s", uuid.New().String()[:8]) @@ -57,6 +63,9 @@ func RunCommand(cmd *exec.Cmd, timeout ...time.Duration) *gexec.Session { func CreateCluster(name string, extraArgs ...string) { GinkgoWriter.Printf("Creating cluster: %s (with auto-assigned API port)\n", name) args := []string{"cluster", "start", "--cluster-name", name, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-image", NodeImage()} + if fw := BootFirmware(); fw != "" { + args = append(args, "--boot-firmware", fw) + } args = append(args, extraArgs...) cmd := BinkCmd(args...) session := RunCommand(cmd, 10*time.Minute) @@ -66,7 +75,11 @@ func CreateCluster(name string, extraArgs ...string) { // CreateClusterWithNodeName creates a cluster with a custom control-plane node name func CreateClusterWithNodeName(name, nodeName string) { GinkgoWriter.Printf("Creating cluster: %s with node name: %s (with auto-assigned API port)\n", name, nodeName) - cmd := BinkCmd("cluster", "start", "--cluster-name", name, "--node-name", nodeName, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-image", NodeImage()) + args := []string{"cluster", "start", "--cluster-name", name, "--node-name", nodeName, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-image", NodeImage()} + if fw := BootFirmware(); fw != "" { + args = append(args, "--boot-firmware", fw) + } + cmd := BinkCmd(args...) session := RunCommand(cmd, 10*time.Minute) Expect(session.ExitCode()).To(Equal(0), "Failed to create cluster: %s", string(session.Err.Contents())) } @@ -75,6 +88,9 @@ func CreateClusterWithNodeName(name, nodeName string) { func AddNode(clusterName, nodeName string, extraArgs ...string) { GinkgoWriter.Printf("Adding node %s to cluster %s\n", nodeName, clusterName) args := []string{"node", "add", nodeName, "--cluster-name", clusterName} + if fw := BootFirmware(); fw != "" { + args = append(args, "--boot-firmware", fw) + } args = append(args, extraArgs...) // Use lower memory for worker nodes since they don't run control plane components From 1f82ea320723553da033620691d8c99c247d04b4 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 20 Oct 2026 10:11:34 +0000 Subject: [PATCH 4/4] ci: enable UEFI boot for composefs integration tests Bump VM memory to 2048MB when using UEFI because OVMF firmware consumes ~260MB, leaving less than kubeadm's 1700MB minimum with the default 1900MB. Assisted-by: AI Signed-off-by: Alice Frosi --- .github/workflows/integration-tests.yml | 1 + test/integration/cluster_test.go | 6 +++++- test/integration/helpers/cluster.go | 18 +++++++++++++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4ed72b1..5bfb019 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -285,6 +285,7 @@ jobs: timeout-minutes: 90 env: BINK_NODE_IMAGE: ghcr.io/bootc-dev/bink/node:v${{ needs.supported-versions.outputs.default-version }}-fedora-${{ needs.supported-versions.outputs.fedora-version }}-disk-composefs + BINK_BOOT_FIRMWARE: uefi - name: Collect logs if: failure() diff --git a/test/integration/cluster_test.go b/test/integration/cluster_test.go index f67df5c..2d89774 100644 --- a/test/integration/cluster_test.go +++ b/test/integration/cluster_test.go @@ -42,7 +42,11 @@ var _ = Describe("Cluster Lifecycle", func() { targetImgRef := "registry.cluster.local:5000/node:latest" By("Creating cluster with --expose, custom node name, memory ballooning, and target-imgref") - args := []string{"cluster", "start", "--cluster-name", clusterName, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-name", customNodeName, "--expose", kubeconfigPath, "--target-imgref", targetImgRef, "--node-image", helpers.NodeImage()} + memory := "1900" + if helpers.BootFirmware() == "uefi" { + memory = "2048" + } + args := []string{"cluster", "start", "--cluster-name", clusterName, "--api-port", "0", "--memory", memory, "--max-memory", "4096", "--node-name", customNodeName, "--expose", kubeconfigPath, "--target-imgref", targetImgRef, "--node-image", helpers.NodeImage()} if fw := helpers.BootFirmware(); fw != "" { args = append(args, "--boot-firmware", fw) } diff --git a/test/integration/helpers/cluster.go b/test/integration/helpers/cluster.go index f99ac9f..bf008e5 100644 --- a/test/integration/helpers/cluster.go +++ b/test/integration/helpers/cluster.go @@ -62,7 +62,11 @@ func RunCommand(cmd *exec.Cmd, timeout ...time.Duration) *gexec.Session { // Uses auto-assigned ports (--api-port 0) to avoid port conflicts in tests func CreateCluster(name string, extraArgs ...string) { GinkgoWriter.Printf("Creating cluster: %s (with auto-assigned API port)\n", name) - args := []string{"cluster", "start", "--cluster-name", name, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-image", NodeImage()} + memory := "1900" + if BootFirmware() == "uefi" { + memory = "2048" + } + args := []string{"cluster", "start", "--cluster-name", name, "--api-port", "0", "--memory", memory, "--max-memory", "4096", "--node-image", NodeImage()} if fw := BootFirmware(); fw != "" { args = append(args, "--boot-firmware", fw) } @@ -75,7 +79,11 @@ func CreateCluster(name string, extraArgs ...string) { // CreateClusterWithNodeName creates a cluster with a custom control-plane node name func CreateClusterWithNodeName(name, nodeName string) { GinkgoWriter.Printf("Creating cluster: %s with node name: %s (with auto-assigned API port)\n", name, nodeName) - args := []string{"cluster", "start", "--cluster-name", name, "--node-name", nodeName, "--api-port", "0", "--memory", "1900", "--max-memory", "4096", "--node-image", NodeImage()} + memory := "1900" + if BootFirmware() == "uefi" { + memory = "2048" + } + args := []string{"cluster", "start", "--cluster-name", name, "--node-name", nodeName, "--api-port", "0", "--memory", memory, "--max-memory", "4096", "--node-image", NodeImage()} if fw := BootFirmware(); fw != "" { args = append(args, "--boot-firmware", fw) } @@ -102,7 +110,11 @@ func AddNode(clusterName, nodeName string, extraArgs ...string) { } } if isControlPlane { - args = append(args, "--memory", "1900", "--max-memory", "4096") + memory := "1900" + if BootFirmware() == "uefi" { + memory = "2048" + } + args = append(args, "--memory", memory, "--max-memory", "4096") } else { args = append(args, "--memory", "768", "--max-memory", "2048") }