Skip to content

Commit 6a12774

Browse files
committed
External binary caching and code clean-up
1 parent 2ae2adf commit 6a12774

File tree

8 files changed

+891
-515
lines changed

8 files changed

+891
-515
lines changed

pkg/test/externalbinary/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# External Binaries
2+
3+
This package includes the code used for working with external test binaries.
4+
It's intended to house the implementation of the openshift-tests side of the
5+
[openshift-tests extension interface](https://github.com/openshift/enhancements/pull/1676), which is only
6+
partially implemented here for the moment.
7+
8+
There is a registry defined in binary.go, that lists the release image tag, and
9+
path to each external test binary. These binaries should implement the OTE
10+
interface defined in the enhancement, and implemented by the vendorable
11+
[openshift-tests-extension](https://github.com/openshift-eng/openshift-tests-extension).
12+
13+
## Requirements
14+
15+
If the architecture of your local system where `openshift-tests` will run
16+
differs from the cluster under test, you should override the release payload
17+
with a payload of the architecture of your own system, as it is where the
18+
binaries will execute. Note, your OS must still be Linux. That means on Apple
19+
Silicon, you'll still need to run this in a Linux environment, such as a
20+
virtual machine, or x86 podman container.
21+
22+
## Overrides
23+
24+
A number of environment variables for overriding the behavior of external
25+
binaries are available, but in general this should "just work". A complex set
26+
of logic for determining the optimal release payload, and which pull
27+
credentials to use are found in this code, and extensively documented in code
28+
comments. The following environment variables are available to force certain
29+
behaviors:
30+
31+
### Caching
32+
33+
By default, binaries will be cached in `$XDG_CACHE_HOME/openshift-tests`
34+
(typically: `$HOME/.cache/openshift-tests`). Upon invocation, older binaries
35+
than 7 days will be cleaned up. To disable this feature:
36+
37+
```bash
38+
export OPENSHIFT_TESTS_DISABLE_CACHE=1
39+
```
40+
41+
### Registry Auth Credentials
42+
43+
To change the pull secrets used for extracting the external binaries, set:
44+
45+
```bash
46+
export REGISTRY_AUTH_FILE=$HOME/pull.json
47+
```
48+
49+
### Release Payload
50+
51+
To change the payload used for extracting the external binaries, set:
52+
53+
```bash
54+
export EXTENSIONS_PAYLOAD_OVERRIDE=registry.ci.openshift.org/ocp-arm64/release-arm64:4.18.0-0.nightly-arm64-2024-11-15-135718
55+
```

pkg/test/externalbinary/binary.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package externalbinary
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"github.com/openshift/origin/test/extended/util"
9+
"github.com/pkg/errors"
10+
"io"
11+
"log"
12+
"os/exec"
13+
"path/filepath"
14+
"strings"
15+
"sync"
16+
"syscall"
17+
"time"
18+
)
19+
20+
type externalBinaryStruct struct {
21+
// The payload image tag in which an external binary path can be found
22+
imageTag string
23+
// The binary path to extract from the image
24+
binaryPath string
25+
}
26+
27+
var externalBinaries = []externalBinaryStruct{
28+
{
29+
imageTag: "hyperkube",
30+
binaryPath: "/usr/bin/k8s-tests",
31+
},
32+
}
33+
34+
// TestBinary is an abstraction around extracted test binaries that provides an interface for listing the available
35+
// tests. In the future, it will implement the entire openshift-tests-extension interface.
36+
type TestBinary struct {
37+
path string
38+
logger *log.Logger
39+
}
40+
41+
// ListTests returns which tests this binary advertises. Eventually, it should take an environment struct
42+
// to provide to the binary so it can determine for itself which tests are relevant.
43+
func (b *TestBinary) ListTests(ctx context.Context) (ExtensionTestSpecs, error) {
44+
var tests ExtensionTestSpecs
45+
start := time.Now()
46+
binName := filepath.Base(b.path)
47+
48+
b.logger.Printf("Listing tests for %q", binName)
49+
command := exec.Command(b.path, "list")
50+
testList, err := runWithTimeout(ctx, command, 10*time.Minute)
51+
if err != nil {
52+
return nil, fmt.Errorf("failed running '%s list': %w", b.path, err)
53+
}
54+
buf := bytes.NewBuffer(testList)
55+
for {
56+
line, err := buf.ReadString('\n')
57+
if err == io.EOF {
58+
break
59+
}
60+
if !strings.HasPrefix(line, "[{") {
61+
continue
62+
}
63+
64+
var extensionTestSpecs ExtensionTestSpecs
65+
err = json.Unmarshal([]byte(line), &extensionTestSpecs)
66+
if err != nil {
67+
return nil, err
68+
}
69+
for i := range extensionTestSpecs {
70+
extensionTestSpecs[i].Binary = b.path
71+
}
72+
tests = append(tests, extensionTestSpecs...)
73+
}
74+
b.logger.Printf("Listed %d tests for %q in %v", len(tests), binName, time.Since(start))
75+
return tests, nil
76+
}
77+
78+
// ExtractAllTestBinaries determines the optimal release payload to use, and extracts all the external
79+
// test binaries from it, and returns a slice of them.
80+
func ExtractAllTestBinaries(ctx context.Context, logger *log.Logger, parallelism int) (func(), TestBinaries, error) {
81+
if parallelism < 1 {
82+
return nil, nil, errors.New("parallelism must be greater than zero")
83+
}
84+
85+
releaseImage, err := determineReleasePayloadImage(logger)
86+
if err != nil {
87+
return nil, nil, errors.WithMessage(err, "couldn't determine release image")
88+
}
89+
90+
oc := util.NewCLIWithoutNamespace("default")
91+
registryAuthfilePath, err := getRegistryAuthFilePath(logger, oc)
92+
if err != nil {
93+
return nil, nil, errors.WithMessage(err, "couldn't get registry auth file path")
94+
}
95+
96+
externalBinaryProvider, err := NewExternalBinaryProvider(logger, releaseImage, registryAuthfilePath)
97+
if err != nil {
98+
return nil, nil, errors.WithMessage(err, "could not create external binary provider")
99+
}
100+
101+
var (
102+
binaries []*TestBinary
103+
mu sync.Mutex
104+
wg sync.WaitGroup
105+
errCh = make(chan error, len(externalBinaries))
106+
jobCh = make(chan externalBinaryStruct)
107+
)
108+
109+
// Producer: sends jobs to the jobCh channel
110+
go func() {
111+
defer close(jobCh)
112+
for _, b := range externalBinaries {
113+
select {
114+
case <-ctx.Done():
115+
return // Exit if context is cancelled
116+
case jobCh <- b:
117+
}
118+
}
119+
}()
120+
121+
// Consumer workers: extract test binaries concurrently
122+
for i := 0; i < parallelism; i++ {
123+
wg.Add(1)
124+
go func() {
125+
defer wg.Done()
126+
for {
127+
select {
128+
case <-ctx.Done():
129+
return // Context is cancelled
130+
case b, ok := <-jobCh:
131+
if !ok {
132+
return // Channel is closed
133+
}
134+
testBinary, err := externalBinaryProvider.ExtractBinaryFromReleaseImage(b.imageTag, b.binaryPath)
135+
if err != nil {
136+
errCh <- err
137+
continue
138+
}
139+
mu.Lock()
140+
binaries = append(binaries, testBinary)
141+
mu.Unlock()
142+
}
143+
}
144+
145+
}()
146+
}
147+
148+
// Wait for all workers to finish
149+
wg.Wait()
150+
close(errCh)
151+
152+
// Check if any errors were reported
153+
var errs []string
154+
for err := range errCh {
155+
errs = append(errs, err.Error())
156+
}
157+
if len(errs) > 0 {
158+
externalBinaryProvider.Cleanup()
159+
return nil, nil, fmt.Errorf("encountered errors while extracting binaries: %s", strings.Join(errs, ";"))
160+
}
161+
162+
return externalBinaryProvider.Cleanup, binaries, nil
163+
}
164+
165+
type TestBinaries []*TestBinary
166+
167+
// ListTests extracts the tests from all TestBinaries using the specified parallelism.
168+
func (binaries TestBinaries) ListTests(ctx context.Context, parallelism int) (ExtensionTestSpecs, error) {
169+
var (
170+
allTests ExtensionTestSpecs
171+
mu sync.Mutex
172+
wg sync.WaitGroup
173+
errCh = make(chan error, len(binaries))
174+
jobCh = make(chan *TestBinary)
175+
)
176+
177+
// Producer: sends jobs to the jobCh channel
178+
go func() {
179+
defer close(jobCh)
180+
for _, binary := range binaries {
181+
select {
182+
case <-ctx.Done():
183+
return // Exit when context is cancelled
184+
case jobCh <- binary:
185+
}
186+
}
187+
}()
188+
189+
// Consumer workers: extract tests concurrently
190+
for i := 0; i < parallelism; i++ {
191+
wg.Add(1)
192+
go func() {
193+
defer wg.Done()
194+
for {
195+
select {
196+
case <-ctx.Done():
197+
return // Exit when context is cancelled
198+
case binary, ok := <-jobCh:
199+
if !ok {
200+
return // Channel was closed
201+
}
202+
tests, err := binary.ListTests(ctx)
203+
if err != nil {
204+
errCh <- err
205+
}
206+
mu.Lock()
207+
allTests = append(allTests, tests...)
208+
mu.Unlock()
209+
}
210+
}
211+
}()
212+
}
213+
214+
// Wait for all workers to finish
215+
wg.Wait()
216+
close(errCh)
217+
218+
// Check if any errors were reported
219+
var errs []string
220+
for err := range errCh {
221+
errs = append(errs, err.Error())
222+
}
223+
if len(errs) > 0 {
224+
return nil, fmt.Errorf("encountered errors while listing tests: %s", strings.Join(errs, ";"))
225+
}
226+
227+
return allTests, nil
228+
}
229+
230+
func runWithTimeout(ctx context.Context, c *exec.Cmd, timeout time.Duration) ([]byte, error) {
231+
if timeout > 0 {
232+
go func() {
233+
select {
234+
// interrupt tests after timeout, and abort if they don't complete quick enough
235+
case <-time.After(timeout):
236+
if c.Process != nil {
237+
c.Process.Signal(syscall.SIGINT)
238+
}
239+
// if the process appears to be hung a significant amount of time after the timeout
240+
// send an ABRT so we get a stack dump
241+
select {
242+
case <-time.After(time.Minute):
243+
if c.Process != nil {
244+
c.Process.Signal(syscall.SIGABRT)
245+
}
246+
}
247+
case <-ctx.Done():
248+
if c.Process != nil {
249+
c.Process.Signal(syscall.SIGINT)
250+
}
251+
}
252+
253+
}()
254+
}
255+
return c.CombinedOutput()
256+
}

0 commit comments

Comments
 (0)