From e8309916cec050bee03cb66f8255590b7e07c040 Mon Sep 17 00:00:00 2001 From: Luther Monson Date: Tue, 14 Jul 2026 11:26:42 -0700 Subject: [PATCH] fix(cni): select embedded CNI plugins by arch (arm64 macOS dispatch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Apple Silicon, ephemerd boots an arm64 Linux VM to run Linux container jobs. The macOS build cross-compiles ephemerd-linux for arm64 and downloads the arm64 CNI plugins, but the amd64 tarball also lingers in pkg/cni/embed/ (CI cache / Dockerfile). `//go:embed all:embed` bundles both, and findTarball() returned the first entry — amd64. Every container's CNI setup then failed inside the arm64 VM with: plugin type="loopback" ... exec format error so no Linux job could run on a Mac runner. Select the archive by runtime.GOARCH (the compiled arch of the in-VM ephemerd-linux) instead of grabbing whichever tarball sorts first. The runners already stash non-matching arches for this reason; CNI now does the equivalent at extract time. Pure selection logic lives in select.go with a table test; a clear error lists what's embedded when no arch matches. --- pkg/cni/cni_linux.go | 22 ++++++++++++++-------- pkg/cni/select.go | 30 ++++++++++++++++++++++++++++++ pkg/cni/select_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 pkg/cni/select.go create mode 100644 pkg/cni/select_test.go diff --git a/pkg/cni/cni_linux.go b/pkg/cni/cni_linux.go index da64849c..08175122 100644 --- a/pkg/cni/cni_linux.go +++ b/pkg/cni/cni_linux.go @@ -11,6 +11,7 @@ import ( "log/slog" "os" "path/filepath" + "runtime" ) //go:embed all:embed @@ -79,21 +80,26 @@ func (m *Manager) Extract() error { return nil } +// findTarball selects the CNI plugins archive matching this binary's arch. +// The macOS build cross-compiles ephemerd-linux for arm64, but the embed dir +// can hold BOTH cni-plugins-linux-amd64 and -arm64 (the amd64 one lingers +// from the CI cache). `//go:embed all:embed` then bundles both, so a naive +// "first entry" pick returns amd64 and the arm64 Linux VM fails every CNI +// exec with "exec format error". Select by runtime.GOARCH instead. func (m *Manager) findTarball() (string, error) { entries, err := cniFS.ReadDir("embed") if err != nil { return "", fmt.Errorf("reading embedded files: %w", err) } - + names := make([]string, 0, len(entries)) for _, e := range entries { - name := e.Name() - if name == ".gitkeep" { - continue - } - return filepath.Join("embed", name), nil + names = append(names, e.Name()) } - - return "", fmt.Errorf("no CNI plugins archive found in embedded files (did you run 'make download-cni'?)") + name, err := selectCNITarball(names, runtime.GOARCH) + if err != nil { + return "", err + } + return filepath.Join("embed", name), nil } func extractTarGz(r io.Reader, dest string) error { diff --git a/pkg/cni/select.go b/pkg/cni/select.go new file mode 100644 index 00000000..ccd452ca --- /dev/null +++ b/pkg/cni/select.go @@ -0,0 +1,30 @@ +package cni + +import ( + "fmt" + "strings" +) + +// selectCNITarball picks the CNI plugins archive matching goarch from the +// embed listing. The macOS build cross-compiles ephemerd-linux for arm64 but +// the embed dir can hold BOTH cni-plugins-linux-amd64 and -arm64 (the amd64 +// one lingers from the CI cache). `//go:embed all:embed` then bundles both, +// so a naive "first entry" pick returns amd64 and the arm64 Linux VM fails +// every CNI exec with "exec format error". Select by arch instead. +func selectCNITarball(names []string, goarch string) (string, error) { + want := "cni-plugins-linux-" + goarch + "-" + var found []string + for _, name := range names { + if name == ".gitkeep" { + continue + } + found = append(found, name) + if strings.HasPrefix(name, want) { + return name, nil + } + } + if len(found) == 0 { + return "", fmt.Errorf("no CNI plugins archive found in embedded files (did you run the download targets?)") + } + return "", fmt.Errorf("no CNI plugins archive for linux/%s in embedded files (have: %s)", goarch, strings.Join(found, ", ")) +} diff --git a/pkg/cni/select_test.go b/pkg/cni/select_test.go new file mode 100644 index 00000000..9be86be6 --- /dev/null +++ b/pkg/cni/select_test.go @@ -0,0 +1,39 @@ +package cni + +import "testing" + +func TestSelectCNITarball(t *testing.T) { + both := []string{"cni-plugins-linux-amd64-v1.6.2.tgz", "cni-plugins-linux-arm64-v1.6.2.tgz"} + + tests := []struct { + name string + names []string + goarch string + want string + wantErr bool + }{ + {"arm64 picks arm64 from both", both, "arm64", "cni-plugins-linux-arm64-v1.6.2.tgz", false}, + {"amd64 picks amd64 from both", both, "amd64", "cni-plugins-linux-amd64-v1.6.2.tgz", false}, + {"amd64 alone", both[:1], "amd64", "cni-plugins-linux-amd64-v1.6.2.tgz", false}, + {"skips gitkeep", []string{".gitkeep", "cni-plugins-linux-arm64-v1.6.2.tgz"}, "arm64", "cni-plugins-linux-arm64-v1.6.2.tgz", false}, + {"no match errors", both[:1], "arm64", "", true}, + {"empty errors", []string{".gitkeep"}, "arm64", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := selectCNITarball(tt.names, tt.goarch) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got %q", got) + } + return + } + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +}