From 8ec6fc1d8a8523c4f15da51f8b703986a8f995fd Mon Sep 17 00:00:00 2001 From: Andrew McDermott Date: Mon, 12 Jan 2026 09:22:16 +0000 Subject: [PATCH 1/5] Remove BpffsInitImage configuration option from Config API The BpffsInitImage field in DaemonSpec allowed overriding the init container image that mounts bpffs. This configurability is no longer required as the init container will use the agent image directly in subsequent changes. Remove the field from the API, the corresponding override logic from the controller, and update the tests to only cover CSI registrar image overrides. Signed-off-by: Andrew McDermott --- apis/v1alpha1/config_types.go | 3 -- controllers/bpfman-operator/config.go | 7 ---- controllers/bpfman-operator/config_test.go | 42 +++------------------- 3 files changed, 5 insertions(+), 47 deletions(-) diff --git a/apis/v1alpha1/config_types.go b/apis/v1alpha1/config_types.go index 4ed3e8364..cd585caf3 100644 --- a/apis/v1alpha1/config_types.go +++ b/apis/v1alpha1/config_types.go @@ -56,9 +56,6 @@ type ConfigSpec struct { // DaemonSpec defines the desired state of the bpfman daemon. type DaemonSpec struct { - // BpffsInitImage holds the image for the init container that mounts bpffs. - // +optional - BpffsInitImage string `json:"bpffsInitImage,omitempty"` // CsiRegistrarImage holds the image for the CSI node driver registrar // sidecar container. // +optional diff --git a/controllers/bpfman-operator/config.go b/controllers/bpfman-operator/config.go index 35931f7b2..fd2a97062 100644 --- a/controllers/bpfman-operator/config.go +++ b/controllers/bpfman-operator/config.go @@ -288,13 +288,6 @@ func configureBpfmanDs(staticBpfmanDS *appsv1.DaemonSet, config *v1alpha1.Config // Do nothing } } - - // Configure init containers - for cindex, container := range staticBpfmanDS.Spec.Template.Spec.InitContainers { - if container.Name == internal.BpfmanInitContainerName && config.Spec.Daemon.BpffsInitImage != "" { - staticBpfmanDS.Spec.Template.Spec.InitContainers[cindex].Image = config.Spec.Daemon.BpffsInitImage - } - } } // configureMetricsProxyDs configures the metrics-proxy DaemonSet with runtime values. diff --git a/controllers/bpfman-operator/config_test.go b/controllers/bpfman-operator/config_test.go index f0c1cf36d..4d11c9dc8 100644 --- a/controllers/bpfman-operator/config_test.go +++ b/controllers/bpfman-operator/config_test.go @@ -581,45 +581,25 @@ func verifyCM(cm *corev1.ConfigMap, requiredFields map[string]*string) error { return nil } -// TestConfigureBpfmanDsImageOverrides verifies that optional image -// fields in the Config CR correctly override container images in the -// DaemonSet. -func TestConfigureBpfmanDsImageOverrides(t *testing.T) { +// TestConfigureBpfmanDsCsiRegistrarImageOverride verifies that the optional +// CsiRegistrarImage field in the Config CR correctly overrides the CSI node +// driver registrar container image in the DaemonSet. +func TestConfigureBpfmanDsCsiRegistrarImageOverride(t *testing.T) { tests := []struct { name string - bpffsInitImage string csiRegistrarImage string - expectedInitImage string expectedCsiRegistrarImage string }{ { - name: "defaults preserved when fields empty", - bpffsInitImage: "", + name: "default preserved when field empty", csiRegistrarImage: "", - expectedInitImage: "quay.io/fedora/fedora-minimal:39", - expectedCsiRegistrarImage: "quay.io/bpfman/csi-node-driver-registrar:v2.13.0", - }, - { - name: "init image overridden when set", - bpffsInitImage: "registry.example.com/custom-init:v1", - csiRegistrarImage: "", - expectedInitImage: "registry.example.com/custom-init:v1", expectedCsiRegistrarImage: "quay.io/bpfman/csi-node-driver-registrar:v2.13.0", }, { name: "csi registrar image overridden when set", - bpffsInitImage: "", csiRegistrarImage: "registry.example.com/custom-csi:v1", - expectedInitImage: "quay.io/fedora/fedora-minimal:39", expectedCsiRegistrarImage: "registry.example.com/custom-csi:v1", }, - { - name: "both images overridden when set", - bpffsInitImage: "registry.example.com/custom-init:v2", - csiRegistrarImage: "registry.example.com/custom-csi:v2", - expectedInitImage: "registry.example.com/custom-init:v2", - expectedCsiRegistrarImage: "registry.example.com/custom-csi:v2", - }, } for _, tc := range tests { @@ -645,7 +625,6 @@ func TestConfigureBpfmanDsImageOverrides(t *testing.T) { Daemon: v1alpha1.DaemonSpec{ Image: "quay.io/bpfman/bpfman:latest", LogLevel: "info", - BpffsInitImage: tc.bpffsInitImage, CsiRegistrarImage: tc.csiRegistrarImage, }, }, @@ -654,17 +633,6 @@ func TestConfigureBpfmanDsImageOverrides(t *testing.T) { // Apply configuration to the DaemonSet. configureBpfmanDs(ds, config) - // Verify init container image. - var initImage string - for _, c := range ds.Spec.Template.Spec.InitContainers { - if c.Name == internal.BpfmanInitContainerName { - initImage = c.Image - break - } - } - require.Equal(t, tc.expectedInitImage, initImage, - "init container image mismatch") - // Verify CSI registrar container image. var csiImage string for _, c := range ds.Spec.Template.Spec.Containers { From f64349ec2cc6a8465fe38dffcaf28ba6273ca659 Mon Sep 17 00:00:00 2001 From: Andrew McDermott Date: Mon, 12 Jan 2026 09:22:29 +0000 Subject: [PATCH 2/5] Regenerate CRD manifests and OLM bundle Regenerate the Config CRD and OLM bundle manifests following the removal of the BpffsInitImage field from the DaemonSpec API. Signed-off-by: Andrew McDermott --- bundle/manifests/bpfman-operator.clusterserviceversion.yaml | 2 +- bundle/manifests/bpfman.io_configs.yaml | 4 ---- config/crd/bases/bpfman.io_configs.yaml | 4 ---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/bundle/manifests/bpfman-operator.clusterserviceversion.yaml b/bundle/manifests/bpfman-operator.clusterserviceversion.yaml index eb47c260e..e8806cb26 100644 --- a/bundle/manifests/bpfman-operator.clusterserviceversion.yaml +++ b/bundle/manifests/bpfman-operator.clusterserviceversion.yaml @@ -1014,7 +1014,7 @@ metadata: capabilities: Basic Install categories: OpenShift Optional containerImage: quay.io/bpfman/bpfman-operator:latest - createdAt: "2025-12-23T10:13:33Z" + createdAt: "2026-01-12T09:20:12Z" description: The bpfman Operator is designed to manage eBPF programs for applications. features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "false" diff --git a/bundle/manifests/bpfman.io_configs.yaml b/bundle/manifests/bpfman.io_configs.yaml index 316047a97..0576be23b 100644 --- a/bundle/manifests/bpfman.io_configs.yaml +++ b/bundle/manifests/bpfman.io_configs.yaml @@ -70,10 +70,6 @@ spec: daemon: description: Daemon holds the configuration for the bpfman daemon. properties: - bpffsInitImage: - description: BpffsInitImage holds the image for the init container - that mounts bpffs. - type: string csiRegistrarImage: description: |- CsiRegistrarImage holds the image for the CSI node driver registrar diff --git a/config/crd/bases/bpfman.io_configs.yaml b/config/crd/bases/bpfman.io_configs.yaml index b701988a4..0ac5a7beb 100644 --- a/config/crd/bases/bpfman.io_configs.yaml +++ b/config/crd/bases/bpfman.io_configs.yaml @@ -70,10 +70,6 @@ spec: daemon: description: Daemon holds the configuration for the bpfman daemon. properties: - bpffsInitImage: - description: BpffsInitImage holds the image for the init container - that mounts bpffs. - type: string csiRegistrarImage: description: |- CsiRegistrarImage holds the image for the CSI node driver registrar From 31b4fb288424e0b822556728f18ca77e01bf5fe3 Mon Sep 17 00:00:00 2001 From: Andrew McDermott Date: Tue, 6 Jan 2026 13:11:48 +0000 Subject: [PATCH 3/5] Add bpffs mount capability to bpfman-agent The init container that ensures bpffs is mounted before the main containers start previously used a fedora-minimal image with findmnt and mount utilities. This had two drawbacks: pulling an additional container image on startup, and incompatibility with minimal base images like ubi9-minimal that lack these utilities. By moving this functionality into the agent, downstream bpfman distributions no longer need a custom init container image, and the operator does not need to parameterise the init container image reference. Extend bpfman-agent with --mount-bpffs flag to handle this directly. The agent now parses /proc/self/mountinfo to check for existing bpf mounts (matching libmount's parsing approach from util-linux) and uses syscall.Mount when needed. This eliminates the external image dependency since the agent image is already pulled for the main container. Add internal/bpffs package with: - IsMounted: parse mountinfo to detect bpf mounts at a given path - Mount: create bpffs mount, creating the directory if needed - Unmount: remove a bpffs mount - EnsureMounted: idempotent helper for the common check-then-mount pattern The --mount-bpffs-remount flag forces a fresh mount by unmounting first if already mounted, useful for testing the mount code path. Both modes handle the race where another process might mount between check and mount by treating mount failures as success if the filesystem is now mounted. Signed-off-by: Andrew McDermott --- cmd/bpfman-agent/main.go | 73 ++++++++++ config/bpfman-deployment/daemonset.yaml | 11 +- internal/bpffs/bpffs.go | 162 +++++++++++++++++++++ internal/bpffs/bpffs_test.go | 181 ++++++++++++++++++++++++ 4 files changed, 418 insertions(+), 9 deletions(-) create mode 100644 internal/bpffs/bpffs.go create mode 100644 internal/bpffs/bpffs_test.go diff --git a/cmd/bpfman-agent/main.go b/cmd/bpfman-agent/main.go index c9f7e717c..dee60a988 100644 --- a/cmd/bpfman-agent/main.go +++ b/cmd/bpfman-agent/main.go @@ -23,11 +23,13 @@ import ( "net" "net/http" "os" + "strings" "sync" "time" bpfmaniov1alpha1 "github.com/bpfman/bpfman-operator/apis/v1alpha1" bpfmanagent "github.com/bpfman/bpfman-operator/controllers/bpfman-agent" + "github.com/bpfman/bpfman-operator/internal/bpffs" "github.com/bpfman/bpfman-operator/internal/conn" gobpfman "github.com/bpfman/bpfman/clients/gobpfman/v1" @@ -69,6 +71,59 @@ func init() { //+kubebuilder:scaffold:scheme } +// handleMountBPFFS performs the --mount-bpffs logic. It returns +// (exitCode, message, err). +func handleMountBPFFS(mountInfoPath, mountPoint string, remount bool) (int, string, error) { + if !remount { + mounted, err := bpffs.IsMounted(mountInfoPath, mountPoint) + if err != nil { + return 1, "", fmt.Errorf("failed to check mount status at %s: %w", mountPoint, err) + } + if mounted { + return 0, fmt.Sprintf("bpffs already mounted at %s\n", mountPoint), nil + } + + if err := bpffs.Mount(mountPoint); err != nil { + // Handle race: another process may have mounted + // between our check and mount attempt. + mounted, mErr := bpffs.IsMounted(mountInfoPath, mountPoint) + if mErr == nil && mounted { + return 0, fmt.Sprintf("bpffs already mounted at %s\n", mountPoint), nil + } + return 1, "", fmt.Errorf("failed to mount bpffs at %s: %w", mountPoint, err) + } + return 0, fmt.Sprintf("bpffs mounted at %s\n", mountPoint), nil + } + + // Remount mode: exercise Mount() code path. + mounted, err := bpffs.IsMounted(mountInfoPath, mountPoint) + if err != nil { + return 1, "", fmt.Errorf("failed to check mount status at %s: %w", mountPoint, err) + } + + var msg strings.Builder + if mounted { + if err := bpffs.Unmount(mountPoint); err != nil { + return 1, "", fmt.Errorf("failed to unmount bpffs at %s: %w", mountPoint, err) + } + fmt.Fprintf(&msg, "bpffs unmounted at %s\n", mountPoint) + } + + if err := bpffs.Mount(mountPoint); err != nil { + // Handle race: another process may have mounted + // between our check and mount attempt. + mounted, mErr := bpffs.IsMounted(mountInfoPath, mountPoint) + if mErr == nil && mounted { + fmt.Fprintf(&msg, "bpffs already mounted at %s\n", mountPoint) + return 0, msg.String(), nil + } + return 1, "", fmt.Errorf("failed to mount bpffs at %s: %w", mountPoint, err) + } + + fmt.Fprintf(&msg, "bpffs mounted at %s\n", mountPoint) + return 0, msg.String(), nil +} + // agentMetricsServer provides an HTTP server for exposing Prometheus // metrics on a Unix domain socket. // @@ -317,9 +372,27 @@ func main() { flag.StringVar(&pprofAddr, "profiling-bind-address", "", "The address the profiling endpoint binds to, such as ':6060'. Leave unset to disable profiling.") flag.BoolVar(&enableInterfacesDiscovery, "enable-interfaces-discovery", true, "Enable ebpfman agent process to auto detect interfaces creation and deletion") flag.StringVar(&certDir, "cert-dir", "/tmp/k8s-webhook-server/serving-certs", "The directory containing TLS certificates for HTTPS servers.") + mountBPFFS := flag.Bool("mount-bpffs", false, "Ensure bpffs is mounted at the given path, then exit (init-container mode).") + mountBPFFSPath := flag.String("mount-bpffs-path", bpffs.DefaultMountPoint, "Path where bpffs should be mounted.") + remountBPFFS := flag.Bool("mount-bpffs-remount", false, "Unmount bpffs if mounted, then mount it (testing only).") flag.Parse() + // Handle --mount-bpffs mode: mount bpffs if needed and exit. + // This is used by init containers to ensure bpffs is mounted + // before the main containers start, without requiring + // external utilities. + if *mountBPFFS { + code, out, err := handleMountBPFFS(bpffs.DefaultMountInfoPath, *mountBPFFSPath, *remountBPFFS) + if out != "" { + fmt.Print(out) + } + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + } + os.Exit(code) + } + // Get the Log level for bpfman deployment where this pod is running. logLevel := os.Getenv("GO_LOG") switch logLevel { diff --git a/config/bpfman-deployment/daemonset.yaml b/config/bpfman-deployment/daemonset.yaml index 2dde01a53..435cd001d 100644 --- a/config/bpfman-deployment/daemonset.yaml +++ b/config/bpfman-deployment/daemonset.yaml @@ -41,15 +41,8 @@ spec: ## in kind so mount it if needed. initContainers: - name: mount-bpffs - image: quay.io/fedora/fedora-minimal:39 - command: - - /bin/sh - - -xc - - | - #!/bin/sh - if ! /usr/bin/findmnt --noheadings --types bpf /sys/fs/bpf >/dev/null 2>&1; then - /bin/mount bpffs /sys/fs/bpf -t bpf - fi + image: quay.io/bpfman/bpfman-agent:latest + command: ["/bpfman-agent", "--mount-bpffs"] securityContext: privileged: true runAsUser: 0 diff --git a/internal/bpffs/bpffs.go b/internal/bpffs/bpffs.go new file mode 100644 index 000000000..1c537a2b7 --- /dev/null +++ b/internal/bpffs/bpffs.go @@ -0,0 +1,162 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package bpffs provides functions to check and mount the BPF +// filesystem. +package bpffs + +import ( + "bufio" + "fmt" + "os" + "strings" + "syscall" +) + +const ( + // DefaultMountPoint is the standard location for the BPF + // filesystem. + DefaultMountPoint = "/sys/fs/bpf" + + // DefaultMountInfoPath is the path to the mountinfo file. + DefaultMountInfoPath = "/proc/self/mountinfo" + + // defaultScanMaxLineLen is the maximum line length for + // scanning mountinfo. Some nodes/runtimes can produce long + // lines; this prevents ErrTooLong. + defaultScanMaxLineLen = 1024 * 1024 +) + +// IsMounted reports whether a bpffs is mounted at mountPoint by +// parsing mountInfoPath (e.g. /proc/self/mountinfo). +// +// The mountinfo format is documented in proc(5). Each line contains: +// +// mount_id parent_id major:minor root mount_point options [optional_fields...] - fstype source super_options +// +// Example bpffs entry: +// +// 30 22 0:27 / /sys/fs/bpf rw,nosuid shared:9 - bpf bpf rw,mode=700 +// ↑ ↑ +// mount_point (fields[4]) fstype (after " - ") +// +// The key insight from libmount (util-linux) is that the separator " +// - " must be found using string search, not by assuming a fixed +// field position. This is because optional fields (like "shared:N" +// for mount propagation) may be present between the mount options and +// the separator. +func IsMounted(mountInfoPath, mountPoint string) (bool, error) { + file, err := os.Open(mountInfoPath) + if err != nil { + return false, fmt.Errorf("opening mountinfo: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 0, 64*1024), defaultScanMaxLineLen) + + for scanner.Scan() { + line := scanner.Text() + + // Find the separator " - " which precedes "fstype + // source super_options". This is how libmount parses + // mountinfo (see mnt_parse_mountinfo_line). + sepIdx := strings.Index(line, " - ") + if sepIdx == -1 { + continue + } + + // Parse the prefix: mount_id parent_id major:minor + // root mount_point ... + prefix := line[:sepIdx] + fields := strings.Fields(prefix) + if len(fields) < 5 { + continue + } + mntPoint := fields[4] + + // Parse the suffix after " - ": fstype source + // super_options. + suffix := line[sepIdx+3:] // skip " - " + suffixFields := strings.Fields(suffix) + if len(suffixFields) < 1 { + continue + } + fsType := suffixFields[0] + + // Match: bpffs at the requested path. + if mntPoint == mountPoint && fsType == "bpf" { + return true, nil + } + } + + if err := scanner.Err(); err != nil { + return false, fmt.Errorf("reading mountinfo: %w", err) + } + + return false, nil +} + +// Mount mounts a bpffs at mountPoint, creating the directory if needed. +func Mount(mountPoint string) error { + fi, err := os.Stat(mountPoint) + switch { + case err == nil: + if !fi.IsDir() { + return fmt.Errorf("mount point exists but is not a directory") + } + case os.IsNotExist(err): + if err := os.MkdirAll(mountPoint, 0755); err != nil { + return fmt.Errorf("creating mount point directory: %w", err) + } + default: + return fmt.Errorf("stat mount point: %w", err) + } + + if err := syscall.Mount("bpffs", mountPoint, "bpf", 0, ""); err != nil { + return fmt.Errorf("mount syscall: %w", err) + } + + return nil +} + +// Unmount unmounts the bpffs at mountPoint. +func Unmount(mountPoint string) error { + if err := syscall.Unmount(mountPoint, 0); err != nil { + return fmt.Errorf("unmount syscall: %w", err) + } + return nil +} + +// EnsureMounted ensures a bpffs is mounted at mountPoint. It checks +// mountInfoPath (e.g. /proc/self/mountinfo) for an existing bpf mount +// at mountPoint; if none is found, it mounts one. +// +// Equivalent to: +// +// if ! findmnt --noheadings --types bpf ; then +// mount bpffs -t bpf +// fi +func EnsureMounted(mountInfoPath, mountPoint string) error { + mounted, err := IsMounted(mountInfoPath, mountPoint) + if err != nil { + return err + } + if mounted { + return nil + } + return Mount(mountPoint) +} diff --git a/internal/bpffs/bpffs_test.go b/internal/bpffs/bpffs_test.go new file mode 100644 index 000000000..baabb7c97 --- /dev/null +++ b/internal/bpffs/bpffs_test.go @@ -0,0 +1,181 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bpffs_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/bpfman/bpfman-operator/internal/bpffs" +) + +func TestBPFFSIsMounted(t *testing.T) { + tests := []struct { + name string + mountinfo string + mountPoint string + want bool + }{ + { + name: "util-linux format without propagation - no bpf", + mountinfo: `15 20 0:3 / /proc rw,relatime - proc /proc rw +16 20 0:15 / /sys rw,relatime - sysfs /sys rw +17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 +20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered +`, + mountPoint: "/sys/fs/bpf", + want: false, + }, + { + name: "util-linux format without propagation - with bpf", + mountinfo: `15 20 0:3 / /proc rw,relatime - proc /proc rw +16 20 0:15 / /sys rw,relatime - sysfs /sys rw +17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755 +20 1 8:4 / / rw,noatime - ext3 /dev/sda4 rw,errors=continue,user_xattr,acl,barrier=0,data=ordered +48 16 0:39 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime - bpf bpf rw,mode=700 +`, + mountPoint: "/sys/fs/bpf", + want: true, + }, + { + name: "NixOS format with propagation - no bpf", + mountinfo: `22 31 0:6 / /dev rw,nosuid shared:12 - devtmpfs devtmpfs rw,size=6532720k,nr_inodes=16327128,mode=755 +25 31 0:23 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw +28 31 0:26 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw +36 28 0:35 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:8 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot +`, + mountPoint: "/sys/fs/bpf", + want: false, + }, + { + name: "NixOS format with propagation - with bpf", + mountinfo: `22 31 0:6 / /dev rw,nosuid shared:12 - devtmpfs devtmpfs rw,size=6532720k,nr_inodes=16327128,mode=755 +25 31 0:23 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw +28 31 0:26 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw +36 28 0:35 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:8 - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot +39 28 0:38 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime shared:11 - bpf bpf rw,gid=983,mode=770 +`, + mountPoint: "/sys/fs/bpf", + want: true, + }, + { + name: "CoreOS format with propagation - no bpf", + mountinfo: `21 72 0:20 / /proc rw,nosuid,nodev,noexec,relatime shared:15 - proc proc rw +22 72 0:21 / /sys rw,nosuid,nodev,noexec,relatime shared:5 - sysfs sysfs rw,seclabel +23 72 0:5 / /dev rw,nosuid shared:11 - devtmpfs devtmpfs rw,seclabel,size=4096k,nr_inodes=4094014,mode=755,inode64 +28 22 0:25 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:7 - cgroup2 cgroup2 rw,seclabel +`, + mountPoint: "/sys/fs/bpf", + want: false, + }, + { + name: "CoreOS format with propagation - with bpf", + mountinfo: `21 72 0:20 / /proc rw,nosuid,nodev,noexec,relatime shared:15 - proc proc rw +22 72 0:21 / /sys rw,nosuid,nodev,noexec,relatime shared:5 - sysfs sysfs rw,seclabel +23 72 0:5 / /dev rw,nosuid shared:11 - devtmpfs devtmpfs rw,seclabel,size=4096k,nr_inodes=4094014,mode=755,inode64 +28 22 0:25 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:7 - cgroup2 cgroup2 rw,seclabel +30 22 0:27 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime shared:9 - bpf bpf rw,mode=700 +`, + mountPoint: "/sys/fs/bpf", + want: true, + }, + { + name: "different mount point", + mountinfo: `30 22 0:27 / /sys/fs/bpf rw,nosuid,nodev,noexec,relatime shared:9 - bpf bpf rw,mode=700 +`, + mountPoint: "/some/other/path", + want: false, + }, + { + name: "multiple optional fields", + mountinfo: `30 22 0:27 / /sys/fs/bpf rw,nosuid shared:9 master:1 - bpf bpf rw,mode=700 +`, + mountPoint: "/sys/fs/bpf", + want: true, + }, + { + name: "empty file", + mountinfo: "", + mountPoint: "/sys/fs/bpf", + want: false, + }, + { + name: "malformed line without separator", + mountinfo: `this line has no separator +30 22 0:27 / /sys/fs/bpf rw,nosuid shared:9 - bpf bpf rw,mode=700 +`, + mountPoint: "/sys/fs/bpf", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary file with the test mountinfo content + tmpDir := t.TempDir() + mountInfoPath := filepath.Join(tmpDir, "mountinfo") + if err := os.WriteFile(mountInfoPath, []byte(tt.mountinfo), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := bpffs.IsMounted(mountInfoPath, tt.mountPoint) + if err != nil { + t.Fatalf("IsMounted() error = %v", err) + } + if got != tt.want { + t.Errorf("IsMounted() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBPFFSIsMounted_FileNotFound(t *testing.T) { + _, err := bpffs.IsMounted("/nonexistent/path/mountinfo", "/sys/fs/bpf") + if err == nil { + t.Error("IsMounted() expected error for nonexistent file, got nil") + } +} + +func TestBPFFSIsMounted_LongLine(t *testing.T) { + // Generate a mountinfo line > 64 KiB (default scanner limit). + // This tests the scanner buffer increase (prevents ErrTooLong). + // Target ~70 KiB to ensure it fails without the buffer bump. + var b strings.Builder + b.WriteString("30 22 0:27 / /sys/fs/bpf rw") + for b.Len() < 70*1024 { + b.WriteString(",option") + b.WriteByte(byte('a' + (b.Len() % 26))) + } + b.WriteString(" shared:9 - bpf bpf rw,mode=700\n") + mountinfo := b.String() + + tmpDir := t.TempDir() + mountInfoPath := filepath.Join(tmpDir, "mountinfo") + if err := os.WriteFile(mountInfoPath, []byte(mountinfo), 0644); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + got, err := bpffs.IsMounted(mountInfoPath, "/sys/fs/bpf") + if err != nil { + t.Fatalf("IsMounted() error = %v (scanner buffer may be too small)", err) + } + if !got { + t.Error("IsMounted() = false, want true") + } +} From 82de0342772d50f1f1f4e9125583f3ddc8f29dbf Mon Sep 17 00:00:00 2001 From: Andrew McDermott Date: Tue, 6 Jan 2026 13:46:37 +0000 Subject: [PATCH 4/5] Update init container image when configuring the DaemonSet The configureBpfmanDs function was only updating images for the main containers, leaving the mount-bpffs init container with a hardcoded image reference. This caused CI failures because the init container would use quay.io/bpfman/bpfman-agent:latest instead of the freshly built test image, meaning the --mount-bpffs functionality was not available. Add logic to update the init container image from config.Spec.Agent.Image, consistent with how the bpfman-agent container image is configured. Signed-off-by: Andrew McDermott --- controllers/bpfman-operator/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/controllers/bpfman-operator/config.go b/controllers/bpfman-operator/config.go index fd2a97062..33a0cdf95 100644 --- a/controllers/bpfman-operator/config.go +++ b/controllers/bpfman-operator/config.go @@ -266,6 +266,14 @@ func configureBpfmanDs(staticBpfmanDS *appsv1.DaemonSet, config *v1alpha1.Config staticBpfmanDS.Name = internal.BpfmanDsName staticBpfmanDS.Namespace = config.Spec.Namespace staticBpfmanDS.Spec.Template.Spec.AutomountServiceAccountToken = ptr.To(true) + + // Update init container images + for cindex, container := range staticBpfmanDS.Spec.Template.Spec.InitContainers { + if container.Name == internal.BpfmanInitContainerName { + staticBpfmanDS.Spec.Template.Spec.InitContainers[cindex].Image = config.Spec.Agent.Image + } + } + for cindex, container := range staticBpfmanDS.Spec.Template.Spec.Containers { switch container.Name { case internal.BpfmanContainerName: From 093ba4b681a4cbd37c88f5994f66d0e1dadb28c0 Mon Sep 17 00:00:00 2001 From: Andrew McDermott Date: Tue, 6 Jan 2026 14:37:10 +0000 Subject: [PATCH 5/5] Fix lifecycle test to use CI-loaded images and correct timeout error The lifecycle test was failing because it used hardcoded :latest image tags when creating a new Config, but CI only loads :int-test images into the kind cluster. The init container now uses the agent image from the Config, so when the test created a Config with :latest images, the pods would fail to start. Read image tags from environment variables (BPFMAN_IMG, BPFMAN_AGENT_IMG) with fallback to :latest defaults for local testing. This aligns with how the integration tests handle images. Also fix the error formatting in waitUntilCondition which was using ctx.Err() instead of timeoutCTX.Err(), producing malformed error messages like "%!w()" when the timeout fired. Signed-off-by: Andrew McDermott --- test/lifecycle/lifecycle_test.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/lifecycle/lifecycle_test.go b/test/lifecycle/lifecycle_test.go index 06e299f80..e0ee38c8c 100644 --- a/test/lifecycle/lifecycle_test.go +++ b/test/lifecycle/lifecycle_test.go @@ -6,6 +6,7 @@ package lifecycle import ( "context" "fmt" + "os" "slices" "testing" "time" @@ -42,6 +43,19 @@ const ( fieldOwner = "lifecycle-test" ) +// Image defaults - overridden by environment variables in CI. +var ( + bpfmanImage = getEnvOrDefault("BPFMAN_IMG", "quay.io/bpfman/bpfman:latest") + bpfmanAgentImage = getEnvOrDefault("BPFMAN_AGENT_IMG", "quay.io/bpfman/bpfman-agent:latest") +) + +func getEnvOrDefault(key, defaultValue string) string { + if v := os.Getenv(key); v != "" { + return v + } + return defaultValue +} + // Create new Config with modified settings var ( newConfig = &v1alpha1.Config{ @@ -57,13 +71,13 @@ millisec_delay = 10000 allow_unsigned = true verify_enabled = true`, Agent: v1alpha1.AgentSpec{ - Image: "quay.io/bpfman/bpfman-agent:latest", + Image: bpfmanAgentImage, LogLevel: "debug", // Changed from info to debug HealthProbePort: 8175, }, Daemon: v1alpha1.DaemonSpec{ - Image: "quay.io/bpfman/bpfman:latest", - LogLevel: "bpfman=info", // Changed from debug to info + Image: bpfmanImage, + LogLevel: "bpfman=info", }, }, } @@ -654,7 +668,7 @@ func waitUntilCondition(ctx context.Context, conditionFunc func() (bool, error)) for { select { case <-timeoutCTX.Done(): - return fmt.Errorf("timeout or context cancelled waiting for resource deletion: %w", ctx.Err()) + return fmt.Errorf("timeout or context cancelled: %w", timeoutCTX.Err()) case <-ticker.C: b, err := conditionFunc() if err != nil {