Skip to content
Merged
2 changes: 1 addition & 1 deletion pkg/cmd/openshift-tests/images/images_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func createImageMirrorForInternalImages(prefix string, ref reference.DockerImage
// Extract all test binaries
extractionContext, extractionContextCancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer extractionContextCancel()
cleanUpFn, externalBinaries, _, err := extensions.ExtractAllTestBinaries(extractionContext, 10)
cleanUpFn, externalBinaries, _, err := extensions.ExtractAllTestBinaries(extractionContext, 10, nil, false)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/openshift-tests/list/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func NewListExtensionsCommand(streams genericclioptions.IOStreams) *cobra.Comman
}

// Extract all test binaries from the release payload
cleanup, binaries, _, err := extensions.ExtractAllTestBinaries(ctx, 10)
cleanup, binaries, _, err := extensions.ExtractAllTestBinaries(ctx, 10, nil, false)
if err != nil {
return fmt.Errorf("failed to extract test binaries: %w", err)
}
Expand Down
132 changes: 130 additions & 2 deletions pkg/test/extensions/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,10 @@ func (b *TestBinary) ListImages(ctx context.Context) (ImageSet, error) {
// ExtractAllTestBinaries determines the optimal release payload to use, and extracts all the external
// test binaries from it (payload + permitted non-payload), and returns cleanup, binaries, and any
// unpermitted non-payload extensions for synthetic skip tests.
func ExtractAllTestBinaries(ctx context.Context, parallelism int) (func(), TestBinaries, []UnpermittedExtension, error) {
//
// localBinaryPaths is a list of extension binaries on the local filesystem to load directly.
// If localOnly is true, only local binaries are loaded (payload extraction is skipped).
func ExtractAllTestBinaries(ctx context.Context, parallelism int, localBinaryPaths []string, localOnly bool) (func(), TestBinaries, []UnpermittedExtension, error) {
if len(os.Getenv("OPENSHIFT_SKIP_EXTERNAL_TESTS")) > 0 {
logrus.Warning("Using built-in tests only due to OPENSHIFT_SKIP_EXTERNAL_TESTS being set")
var internalBinaries []*TestBinary
Expand All @@ -646,17 +649,128 @@ func ExtractAllTestBinaries(ctx context.Context, parallelism int) (func(), TestB
return nil, nil, nil, errors.New("parallelism must be greater than zero")
}

// Load local extension binaries if provided (before payload extraction)
var localBinaries []*TestBinary
var localTempFiles []string // Track temp files for cleanup
cleanupLocalFiles := func() {
for _, tempFile := range localTempFiles {
if err := os.Remove(tempFile); err != nil {
logrus.Warnf("Failed to remove local temp file %s: %v", tempFile, err)
}
}
}

if len(localBinaryPaths) > 0 {
hasValidPath := false

for _, path := range localBinaryPaths {
path = strings.TrimSpace(path)
if path == "" {
continue
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
hasValidPath = true

logrus.Infof("Loading local extension binary from %s", path)

// Check if file exists and is a regular file
info, err := os.Stat(path)
Comment thread
amiskin94 marked this conversation as resolved.
if err != nil {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("local extension binary not found: %s: %w", path, err)
}
if !info.Mode().IsRegular() {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("local extension binary is not a regular file: %s (mode: %v)", path, info.Mode())
}

// Create temp file for the binary (preserves source file for both .gz and non-.gz)
tempFile, err := os.CreateTemp("", "local-ext-*.bin")
if err != nil {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("failed to create temp file for %s: %w", path, err)
}
tempPath := tempFile.Name()
if err := tempFile.Close(); err != nil {
cleanupLocalFiles()
if removeErr := os.Remove(tempPath); removeErr != nil {
return nil, nil, nil, fmt.Errorf("failed to close temp file %s: %w (also failed to remove: %v)", tempPath, err, removeErr)
}
return nil, nil, nil, fmt.Errorf("failed to close temp file %s: %w", tempPath, err)
}

// Prepare the binary in temp (decompress .gz or copy non-.gz)
if strings.HasSuffix(path, ".gz") {
// Decompress to temp path
if err := decompressGzipToFile(path, tempPath); err != nil {
cleanupLocalFiles()
if removeErr := os.Remove(tempPath); removeErr != nil {
logrus.Warnf("Failed to remove temp file %s after decompression error: %v", tempPath, removeErr)
}
return nil, nil, nil, fmt.Errorf("failed to decompress %s: %w", path, err)
}
} else {
// Copy non-.gz file to temp (preserves source file permissions)
if err := copyFile(path, tempPath); err != nil {
cleanupLocalFiles()
if removeErr := os.Remove(tempPath); removeErr != nil {
logrus.Warnf("Failed to remove temp file %s after copy error: %v", tempPath, removeErr)
}
return nil, nil, nil, fmt.Errorf("failed to copy %s to temp: %w", path, err)
}
}

// Make executable (on temp copy, not source)
if err := os.Chmod(tempPath, 0755); err != nil {
cleanupLocalFiles()
if removeErr := os.Remove(tempPath); removeErr != nil {
logrus.Warnf("Failed to remove temp file %s after chmod error: %v", tempPath, removeErr)
}
return nil, nil, nil, fmt.Errorf("failed making temp binary %s executable: %w", tempPath, err)
}

localTempFiles = append(localTempFiles, tempPath)

// Create TestBinary for temp path
tb := &TestBinary{
imageTag: "local",
binaryPath: tempPath,
}

localBinaries = append(localBinaries, tb)
}

// Validate non-empty input produced at least one valid path
if !hasValidPath {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("--extension-binaries specified but no valid paths found (input was %v)", localBinaryPaths)
}

logrus.Infof("Loaded %d local extension binaries", len(localBinaries))
}

// Check for local-only mode (skip payload extraction)
if localOnly {
if len(localBinaries) == 0 {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("--extension-binaries-only set but no local binaries loaded")
}
logrus.Info("Local-only mode: skipping payload extraction (--extension-binaries-only set)")
return cleanupLocalFiles, localBinaries, nil, nil
}

// Filter extension binaries based on environment variables
filteredBinaries := filterExtensionBinariesByTags(extensionBinaries)
filteredBinaries = filterExtensionBinariesByArchitecture(filteredBinaries, extensionBinaryArchitecture(runtime.GOARCH))

releaseImage, err := DetermineReleasePayloadImage()
if err != nil {
cleanupLocalFiles()
return nil, nil, nil, errors.WithMessage(err, "couldn't determine release image")
}

tmpDir, err := os.MkdirTemp("", "external-binary")
if err != nil {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("failed to create temporary directory: %w", err)
}

Expand All @@ -665,11 +779,13 @@ func ExtractAllTestBinaries(ctx context.Context, parallelism int) (func(), TestB
oc := exutil.NewCLIWithoutNamespace("default")
registryAuthFilePath, err := DetermineRegistryAuthFilePath(tmpDir, oc)
if err != nil {
cleanupLocalFiles()
return nil, nil, nil, fmt.Errorf("failed to determine registry auth file path: %w", err)
}

externalBinaryProvider, err := NewExternalBinaryProvider(releaseImage, registryAuthFilePath)
if err != nil {
cleanupLocalFiles()
return nil, nil, nil, errors.WithMessage(err, "could not create external binary provider")
}

Expand Down Expand Up @@ -749,6 +865,7 @@ func ExtractAllTestBinaries(ctx context.Context, parallelism int) (func(), TestB
errs = append(errs, err.Error())
}
if len(errs) > 0 {
cleanupLocalFiles()
externalBinaryProvider.Cleanup()
return nil, nil, nil, fmt.Errorf("encountered errors while extracting binaries: %s", strings.Join(errs, ";"))
}
Expand All @@ -762,7 +879,18 @@ func ExtractAllTestBinaries(ctx context.Context, parallelism int) (func(), TestB
binaries = append(binaries, tb)
}

return externalBinaryProvider.Cleanup, binaries, unpermittedNonPayload, nil
// Append local binaries to payload binaries (additive mode)
binaries = append(binaries, localBinaries...)

// Combine cleanup functions (local temp files + payload cleanup)
combinedCleanup := func() {
cleanupLocalFiles()
if externalBinaryProvider != nil {
externalBinaryProvider.Cleanup()
}
}

return combinedCleanup, binaries, unpermittedNonPayload, nil
}

type TestBinaries []*TestBinary
Expand Down
68 changes: 68 additions & 0 deletions pkg/test/extensions/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,76 @@ func Time(t *dbtime.DBTime) time.Time {
return time.Time(*t)
}

// decompressGzipToFile decompresses a gzip file from src to dst, keeping src intact.
// This is used for local extension binaries where we must preserve the source file.
func decompressGzipToFile(src, dst string) (err error) {
gzFile, openErr := os.Open(src)
if openErr != nil {
return fmt.Errorf("failed to open gzip file: %w", openErr)
}
defer func() {
if closeErr := gzFile.Close(); err == nil && closeErr != nil {
err = fmt.Errorf("failed to close source file: %w", closeErr)
}
}()

gzipReader, readerErr := gzip.NewReader(gzFile)
if readerErr != nil {
return fmt.Errorf("failed to create gzip reader: %w", readerErr)
}
defer func() {
if closeErr := gzipReader.Close(); err == nil && closeErr != nil {
err = fmt.Errorf("failed to close gzip reader: %w", closeErr)
}
}()

outFile, createErr := os.Create(dst)
if createErr != nil {
return fmt.Errorf("failed to create output file: %w", createErr)
}
defer func() {
if closeErr := outFile.Close(); err == nil && closeErr != nil {
err = fmt.Errorf("failed to close output file: %w", closeErr)
}
}()

if _, copyErr := io.Copy(outFile, gzipReader); copyErr != nil {
return fmt.Errorf("failed to decompress: %w", copyErr)
}

return nil
}

// copyFile copies a file from src to dst, preserving the content but not permissions.
// Used for local extension binaries to create a managed temporary copy.
func copyFile(src, dst string) (err error) {
srcFile, openErr := os.Open(src)
if openErr != nil {
return fmt.Errorf("failed to open source file: %w", openErr)
}
defer srcFile.Close()

dstFile, createErr := os.Create(dst)
if createErr != nil {
return fmt.Errorf("failed to create destination file: %w", createErr)
}
defer func() {
if closeErr := dstFile.Close(); err == nil && closeErr != nil {
err = fmt.Errorf("failed to close destination file: %w", closeErr)
}
}()

if _, copyErr := io.Copy(dstFile, srcFile); copyErr != nil {
return fmt.Errorf("failed to copy file: %w", copyErr)
}

return nil
}

// ungzipFile checks if a binary is gzipped (ends with .gz) and decompresses it.
// Returns the new filename of the decompressed file (original is deleted), or original filename if it was not gzipped.
// Note: This function deletes the source file and should NOT be used for user-provided local binaries.
// Use decompressGzipToFile for local binaries to preserve the source.
func ungzipFile(extractedBinary string) (string, error) {

if strings.HasSuffix(extractedBinary, ".gz") {
Expand Down
18 changes: 17 additions & 1 deletion pkg/test/ginkgo/cmd_runsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ type GinkgoRunSuiteOptions struct {

// WithHypervisorConfigJSON contains JSON configuration for hypervisor-based recovery operations
WithHypervisorConfigJSON string

// LocalExtensionBinaries contains paths to extension binaries on the local filesystem.
// These are loaded directly without extraction from payload or non-payload images.
LocalExtensionBinaries []string

// LocalExtensionBinariesOnly when true skips payload extraction and uses only local binaries.
LocalExtensionBinariesOnly bool
}

func NewGinkgoRunSuiteOptions(streams genericclioptions.IOStreams) *GinkgoRunSuiteOptions {
Expand All @@ -120,6 +127,13 @@ func (o *GinkgoRunSuiteOptions) BindFlags(flags *pflag.FlagSet) {

monitorNames := defaultmonitortests.ListAllMonitorTests()

// Parse EXTENSION_LOCAL_BINARIES env var as colon-separated paths for backward compatibility
envLocalBinaries := os.Getenv("EXTENSION_LOCAL_BINARIES")
var defaultLocalBinaries []string
if envLocalBinaries != "" {
defaultLocalBinaries = strings.Split(envLocalBinaries, ":")
}

flags.BoolVar(&o.DryRun, "dry-run", o.DryRun, "Print the tests to run without executing them.")
flags.BoolVar(&o.PrintCommands, "print-commands", o.PrintCommands, "Print the sub-commands that would be executed instead.")
flags.StringVar(&o.ClusterStabilityDuringTest, "cluster-stability", o.ClusterStabilityDuringTest, "cluster stability during test, usually dependent on the job: Stable or Disruptive. Empty default will be treated as Stable.")
Expand All @@ -139,6 +153,8 @@ func (o *GinkgoRunSuiteOptions) BindFlags(flags *pflag.FlagSet) {
availableStrategies := getAvailableRetryStrategies()
flags.Var(newRetryStrategyFlag(&o.RetryStrategy), "retry-strategy", fmt.Sprintf("Test retry strategy (available: %s, default: %s)", strings.Join(availableStrategies, ", "), defaultRetryStrategy))
flags.StringVar(&o.WithHypervisorConfigJSON, "with-hypervisor-json", os.Getenv("HYPERVISOR_CONFIG"), "JSON configuration for hypervisor-based recovery operations. Must contain hypervisorIP, sshUser, and privateKeyPath fields.")
flags.StringSliceVar(&o.LocalExtensionBinaries, "extension-local-binaries", defaultLocalBinaries, "Paths to local extension binaries. These are loaded directly without payload extraction.")
flags.BoolVar(&o.LocalExtensionBinariesOnly, "extension-local-binaries-only", os.Getenv("EXTENSION_LOCAL_BINARIES_ONLY") != "", "Skip payload extraction and use only local extension binaries.")
}

func (o *GinkgoRunSuiteOptions) Validate() error {
Expand Down Expand Up @@ -315,7 +331,7 @@ func (o *GinkgoRunSuiteOptions) Run(suite *TestSuite, clusterConfig *clusterdisc
// Extract all test binaries
extractionContext, extractionContextCancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer extractionContextCancel()
cleanUpFn, allBinaries, unpermitted, err := extensions.ExtractAllTestBinaries(extractionContext, defaultBinaryParallelism)
cleanUpFn, allBinaries, unpermitted, err := extensions.ExtractAllTestBinaries(extractionContext, defaultBinaryParallelism, o.LocalExtensionBinaries, o.LocalExtensionBinariesOnly)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/testsuites/standard_suites.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func AllTestSuites(ctx context.Context) ([]*ginkgo.TestSuite, error) {
}

// Extract all test binaries from the release payload
cleanup, binaries, _, err := extensions.ExtractAllTestBinaries(ctx, 10)
cleanup, binaries, _, err := extensions.ExtractAllTestBinaries(ctx, 10, nil, false)
if err != nil {
return nil, fmt.Errorf("failed to extract test binaries: %w", err)
}
Expand Down