-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Expand file tree
/
Copy pathutil.go
More file actions
349 lines (301 loc) · 12.6 KB
/
util.go
File metadata and controls
349 lines (301 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package extensions
import (
"compress/gzip"
"context"
"crypto/sha1"
"debug/elf"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"syscall"
"time"
kapierrs "k8s.io/apimachinery/pkg/api/errors"
"github.com/openshift-eng/openshift-tests-extension/pkg/dbtime"
"github.com/sirupsen/logrus"
imagev1 "github.com/openshift/api/image/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/openshift/origin/test/extended/util"
)
func Time(t *dbtime.DBTime) time.Time {
if t == nil {
return time.Time{}
}
return time.Time(*t)
}
// 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.
func ungzipFile(extractedBinary string) (string, error) {
if strings.HasSuffix(extractedBinary, ".gz") {
gzFile, err := os.Open(extractedBinary)
if err != nil {
return "", fmt.Errorf("failed to open gzip file: %w", err)
}
defer gzFile.Close()
gzipReader, err := gzip.NewReader(gzFile)
if err != nil {
return "", fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzipReader.Close()
newFilePath := strings.TrimSuffix(extractedBinary, ".gz")
outFile, err := os.Create(newFilePath)
if err != nil {
return "", fmt.Errorf("failed to create output file: %w", err)
}
defer outFile.Close()
if _, err := io.Copy(outFile, gzipReader); err != nil {
return "", fmt.Errorf("failed to write to output file: %w", err)
}
// Attempt to delete the original .gz file
if err := os.Remove(extractedBinary); err != nil {
return "", fmt.Errorf("failed to delete original .gz file: %w", err)
}
return newFilePath, nil
}
// Return the original path if the file was not decompressed
return extractedBinary, nil
}
// Checks whether the binary has a compatible CPU architecture to the
// host.
func checkCompatibleArchitecture(executablePath string) error {
file, err := os.Open(executablePath)
if err != nil {
return fmt.Errorf("failed to open ELF file: %w", err)
}
defer file.Close()
elfFile, err := elf.NewFile(file)
if err != nil {
return fmt.Errorf("failed to parse ELF file: %w", err)
}
// Determine the architecture of the ELF file
elfArch := elfFile.Machine
var expectedArch elf.Machine
// Determine the host architecture
switch runtime.GOARCH {
case "amd64":
expectedArch = elf.EM_X86_64
case "arm64":
expectedArch = elf.EM_AARCH64
case "s390x":
expectedArch = elf.EM_S390
case "ppc64le":
expectedArch = elf.EM_PPC64
default:
return fmt.Errorf("unsupported host architecture: %s", runtime.GOARCH)
}
if elfArch != expectedArch {
return fmt.Errorf("binary architecture %q doesn't matched expected architecture %q", elfArch, expectedArch)
}
return nil
}
// runImageExtract extracts src from specified image to dst
func runImageExtract(image, src, dst string, dockerConfigJsonPath string) error {
var err error
var out []byte
maxRetries := 6
startTime := time.Now()
logrus.Infof("Run image extract for release image %q and src %q", image, src)
for i := 1; i <= maxRetries; i++ {
args := []string{"--kubeconfig=" + util.KubeConfigPath(), "image", "extract", image, fmt.Sprintf("--path=%s:%s", src, dst), "--confirm"}
if len(dockerConfigJsonPath) > 0 {
args = append(args, fmt.Sprintf("--registry-config=%s", dockerConfigJsonPath))
}
cmd := exec.Command("oc", args...)
out, err = cmd.CombinedOutput()
if err != nil {
// Allow retries for up to one minute. The openshift internal registry
// occasionally reports "manifest unknown" when a new image has just
// been exposed through an imagestream.
time.Sleep(10 * time.Second)
continue
}
logrus.Infof("Completed image extract for release image %q in %+v", image, time.Since(startTime))
return nil
}
return fmt.Errorf("error during image extract: %w (%v)", err, string(out))
}
// pullSpecToDirName converts a release pullspec to a directory, for use with caching.
func pullSpecToDirName(input string) string {
// Remove any non-alphanumeric characters (except '-') and replace them with '_'.
re := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
safeName := re.ReplaceAllString(input, "_")
// Truncate long names
if len(safeName) > 249 {
safeName = safeName[:249]
}
// Add suffix to avoid collision when truncating
hash := sha1.Sum([]byte(input))
safeName += fmt.Sprintf("_%x", hash[:6])
// Return a clean, safe directory path.
return filepath.Clean(safeName)
}
func DetermineReleasePayloadImage() (string, error) {
var releaseImage string
// Highest priority override is EXTENSIONS_PAYLOAD_OVERRIDE
overrideReleaseImage := os.Getenv("EXTENSIONS_PAYLOAD_OVERRIDE")
if len(overrideReleaseImage) != 0 {
// if "cluster" is specified, prefer target cluster payload even if RELEASE_IMAGE_LATEST is set.
if overrideReleaseImage != "cluster" {
releaseImage = overrideReleaseImage
logrus.Infof("Using env EXTENSIONS_PAYLOAD_OVERRIDE for release image %q", releaseImage)
}
} else {
// Allow testing using an overridden source for external tests.
envReleaseImage := os.Getenv("RELEASE_IMAGE_LATEST")
if len(envReleaseImage) != 0 {
releaseImage = envReleaseImage
logrus.Infof("Using env RELEASE_IMAGE_LATEST for release image %q", releaseImage)
}
}
if len(releaseImage) == 0 {
// Note that MicroShift does not have this resource. The test driver must use ENV vars.
oc := util.NewCLIForMonitorTest("default")
cv, err := oc.AdminConfigClient().ConfigV1().ClusterVersions().Get(context.TODO(), "version",
metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed reading ClusterVersion/version: %w", err)
}
releaseImage = cv.Status.Desired.Image
if len(releaseImage) == 0 {
return "", fmt.Errorf("cannot determine release image from ClusterVersion resource")
}
logrus.WithField("release_image", releaseImage).Infof("Using target cluster release image")
}
return releaseImage, nil
}
// createBinPath ensures the given path exists, is writable, and allows executing binaries.
func createBinPath(path string) error {
// Create the directory if it doesn't exist.
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", path, err)
}
// Create a simple shell script to test executability.
testFile := filepath.Join(path, "cache_test.sh")
scriptContent := "#!/bin/sh\necho 'Executable test passed'"
// Write the script to the cache directory.
if err := os.WriteFile(testFile, []byte(scriptContent), 0755); err != nil {
return fmt.Errorf("failed to write test file in cache path %s: %w", path, err)
}
defer os.Remove(testFile)
// Attempt to execute the test script.
cmd := exec.Command(testFile)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to execute test file in cache path %s: %w", path, err)
}
// Check if the output is as expected.
if string(output) != "Executable test passed\n" {
return fmt.Errorf("unexpected output from executable test in cache path %s: %s", path, output)
}
return nil
}
// ExtractReleaseImageStream extracts image references from the given releaseImage and returns
// an ImageStream object with tags associated with image-references from that payload.
func ExtractReleaseImageStream(extractPath, releaseImage string,
registryAuthFilePath string) (*imagev1.ImageStream, string, error) {
imageReferencesPath := path.Join(extractPath, "image-references")
// Acquire a file lock to prevent concurrent extraction of image-references.
// This is necessary when multiple openshift-tests processes share the same cache directory.
lockPath := imageReferencesPath + ".lock"
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return nil, "", fmt.Errorf("failed to create lock file %q: %w", lockPath, err)
}
defer lockFile.Close()
if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX); err != nil {
return nil, "", fmt.Errorf("failed to acquire lock on %q: %w", lockPath, err)
}
defer syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
if _, err := os.Stat(imageReferencesPath); err != nil {
if err := runImageExtract(releaseImage, "/release-manifests/image-references", extractPath, registryAuthFilePath); err != nil {
return nil, "", fmt.Errorf("failed extracting image-references from %q: %w", releaseImage, err)
}
}
jsonFile, err := os.Open(filepath.Join(extractPath, "image-references"))
if err != nil {
return nil, "", fmt.Errorf("failed reading image-references from %q: %w", releaseImage, err)
}
defer jsonFile.Close()
data, err := io.ReadAll(jsonFile)
if err != nil {
return nil, "", fmt.Errorf("unable to load release image-references from %q: %w", releaseImage, err)
}
is := &imagev1.ImageStream{}
if err := json.Unmarshal(data, &is); err != nil {
return nil, "", fmt.Errorf("unable to load release image-references from %q: %w", releaseImage, err)
}
if is.Kind != "ImageStream" || is.APIVersion != "image.openshift.io/v1" {
return nil, "", fmt.Errorf("unrecognized image-references in release payload %q", releaseImage)
}
// Allow environmental overrides for individual component images.
for _, tag := range is.Spec.Tags {
componentEnvName := "EXTENSIONS_PAYLOAD_OVERRIDE_" + tag.Name
componentOverrideImage := os.Getenv(componentEnvName)
if len(componentOverrideImage) != 0 {
tag.From.Name = componentOverrideImage
logrus.Infof("Overrode release image tag %q for with env %s value %q", tag.Name, componentEnvName, componentOverrideImage)
}
}
return is, releaseImage, nil
}
func DetermineRegistryAuthFilePath(tmpDir string, oc *util.CLI) (string, error) {
// To extract binaries bearing external tests, we must inspect the release
// payload under tests as well as extract content from component images
// referenced by that payload.
// openshift-tests is frequently run in the context of a CI job, within a pod.
// CI sets $RELEASE_IMAGE_LATEST to a pullspec for the release payload under test. This
// pull spec resolve to:
// 1. A build farm ci-op-* namespace / imagestream location (anonymous access permitted).
// 2. A quay.io/openshift-release-dev location (for tests against promoted ART payloads -- anonymous access permitted).
// 3. A registry.ci.openshift.org/ocp-<arch>/release:<tag> (request registry.ci.openshift.org token).
// Within the pod, we don't necessarily have a pull-secret for #3 OR the component images
// a payload references (which are private, unless in a ci-op-* imagestream).
// We try the following options:
// 1. If set, use the REGISTRY_AUTH_FILE environment variable to an auths file with
// pull secrets capable of reading appropriate payload & component image
// information.
// 2. If it exists, use a file /run/secrets/ci.openshift.io/cluster-profile/pull-secret
// (conventional location for pull-secret information for CI cluster profile).
// 3. Use openshift-config secret/pull-secret from the cluster-under-test, if it exists
// (Microshift does not).
// 4. Use unauthenticated access to the payload image and component images.
registryAuthFilePath := os.Getenv("REGISTRY_AUTH_FILE")
// if the environment variable is not set, extract the target cluster's
// platform pull secret.
if len(registryAuthFilePath) != 0 {
logrus.Infof("Using REGISTRY_AUTH_FILE environment variable: %v", registryAuthFilePath)
} else {
// See if the cluster-profile has stored a pull-secret at the conventional location.
ciProfilePullSecretPath := "/run/secrets/ci.openshift.io/cluster-profile/pull-secret"
_, err := os.Stat(ciProfilePullSecretPath)
if !os.IsNotExist(err) {
logrus.Infof("Detected %v; using cluster profile for image access", ciProfilePullSecretPath)
registryAuthFilePath = ciProfilePullSecretPath
} else {
// Inspect the cluster-under-test and read its cluster pull-secret dockerconfigjson value.
clusterPullSecret, err := oc.AdminKubeClient().CoreV1().Secrets("openshift-config").Get(context.Background(), "pull-secret", metav1.GetOptions{})
if err != nil {
if kapierrs.IsNotFound(err) {
logrus.Warningf("Cluster has no openshift-config secret/pull-secret; falling back to unauthenticated image access")
} else {
return "", fmt.Errorf("unable to read ephemeral cluster pull secret: %w", err)
}
} else {
clusterDockerConfig := clusterPullSecret.Data[".dockerconfigjson"]
registryAuthFilePath = filepath.Join(tmpDir, ".dockerconfigjson")
err = os.WriteFile(registryAuthFilePath, clusterDockerConfig, 0600)
if err != nil {
return "", fmt.Errorf("unable to serialize target cluster pull-secret locally: %w", err)
}
logrus.Infof("Using target cluster pull-secrets for registry auth")
}
}
}
return registryAuthFilePath, nil
}