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/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 75af550..0c72d4d 100644 --- a/internal/cli/cluster/start.go +++ b/internal/cli/cluster/start.go @@ -21,17 +21,22 @@ 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 + bootFirmware 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 +52,27 @@ 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") + cmd.Flags().StringVar(&opts.bootFirmware, "boot-firmware", "", "Boot firmware for the VM (uefi or bios, default: bios)") 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 +101,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 +127,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 +135,36 @@ 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), + node.WithBootFirmware(opts.bootFirmware), ) if err != nil { return fmt.Errorf("creating node: %w", err) @@ -168,23 +176,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 +225,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..d2e7f99 100644 --- a/internal/cli/node/add.go +++ b/internal/cli/node/add.go @@ -20,14 +20,19 @@ 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 + bootFirmware 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 +48,23 @@ 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.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 @@ -86,19 +92,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 +117,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 +144,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 +165,15 @@ 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), + 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/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 \ diff --git a/test/integration/cluster_test.go b/test/integration/cluster_test.go index 2ac0d61..2d89774 100644 --- a/test/integration/cluster_test.go +++ b/test/integration/cluster_test.go @@ -42,7 +42,15 @@ 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()) + 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) + } + 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..bf008e5 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]) @@ -56,7 +62,14 @@ 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) + } args = append(args, extraArgs...) cmd := BinkCmd(args...) session := RunCommand(cmd, 10*time.Minute) @@ -66,7 +79,15 @@ 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()) + 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) + } + 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 +96,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 @@ -86,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") }