From 6e5fbf3d95b98e190b670f1e40ca689789018158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Mon, 11 May 2026 14:05:21 +0200 Subject: [PATCH 1/2] fix: prevent parallel Ginkgo processes from racing on func CLI download When e2e tests run with ginkgo -p, multiple OS processes can simultaneously detect that the func CLI binary is missing and attempt to download it concurrently. This causes flaky failures from corrupted binaries or missing temp files. Fix by using syscall.Flock for cross-process file locking with double-checked locking, and writing to a temp file with atomic rename so partially-written binaries are never visible. --- test/utils/func.go | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/test/utils/func.go b/test/utils/func.go index 0c07253..48f036f 100644 --- a/test/utils/func.go +++ b/test/utils/func.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "syscall" "time" "github.com/functions-dev/func-operator/internal/funccli" @@ -176,7 +177,8 @@ func WithEnvVars(envVars map[string]string) FuncDeployOption { } } -// ensureFuncVersion ensures the specified func version is available and returns its path +// ensureFuncVersion ensures the specified func version is available and returns its path. +// Uses file locking to prevent parallel Ginkgo processes from racing on the download. func ensureFuncVersion(version string) (string, error) { projectDir, err := GetProjectDir() if err != nil { @@ -186,7 +188,30 @@ func ensureFuncVersion(version string) (string, error) { versionDir := filepath.Join(projectDir, "bin", "func-cli", version) funcBinary := filepath.Join(versionDir, "func") - // Check if already cached + // Fast path: binary already exists, no lock needed + if _, err := os.Stat(funcBinary); err == nil { + return funcBinary, nil + } + + // Ensure the directory exists before creating the lock file + if err := os.MkdirAll(versionDir, 0755); err != nil { + return "", fmt.Errorf("failed to create version directory: %w", err) + } + + // Acquire an exclusive file lock so only one Ginkgo process downloads at a time. + // Ginkgo's -p flag runs specs in separate OS processes, so sync.Mutex doesn't work. + lockFile, err := os.OpenFile(filepath.Join(versionDir, ".lock"), os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return "", fmt.Errorf("failed to create lock file: %w", err) + } + defer lockFile.Close() + + if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX); err != nil { + return "", fmt.Errorf("failed to acquire file lock: %w", err) + } + defer syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN) //nolint:errcheck + + // Re-check after acquiring the lock — another process may have finished the download if _, err := os.Stat(funcBinary); err == nil { return funcBinary, nil } @@ -199,12 +224,10 @@ func ensureFuncVersion(version string) (string, error) { return funcBinary, nil } -// downloadFuncVersion downloads the specified func version from GitHub releases +// downloadFuncVersion downloads the specified func version from GitHub releases. +// It writes to a temporary file first and atomically renames it to avoid exposing +// a partially-written binary to other processes. func downloadFuncVersion(version, versionDir, funcBinary string) error { - if err := os.MkdirAll(versionDir, 0o755); err != nil { - return fmt.Errorf("failed to create version directory: %w", err) - } - asset := funccli.AssetName() base := "https://github.com/knative/func/releases/download/knative-" + version binaryURL := base + "/" + asset From f77660f8851c5881fca7a3445e45f200b134ac4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20St=C3=A4bler?= Date: Mon, 11 May 2026 14:34:56 +0200 Subject: [PATCH 2/2] fix: remove unused versionDir parameter from downloadFuncVersion --- test/utils/func.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/func.go b/test/utils/func.go index 48f036f..9d988a2 100644 --- a/test/utils/func.go +++ b/test/utils/func.go @@ -217,7 +217,7 @@ func ensureFuncVersion(version string) (string, error) { } // Download the version - if err := downloadFuncVersion(version, versionDir, funcBinary); err != nil { + if err := downloadFuncVersion(version, funcBinary); err != nil { return "", err } @@ -227,7 +227,7 @@ func ensureFuncVersion(version string) (string, error) { // downloadFuncVersion downloads the specified func version from GitHub releases. // It writes to a temporary file first and atomically renames it to avoid exposing // a partially-written binary to other processes. -func downloadFuncVersion(version, versionDir, funcBinary string) error { +func downloadFuncVersion(version, funcBinary string) error { asset := funccli.AssetName() base := "https://github.com/knative/func/releases/download/knative-" + version binaryURL := base + "/" + asset