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/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/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/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 diff --git a/controllers/bpfman-operator/config.go b/controllers/bpfman-operator/config.go index 35931f7b2..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: @@ -288,13 +296,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 { 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") + } +} 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 {