From eb755c3932f49bc1b119def426414514480603a2 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 7 Aug 2026 14:50:28 -0400 Subject: [PATCH] build(ebpf): stop committing bpf2go-compiled artifacts internal/plumbing/ebpf/prog's usid_bpfel.o/usid_bpfeb.o (and their generated .go bindings) were committed to git and go:embed'd into the binaries. Unlike other generated code (*.pb.go), these embed a compiled binary blob, so a stale commit could silently drift out of sync with usid.c with nothing but a byte-diff to catch it. Gitignore the four generated files and require every build site to regenerate them from usid.c via bpf2go instead: - Taskfile.yaml: build:ebpf now unconditionally regenerates (clang required, via a new require-clang check) instead of falling back to a committed copy when clang is missing. Reordered `build` so build:ebpf runs before fmt/vet, which need the embed target to compile. Added build:ebpf as a dependency of lint, lint-fix, test:unit, test:unit-root, and test:e2e. - ci.yaml: install the pinned clang-18/llvm-18/linux-libc-dev toolchain in the lint, test-unit, test-unit-root, and test-e2e jobs (previously only the build job had it). Removed the build job's `git diff --exit-code` drift check -- there's nothing committed to diff against anymore. - containers/galactic-cni/Dockerfile and containers/galactic-router/Dockerfile (the latter transitively imports internal/plumbing/ebpf/prog via usidmap): install a clang toolchain and run `go generate` in the builder stage instead of relying on a committed .o. - doc.go: documented the new convention and why it differs from the repo's committed-generated-code norm. Trade-off: clang is now a hard build requirement everywhere (dev machines, every CI job, both Dockerfiles) -- there's no more build-against-the-stale-committed-copy fallback when it's missing. Verified: task build/lint/test:unit pass from a clean regeneration; both Dockerfiles build successfully with the new toolchain step. --- .github/workflows/ci.yaml | 79 +++++++--- .gitignore | 10 ++ Taskfile.yaml | 46 ++++-- containers/galactic-cni/Dockerfile | 23 +-- containers/galactic-router/Dockerfile | 9 ++ internal/plumbing/ebpf/prog/doc.go | 27 ++-- internal/plumbing/ebpf/prog/usid_bpfeb.go | 175 ---------------------- internal/plumbing/ebpf/prog/usid_bpfeb.o | Bin 16264 -> 0 bytes internal/plumbing/ebpf/prog/usid_bpfel.go | 175 ---------------------- internal/plumbing/ebpf/prog/usid_bpfel.o | Bin 16264 -> 0 bytes 10 files changed, 138 insertions(+), 406 deletions(-) delete mode 100644 internal/plumbing/ebpf/prog/usid_bpfeb.go delete mode 100644 internal/plumbing/ebpf/prog/usid_bpfeb.o delete mode 100644 internal/plumbing/ebpf/prog/usid_bpfel.go delete mode 100644 internal/plumbing/ebpf/prog/usid_bpfel.o diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3bf7d120..b1b63f87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,14 @@ on: env: GO_VERSION: '1.26' + # Pins every job's regeneration of internal/plumbing/ebpf/prog's bpf2go + # output (usid_bpfel/eb.go and their embedded .o's; not committed, see + # that package's doc.go) to the same explicit clang version, rather than + # each job/runner resolving its own unversioned "clang" off PATH. + # clang-18/llvm-18 is the noble/ubuntu-latest default at the time this + # was written -- reconfirm against the runner image if installation + # starts failing. + BPF2GO_CC: clang-18 jobs: # Tier 1: Fast checks (every PR) @@ -26,6 +34,14 @@ jobs: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install eBPF build dependencies + # `task lint` type-checks the whole module, which needs + # internal/plumbing/ebpf/prog's bpf2go output regenerated first + # (see build job's identical step below for the package pins). + run: | + sudo apt-get update + sudo apt-get install -y clang-18 llvm-18 linux-libc-dev + - name: Run lint run: task lint @@ -44,6 +60,14 @@ jobs: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install eBPF build dependencies + # `task test:unit` builds internal/plumbing/ebpf/prog, which needs + # its bpf2go output regenerated first (see build job's identical + # step below for the package pins). + run: | + sudo apt-get update + sudo apt-get install -y clang-18 llvm-18 linux-libc-dev + - name: Run unit tests run: task test:unit @@ -109,6 +133,18 @@ jobs: sudo apt-get install -y --no-install-recommends "linux-modules-extra-$(uname -r)" sudo modprobe vrf + - name: Install eBPF build dependencies + # `task test:unit-root` builds internal/plumbing/ebpf/prog, which + # needs its bpf2go output regenerated first (see build job's + # identical step below for the package pins). This runs as the + # unprivileged runner user -- it's the regeneration below, run as + # root via `sudo -E task test:unit-root`, that needs clang on + # root's PATH, which -E's preserved PATH (asserted explicitly via + # `env "PATH=$PATH"`) already covers. + run: | + sudo apt-get update + sudo apt-get install -y clang-18 llvm-18 linux-libc-dev + - name: Run root-gated unit tests as root # sudo's secure_path policy overrides -E's preserved PATH for # exactly the PATH variable, which would hide the go/task binaries @@ -134,13 +170,12 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install eBPF build dependencies - # Pinned to an explicit versioned package (clang-18/llvm-18, the - # noble/ubuntu-latest default at the time this was written -- - # reconfirm against the runner image if this starts failing) rather - # than the unversioned clang/llvm meta-packages: BPF object bytes - # (BTF, debug info, section layout) move with the compiler, so an - # unpinned install would let a future runner-image clang bump flip - # the drift check below red on PRs that never touched usid.c. + # Pinned to an explicit versioned package (clang-18/llvm-18) rather + # than the unversioned clang/llvm meta-packages, matching the + # workflow-level BPF2GO_CC pin above -- keeps the BPF object bytes + # (BTF, debug info, section layout) produced by this job identical + # to every other job's regeneration instead of drifting with + # whatever clang a future runner-image bump resolves to. # linux-libc-dev is listed explicitly too -- doc.go's -idirafter # workaround exists specifically to find its headers, so this job # shouldn't rely on it merely happening to be preinstalled. @@ -151,22 +186,11 @@ jobs: - name: Build binary # task build's build:ebpf step regenerates # internal/plumbing/ebpf/prog's bpf2go output (usid_bpfel/eb.go - # and their embedded .o's) from usid.c as a side effect, before - # build:binaries ever runs. The check below diffs the regenerated - # artifacts against the committed ones: the .o's `go test` and - # go.datum.net/galactic/internal/plumbing/ebpf/prog itself load are - # otherwise just checked-in blobs that can drift silently out of - # sync with usid.c, since nothing else forces them to be - # regenerated on a source change. BPF2GO_CC pins this regeneration - # to the exact clang installed above (see doc.go's go:generate - # directive, which omits -cc for exactly this reason) instead of - # whatever unversioned "clang" resolves to. + # and their embedded .o's) from usid.c before build:binaries ever + # runs -- these are gitignored (see that package's doc.go), so + # there's nothing checked in for this to drift against; it's + # produced fresh on every build, here exactly as everywhere else. run: task build - env: - BPF2GO_CC: clang-18 - - - name: Verify committed eBPF artifacts match usid.c - run: git diff --exit-code -- internal/plumbing/ebpf/prog # Tier 2: Integration tests (every PR, main branch, and releases) test-e2e: @@ -185,5 +209,16 @@ jobs: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install eBPF build dependencies + # `task test:e2e` compiles ./tests/e2e/... on the host (the + # galactic-cni image it builds separately regenerates its own copy + # inside containers/galactic-cni/Dockerfile's builder stage), which + # needs internal/plumbing/ebpf/prog's bpf2go output regenerated + # first -- see build job's identical step above for the package + # pins. + run: | + sudo apt-get update + sudo apt-get install -y clang-18 llvm-18 linux-libc-dev + - name: Run E2E tests run: task test:e2e diff --git a/.gitignore b/.gitignore index 6a3d079c..1bfc08ab 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,16 @@ go.work.sum ko-local/ *.img +# eBPF (bpf2go) generated artifacts -- regenerated from usid.c by `task +# build:ebpf` (requires clang; see internal/plumbing/ebpf/prog/doc.go). +# Unlike other generated code (e.g. *.pb.go), these embed a compiled +# binary blob and are deliberately NOT committed, so the bytes shipped +# never drift from usid.c without anyone noticing a stale commit. +internal/plumbing/ebpf/prog/usid_bpfel.go +internal/plumbing/ebpf/prog/usid_bpfel.o +internal/plumbing/ebpf/prog/usid_bpfeb.go +internal/plumbing/ebpf/prog/usid_bpfeb.o + # ============================================================ # Kubernetes / controller-runtime / kubebuilder # ============================================================ diff --git a/Taskfile.yaml b/Taskfile.yaml index f245f203..8f45d766 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -52,14 +52,14 @@ tasks: lint: desc: Lint - deps: [golangci-lint, yamlfmt, yaml-ext] + deps: [golangci-lint, yamlfmt, yaml-ext, build:ebpf] cmds: - '{{.GOLANGCI_LINT}} run' - '{{.YAMLFMT}} -lint' lint-fix: desc: Lint and apply fixes - deps: [golangci-lint, yamlfmt] + deps: [golangci-lint, yamlfmt, build:ebpf] cmds: - '{{.GOLANGCI_LINT}} run --fix' - '{{.YAMLFMT}}' @@ -76,30 +76,38 @@ tasks: build: desc: Build binaries - deps: [fmt, vet] cmds: + # build:ebpf must run before fmt/vet, not just before build:binaries: + # internal/plumbing/ebpf/prog's usid_bpfel.o/usid_bpfeb.o (go:embed'd) + # aren't committed (see .gitignore), so `go vet ./...` -- and anything + # else that type-checks the whole module -- fails to compile that + # package on a fresh checkout until this has generated them. - task: build:ebpf + - task: fmt + - task: vet - task: build:binaries build:ebpf: - desc: >- - Regenerate the eBPF uSID datapath (skips, using the committed - artifacts, if clang isn't installed) + desc: Regenerate the eBPF uSID datapath from usid.c (requires clang) + run: once + deps: [require-clang] + cmds: + - go generate ./internal/plumbing/ebpf/prog/... + + require-clang: + internal: true run: once cmds: - | - if command -v clang >/dev/null 2>&1; then - go generate ./internal/plumbing/ebpf/prog/... - else - echo "WARNING: clang not found; skipping regeneration of the eBPF" >&2 - echo "uSID datapath (internal/plumbing/ebpf/prog/usid.c, via bpf2go)" >&2 - echo "and building against the committed usid_bpfel.o/usid_bpfeb.o" >&2 - echo "as-is. If you edited usid.c, install clang and re-run 'task" >&2 - echo "build:ebpf' (or 'task build') before committing -- CI's build" >&2 - echo "job regenerates and diffs unconditionally, so a stale commit" >&2 - echo "will fail there even if it builds fine here. Install, e.g.:" >&2 + if ! command -v "${BPF2GO_CC:-clang}" >/dev/null 2>&1; then + echo "ERROR: ${BPF2GO_CC:-clang} not found on PATH." >&2 + echo "internal/plumbing/ebpf/prog/usid_bpfel.o/usid_bpfeb.o are" >&2 + echo "generated from usid.c via bpf2go, not committed to git (see" >&2 + echo "internal/plumbing/ebpf/prog/doc.go) -- clang is required to" >&2 + echo "build or test this module at all. Install, e.g.:" >&2 echo " Fedora/RHEL: sudo dnf install clang llvm" >&2 echo " Debian/Ubuntu: sudo apt install clang llvm" >&2 + exit 1 fi build:binaries: @@ -135,16 +143,22 @@ tasks: test:unit: desc: Unit tests with race detection and coverage + deps: [build:ebpf] cmds: - bash scripts/ci.sh unittest test:unit-root: desc: Re-run just the requireRoot(t)-gated unit tests, as root + deps: [build:ebpf] cmds: - bash scripts/ci.sh unittest-root test:e2e: desc: E2E tests (Kind cluster, build+load image, lifecycle) + # build:ebpf here is only for the host-side `go test ./tests/e2e/...` + # compile in scripts/ci.sh -- the galactic-cni image built inside + # ci.sh's docker build regenerates its own copy via the Dockerfile. + deps: [build:ebpf] cmds: - bash scripts/ci.sh e2etest diff --git a/containers/galactic-cni/Dockerfile b/containers/galactic-cni/Dockerfile index cb57846c..c949f093 100644 --- a/containers/galactic-cni/Dockerfile +++ b/containers/galactic-cni/Dockerfile @@ -22,15 +22,20 @@ RUN go mod download COPY cmd/ cmd/ COPY internal/ internal/ -# No eBPF toolchain needed here: internal/plumbing/ebpf/prog's committed -# usid_bpfel.o/usid_bpfeb.o (go:embed'd into the galactic-cni binary below) -# ship as-is from the COPY above, rather than being regenerated from -# usid.c by this build -- so the object embedded in the image is bit-for- -# bit the one committed to git, reviewed in the PR, and verified against -# usid.c by CI's drift check (.github/workflows/ci.yaml's `build` job), -# not a separate build produced by this builder image's own clang. design -# plan .local/plan-ebpf-xdp-usid-datapath.md ยง6; Milestone 5.2 of -# .local/implementation-plan-ebpf-xdp-usid-datapath.md. +# Install the eBPF build toolchain and regenerate +# internal/plumbing/ebpf/prog's bpf2go output (usid_bpfel.go/.o, +# usid_bpfeb.go/.o -- go:embed'd into the galactic-cni binary below) from +# usid.c. These aren't committed to git (see that package's doc.go), so +# every build site produces them fresh instead of embedding a copy +# checked into the repo; this builder stage is no exception. Package +# names are the generic Debian ones (this base image, unlike the CI +# runner's Ubuntu, doesn't have a clang-18 package available) -- byte- +# identical output across build sites isn't the goal here, a working BPF +# program compiled from usid.c is. +RUN apt-get update && apt-get install -y --no-install-recommends \ + clang llvm linux-libc-dev \ + && rm -rf /var/lib/apt/lists/* +RUN go generate ./internal/plumbing/ebpf/prog/... # Build CNI plugin RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build \ diff --git a/containers/galactic-router/Dockerfile b/containers/galactic-router/Dockerfile index 603e4bac..a6cff868 100644 --- a/containers/galactic-router/Dockerfile +++ b/containers/galactic-router/Dockerfile @@ -21,6 +21,15 @@ RUN go mod download COPY cmd/ cmd/ COPY internal/ internal/ +# galactic-router transitively imports internal/plumbing/ebpf/prog (via +# usidmap), whose bpf2go output (usid_bpfel.go/.o, usid_bpfeb.go/.o) isn't +# committed to git (see that package's doc.go) -- regenerate it from +# usid.c here, same as containers/galactic-cni/Dockerfile. +RUN apt-get update && apt-get install -y --no-install-recommends \ + clang llvm linux-libc-dev \ + && rm -rf /var/lib/apt/lists/* +RUN go generate ./internal/plumbing/ebpf/prog/... + # Build the router RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build \ -ldflags "-s -w \ diff --git a/internal/plumbing/ebpf/prog/doc.go b/internal/plumbing/ebpf/prog/doc.go index 05924b29..4e2a190f 100644 --- a/internal/plumbing/ebpf/prog/doc.go +++ b/internal/plumbing/ebpf/prog/doc.go @@ -13,12 +13,21 @@ // into a CO-RE-portable BPF object and generates matching Go bindings // (UsidObjects, LoadUsid, LoadUsidObjects, plus per-map/per-program // fields) in this package -- run `go generate ./...` from the repo root, -// or `go generate` from this directory, after editing usid.c. The -// generated *_bpfel.go/*_bpfel.o (and *_bpfeb.go/*_bpfeb.o) files are -// committed alongside the source they're generated from, matching this -// repo's convention for other generated code (see CLAUDE.md: "Generated -// protobuf files ... are committed; never hand-edit them" -- the same -// rule applies here to bpf2go's output). +// or `go generate` from this directory, after editing usid.c. +// +// Unlike this repo's other generated code (e.g. *.pb.go, committed +// per CLAUDE.md: "Generated protobuf files ... are committed; never +// hand-edit them"), the generated *_bpfel.go/*_bpfel.o and +// *_bpfeb.go/*_bpfeb.o files are gitignored, not committed: they embed a +// compiled binary blob rather than plain Go source, and committing a +// compiled artifact risks it silently drifting out of sync with usid.c +// with nothing to catch the mismatch short of a byte-diff. Instead, every +// build site regenerates them fresh via `task build:ebpf` -- a hard +// dependency of `task build`/`lint`/`test:unit`/`test:unit-root`/`test:e2e` +// (see Taskfile.yaml), and of containers/galactic-cni/Dockerfile and +// containers/galactic-router/Dockerfile's builder stages -- so clang must +// be on PATH (or $BPF2GO_CC) to build or test this module at all; there is +// no fallback to a checked-in copy. // // Placement: sibling of internal/plumbing/ebpf/uformat (Milestone 2.1) // under the shared internal/plumbing/ebpf/ umbrella -- uformat is the @@ -54,8 +63,8 @@ package prog // // -cc is deliberately omitted: bpf2go's own default is // getEnv("BPF2GO_CC", "clang"), so CI can pin an exact clang version by -// setting $BPF2GO_CC (see the `build` job's drift-check step) without -// this directive needing to change, while everyone else keeps getting -// plain "clang" off their PATH. +// setting $BPF2GO_CC (see .github/workflows/ci.yaml) without this +// directive needing to change, while everyone else keeps getting plain +// "clang" off their PATH. // //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cflags "-O2 -g -Wall -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include/aarch64-linux-gnu" -target bpfel,bpfeb -type locator_value -type function_value -type vrf_value Usid usid.c diff --git a/internal/plumbing/ebpf/prog/usid_bpfeb.go b/internal/plumbing/ebpf/prog/usid_bpfeb.go deleted file mode 100644 index 0a802cc7..00000000 --- a/internal/plumbing/ebpf/prog/usid_bpfeb.go +++ /dev/null @@ -1,175 +0,0 @@ -// Code generated by bpf2go; DO NOT EDIT. -//go:build mips || mips64 || ppc64 || s390x - -package prog - -import ( - "bytes" - _ "embed" - "fmt" - "io" - "structs" - - "github.com/cilium/ebpf" -) - -type UsidFunctionValue struct { - _ structs.HostLayout - Behavior uint32 -} - -type UsidLocatorValue struct { - _ structs.HostLayout - Generation uint64 -} - -type UsidVrfValue struct { - _ structs.HostLayout - VrfTableId uint32 - EgressKind uint32 - Packets uint64 - Bytes uint64 - LastSeenNs uint64 - Generation uint64 - DroppedPackets uint64 -} - -// Names of all BPF objects in the ELF. -// -// Used for safe lookups in a Collection or CollectionSpec. -const ( - UsidMapDropReasons = "drop_reasons" - UsidMapFunctionTable = "function_table" - UsidMapLocatorTable = "locator_table" - UsidMapVrfTable = "vrf_table" - UsidProgUsidIngress = "usid_ingress" -) - -// LoadUsid returns the embedded CollectionSpec for Usid. -func LoadUsid() (*ebpf.CollectionSpec, error) { - reader := bytes.NewReader(_UsidBytes) - spec, err := ebpf.LoadCollectionSpecFromReader(reader) - if err != nil { - return nil, fmt.Errorf("can't load Usid: %w", err) - } - - return spec, err -} - -// LoadUsidObjects loads Usid and converts it into a struct. -// -// The following types are suitable as obj argument: -// -// *UsidObjects -// *UsidPrograms -// *UsidMaps -// -// See ebpf.CollectionSpec.LoadAndAssign documentation for details. -func LoadUsidObjects(obj any, opts *ebpf.CollectionOptions) error { - spec, err := LoadUsid() - if err != nil { - return err - } - - return spec.LoadAndAssign(obj, opts) -} - -// UsidSpecs contains maps and programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type UsidSpecs struct { - UsidProgramSpecs - UsidMapSpecs - UsidVariableSpecs -} - -// UsidProgramSpecs contains programs before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type UsidProgramSpecs struct { - UsidIngress *ebpf.ProgramSpec `ebpf:"usid_ingress"` -} - -// UsidMapSpecs contains maps before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type UsidMapSpecs struct { - DropReasons *ebpf.MapSpec `ebpf:"drop_reasons"` - FunctionTable *ebpf.MapSpec `ebpf:"function_table"` - LocatorTable *ebpf.MapSpec `ebpf:"locator_table"` - VrfTable *ebpf.MapSpec `ebpf:"vrf_table"` -} - -// UsidVariableSpecs contains global variables before they are loaded into the kernel. -// -// It can be passed ebpf.CollectionSpec.Assign. -type UsidVariableSpecs struct { -} - -// UsidObjects contains all objects after they have been loaded into the kernel. -// -// It can be passed to LoadUsidObjects or ebpf.CollectionSpec.LoadAndAssign. -type UsidObjects struct { - UsidPrograms - UsidMaps - UsidVariables -} - -func (o *UsidObjects) Close() error { - return _UsidClose( - &o.UsidPrograms, - &o.UsidMaps, - ) -} - -// UsidMaps contains all maps after they have been loaded into the kernel. -// -// It can be passed to LoadUsidObjects or ebpf.CollectionSpec.LoadAndAssign. -type UsidMaps struct { - DropReasons *ebpf.Map `ebpf:"drop_reasons"` - FunctionTable *ebpf.Map `ebpf:"function_table"` - LocatorTable *ebpf.Map `ebpf:"locator_table"` - VrfTable *ebpf.Map `ebpf:"vrf_table"` -} - -func (m *UsidMaps) Close() error { - return _UsidClose( - m.DropReasons, - m.FunctionTable, - m.LocatorTable, - m.VrfTable, - ) -} - -// UsidVariables contains all global variables after they have been loaded into the kernel. -// -// It can be passed to LoadUsidObjects or ebpf.CollectionSpec.LoadAndAssign. -type UsidVariables struct { -} - -// UsidPrograms contains all programs after they have been loaded into the kernel. -// -// It can be passed to LoadUsidObjects or ebpf.CollectionSpec.LoadAndAssign. -type UsidPrograms struct { - UsidIngress *ebpf.Program `ebpf:"usid_ingress"` -} - -func (p *UsidPrograms) Close() error { - return _UsidClose( - p.UsidIngress, - ) -} - -func _UsidClose(closers ...io.Closer) error { - for _, closer := range closers { - if err := closer.Close(); err != nil { - return err - } - } - return nil -} - -// Do not access this directly. -// -//go:embed usid_bpfeb.o -var _UsidBytes []byte diff --git a/internal/plumbing/ebpf/prog/usid_bpfeb.o b/internal/plumbing/ebpf/prog/usid_bpfeb.o deleted file mode 100644 index 5a7186a5b8dd731b0f6e8bbc24af9a30a2bcd3f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16264 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 TYhOa~81w3=sgIV^TpdVPk2$a}qS)eH7Hb99s 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 l9#lDmY$@~|jl=@LSGk$}V{~ysNgY5tS