diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-12 22:39:06 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-12 22:39:06 +0200 |
| commit | 13e7970afb3eeac69f82df833f030711e5cf12ec (patch) | |
| tree | 098c4fb8c5a8c8f27547f03f40c9fee0be63fe35 | |
| parent | 1b21e818a69bf73fde3ca60f89d2dc82a79fd605 (diff) | |
internal: embed BPF object into ior binary
| -rw-r--r-- | Magefile.go | 17 | ||||
| -rw-r--r-- | integrationtests/README.md | 5 | ||||
| -rw-r--r-- | integrationtests/cleanup_test.go | 10 | ||||
| -rw-r--r-- | integrationtests/harness.go | 11 | ||||
| -rw-r--r-- | integrationtests/harness_test.go | 29 | ||||
| -rw-r--r-- | integrationtests/helpers_test.go | 2 | ||||
| -rw-r--r-- | internal/bpfembed.go | 31 | ||||
| -rw-r--r-- | internal/ior.go | 4 | ||||
| -rw-r--r-- | internal/ior_setup_test.go | 97 |
9 files changed, 184 insertions, 22 deletions
diff --git a/Magefile.go b/Magefile.go index cc1feeb..36db529 100644 --- a/Magefile.go +++ b/Magefile.go @@ -70,19 +70,23 @@ func All() error { return nil } -// BpfBuild builds the BPF object and copies it to the repo root. +// BpfBuild builds the embedded BPF object used by the Go binary. func BpfBuild() error { if err := ensureVMLINUX(); err != nil { return err } - if err := buildBPFObject(); err != nil { - return err - } + return buildBPFObject() +} + +// BpfExport copies the built BPF object to the repo root for debug workflows. +func BpfExport() error { + mg.Deps(BpfBuild) return sh.RunV("cp", "-v", bpfObjectPath, bpfOutputPath) } // Test runs the full test suite. func Test() error { + mg.Deps(BpfBuild) if err := sh.RunWithV(goEnv(), "go", "clean", "-testcache"); err != nil { return err } @@ -91,6 +95,7 @@ func Test() error { // TestRace runs the full test suite with the race detector enabled. func TestRace() error { + mg.Deps(BpfBuild) if err := sh.RunWithV(goEnv(), "go", "clean", "-testcache"); err != nil { return err } @@ -99,6 +104,7 @@ func TestRace() error { // TestWithName runs a specific test by name. func TestWithName() error { + mg.Deps(BpfBuild) if err := sh.RunWithV(goEnv(), "go", "clean", "-testcache"); err != nil { return err } @@ -132,6 +138,7 @@ func TestWithName() error { // Bench runs benchmarks. func Bench() error { + mg.Deps(BpfBuild) if err := sh.RunWithV(goEnv(), "go", "test", "./...", "-v", "-bench=.", "-run", "xxx"); err != nil { return err } @@ -150,6 +157,7 @@ func PrReview() error { // BenchProf runs pipeline benchmarks and writes timestamped pprof artifacts. func BenchProf() error { + mg.Deps(BpfBuild) if err := ensureBenchProfilesDir(); err != nil { return err } @@ -244,6 +252,7 @@ func BenchFlameCmp() error { // BenchCompare runs all benchmarks repeatedly and stores output for benchstat. func BenchCompare() error { + mg.Deps(BpfBuild) if err := ensureBenchProfilesDir(); err != nil { return err } diff --git a/integrationtests/README.md b/integrationtests/README.md index 99a3fc0..be65499 100644 --- a/integrationtests/README.md +++ b/integrationtests/README.md @@ -6,9 +6,12 @@ harness asserts the captured `.ior.zst` output matches expectations. ## Prerequisites -- Built `ior` binary and `ior.bpf.o` (`mage all`) +- Built `ior` binary (`mage all`) - Root privileges or `CAP_BPF` (required for BPF tracepoint attachment) +The binary embeds its default BPF object. Set `IOR_BPF_OBJECT=/path/to/ior.bpf.o` +only when you explicitly want to override the embedded object during testing. + ## Running ```bash diff --git a/integrationtests/cleanup_test.go b/integrationtests/cleanup_test.go index 18a0531..316c1b1 100644 --- a/integrationtests/cleanup_test.go +++ b/integrationtests/cleanup_test.go @@ -80,8 +80,7 @@ func TestCleanupOutputDirContainsOnlyExpectedFiles(t *testing.T) { for _, e := range entries { name := e.Name() validSuffix := strings.HasSuffix(name, ".ior.zst") || - strings.HasSuffix(name, ".svg") || - name == "ior.bpf.o" // symlink created by startIor + strings.HasSuffix(name, ".svg") if !validSuffix { t.Errorf("unexpected file in output dir: %s", name) } @@ -172,11 +171,8 @@ func TestCleanupOutputDirEmptyAfterIorFailure(t *testing.T) { t.Fatalf("read output dir: %v", err) } - // Only the BPF symlink should exist; ior produced no output. - for _, e := range entries { - if e.Name() != "ior.bpf.o" { - t.Errorf("unexpected file in output dir after ior failure: %s", e.Name()) - } + if len(entries) != 0 { + t.Fatalf("expected empty output dir after ior failure, found %d entries", len(entries)) } } diff --git a/integrationtests/harness.go b/integrationtests/harness.go index 17ae994..e3ee900 100644 --- a/integrationtests/harness.go +++ b/integrationtests/harness.go @@ -15,6 +15,7 @@ import ( const ( workloadStartupTimeout = 5 * time.Second iorShutdownGrace = 30 * time.Second + bpfObjectOverrideEnv = "IOR_BPF_OBJECT" ) // TestHarness orchestrates integration tests by starting an ior trace @@ -22,7 +23,7 @@ const ( type TestHarness struct { IorBinary string // path to built ior binary WorkloadBinary string // path to built ioworkload binary - BpfObject string // path to ior.bpf.o + BpfObject string // optional path to external BPF object override OutputDir string // temp dir for .ior.zst output } @@ -120,11 +121,6 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) { } func (h *TestHarness) startIor(pid int, scenario string, duration int, extraArgs []string) (*exec.Cmd, error) { - bpfLink := filepath.Join(h.OutputDir, "ior.bpf.o") - if err := os.Symlink(h.BpfObject, bpfLink); err != nil { - return nil, fmt.Errorf("symlink bpf object: %w", err) - } - args := []string{ "-pid", strconv.Itoa(pid), "-flamegraph", @@ -136,6 +132,9 @@ func (h *TestHarness) startIor(pid int, scenario string, duration int, extraArgs cmd.Dir = h.OutputDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + if h.BpfObject != "" { + cmd.Env = append(os.Environ(), bpfObjectOverrideEnv+"="+h.BpfObject) + } if err := cmd.Start(); err != nil { return nil, fmt.Errorf("start ior: %w", err) diff --git a/integrationtests/harness_test.go b/integrationtests/harness_test.go index 6e076ad..27a122c 100644 --- a/integrationtests/harness_test.go +++ b/integrationtests/harness_test.go @@ -178,3 +178,32 @@ func TestIorStartFailureCleansUpWorkload(t *testing.T) { } } } + +func TestStartIorPassesBPFObjectOverrideEnv(t *testing.T) { + tmpDir := t.TempDir() + outputDir := t.TempDir() + overridePath := filepath.Join(tmpDir, "fake.bpf.o") + iorBin := writeScript(t, tmpDir, "ior", `printf '%s' "$IOR_BPF_OBJECT" > "$PWD/override.txt"`) + + h := TestHarness{ + IorBinary: iorBin, + BpfObject: overridePath, + OutputDir: outputDir, + } + + cmd, err := h.startIor(1234, "test", 5, nil) + if err != nil { + t.Fatalf("startIor returned error: %v", err) + } + if err := cmd.Wait(); err != nil { + t.Fatalf("wait for fake ior: %v", err) + } + + data, err := os.ReadFile(filepath.Join(outputDir, "override.txt")) + if err != nil { + t.Fatalf("read override marker: %v", err) + } + if got, want := string(data), overridePath; got != want { + t.Fatalf("IOR_BPF_OBJECT = %q, want %q", got, want) + } +} diff --git a/integrationtests/helpers_test.go b/integrationtests/helpers_test.go index feb7a55..6ef7ba7 100644 --- a/integrationtests/helpers_test.go +++ b/integrationtests/helpers_test.go @@ -9,7 +9,6 @@ import ( const ( iorBinaryDefault = "../ior" workloadBinaryDefault = "../ioworkload" - bpfObjectDefault = "../ior.bpf.o" defaultDuration = 10 parallelEnvVar = "IOR_INTEGRATION_PARALLEL" ) @@ -23,7 +22,6 @@ func newTestHarness(t *testing.T) TestHarness { return TestHarness{ IorBinary: absPath(t, iorBinaryDefault), WorkloadBinary: absPath(t, workloadBinaryDefault), - BpfObject: absPath(t, bpfObjectDefault), OutputDir: t.TempDir(), } } diff --git a/internal/bpfembed.go b/internal/bpfembed.go new file mode 100644 index 0000000..fce784c --- /dev/null +++ b/internal/bpfembed.go @@ -0,0 +1,31 @@ +package internal + +import ( + _ "embed" + "os" + + bpf "github.com/aquasecurity/libbpfgo" +) + +const ( + bpfObjectOverrideEnv = "IOR_BPF_OBJECT" + embeddedBPFObjectName = "ior.bpf.o" +) + +//go:embed c/ior.bpf.o +var embeddedBPFObject []byte + +var ( + newBPFModuleFromFile = bpf.NewModuleFromFile + newBPFModuleFromBuffer = bpf.NewModuleFromBuffer +) + +func loadBPFModule() (*bpf.Module, string, error) { + if path := os.Getenv(bpfObjectOverrideEnv); path != "" { + module, err := newBPFModuleFromFile(path) + return module, "load module from override file", err + } + + module, err := newBPFModuleFromBuffer(embeddedBPFObject, embeddedBPFObjectName) + return module, "load embedded module", err +} diff --git a/internal/ior.go b/internal/ior.go index c14ab37..3f145a9 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -342,9 +342,9 @@ func setupBPFModuleError(stage string, err error) error { func setupBPFModule(parentCtx context.Context, cfg flags.Config) (*bpf.Module, *probemanager.Manager, func(), error) { releaseBindings := func() {} - bpfModule, err := bpf.NewModuleFromFile("ior.bpf.o") + bpfModule, stage, err := loadBPFModule() if err != nil { - return nil, nil, releaseBindings, setupBPFModuleError("load module from file", err) + return nil, nil, releaseBindings, setupBPFModuleError(stage, err) } if err := resizeBPFMaps(cfg, bpfModule); err != nil { bpfModule.Close() diff --git a/internal/ior_setup_test.go b/internal/ior_setup_test.go index 9c8b1b3..9c804c7 100644 --- a/internal/ior_setup_test.go +++ b/internal/ior_setup_test.go @@ -1,8 +1,12 @@ package internal import ( + "bytes" "errors" + "os" "testing" + + bpf "github.com/aquasecurity/libbpfgo" ) func TestSetupBPFModuleErrorWrapsStage(t *testing.T) { @@ -25,3 +29,96 @@ func TestSetupBPFModuleErrorNil(t *testing.T) { t.Fatalf("expected nil error passthrough, got %v", err) } } + +func TestLoadBPFModuleUsesEmbeddedObjectByDefault(t *testing.T) { + origFile := newBPFModuleFromFile + origBuffer := newBPFModuleFromBuffer + origOverride, hadOverride := os.LookupEnv(bpfObjectOverrideEnv) + t.Cleanup(func() { + newBPFModuleFromFile = origFile + newBPFModuleFromBuffer = origBuffer + if hadOverride { + os.Setenv(bpfObjectOverrideEnv, origOverride) + return + } + os.Unsetenv(bpfObjectOverrideEnv) + }) + os.Unsetenv(bpfObjectOverrideEnv) + + wantErr := errors.New("buffer load failed") + newBPFModuleFromFile = func(string) (*bpf.Module, error) { + t.Fatal("expected embedded loader, not file loader") + return nil, nil + } + + var gotBytes []byte + var gotName string + newBPFModuleFromBuffer = func(data []byte, name string) (*bpf.Module, error) { + gotBytes = append([]byte(nil), data...) + gotName = name + return nil, wantErr + } + + module, stage, err := loadBPFModule() + if module != nil { + t.Fatalf("expected nil module from stubbed loader, got %v", module) + } + if got, want := stage, "load embedded module"; got != want { + t.Fatalf("stage = %q, want %q", got, want) + } + if !errors.Is(err, wantErr) { + t.Fatalf("expected embedded loader error, got %v", err) + } + if !bytes.Equal(gotBytes, embeddedBPFObject) { + t.Fatalf("embedded loader received unexpected object bytes") + } + if got, want := gotName, embeddedBPFObjectName; got != want { + t.Fatalf("embedded loader name = %q, want %q", got, want) + } +} + +func TestLoadBPFModuleUsesOverridePathWhenConfigured(t *testing.T) { + origFile := newBPFModuleFromFile + origBuffer := newBPFModuleFromBuffer + origOverride, hadOverride := os.LookupEnv(bpfObjectOverrideEnv) + t.Cleanup(func() { + newBPFModuleFromFile = origFile + newBPFModuleFromBuffer = origBuffer + if hadOverride { + os.Setenv(bpfObjectOverrideEnv, origOverride) + return + } + os.Unsetenv(bpfObjectOverrideEnv) + }) + + overridePath := "/tmp/custom-ior.bpf.o" + if err := os.Setenv(bpfObjectOverrideEnv, overridePath); err != nil { + t.Fatalf("set override env: %v", err) + } + + wantErr := errors.New("file load failed") + newBPFModuleFromBuffer = func([]byte, string) (*bpf.Module, error) { + t.Fatal("expected file loader, not embedded loader") + return nil, nil + } + + var gotPath string + newBPFModuleFromFile = func(path string) (*bpf.Module, error) { + gotPath = path + return nil, wantErr + } + + module, stage, err := loadBPFModule() + if module != nil { + t.Fatalf("expected nil module from stubbed loader, got %v", module) + } + if got, want := stage, "load module from override file"; got != want { + t.Fatalf("stage = %q, want %q", got, want) + } + if !errors.Is(err, wantErr) { + t.Fatalf("expected override loader error, got %v", err) + } + if got, want := gotPath, overridePath; got != want { + t.Fatalf("override path = %q, want %q", got, want) + } +} |
