Skip to content

Commit b146e88

Browse files
committed
envtest: add option to download binaries, bump envtest to v1.32.0
Signed-off-by: Stefan Büringer [email protected]
1 parent 9d8d219 commit b146e88

File tree

4 files changed

+253
-5
lines changed

4 files changed

+253
-5
lines changed

hack/check-everything.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export GOTOOLCHAIN="go$(make --silent go-version)"
3030
${hack_dir}/verify.sh
3131

3232
# Envtest.
33-
ENVTEST_K8S_VERSION=${ENVTEST_K8S_VERSION:-"1.28.0"}
33+
ENVTEST_K8S_VERSION=${ENVTEST_K8S_VERSION:-"1.32.0"}
3434

3535
header_text "installing envtest tools@${ENVTEST_K8S_VERSION} with setup-envtest if necessary"
3636
tmp_bin=/tmp/cr-tests-bin

pkg/client/apiutil/restmapper_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ func setupEnvtest(t *testing.T, disableAggregatedDiscovery bool) *rest.Config {
7777
CRDDirectoryPaths: []string{"testdata"},
7878
}
7979
if disableAggregatedDiscovery {
80+
testEnv.DownloadBinaryAssets = true
81+
testEnv.DownloadBinaryAssetsVersion = "v1.28.0"
8082
testEnv.ControlPlane.GetAPIServer().Configure().Append("feature-gates", "AggregatedDiscoveryEndpoint=false")
8183
}
8284

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright 2021 The Kubernetes Authors
3+
4+
package internal
5+
6+
import (
7+
"context"
8+
"crypto/sha512"
9+
"encoding/hex"
10+
"errors"
11+
"fmt"
12+
"io"
13+
"net/http"
14+
"net/url"
15+
"runtime"
16+
17+
"sigs.k8s.io/yaml"
18+
)
19+
20+
// DefaultIndexURL is the default index used in HTTPClient.
21+
var DefaultIndexURL = "https://raw.githubusercontent.com/kubernetes-sigs/controller-tools/HEAD/envtest-releases.yaml"
22+
23+
// Index represents an index of envtest binary archives. Example:
24+
//
25+
// releases:
26+
// v1.28.0:
27+
// envtest-v1.28.0-darwin-amd64.tar.gz:
28+
// hash: <sha512-hash>
29+
// selfLink: <url-to-archive-with-envtest-binaries>
30+
type Index struct {
31+
// Releases maps Kubernetes versions to Releases (envtest archives).
32+
Releases map[string]Release `json:"releases"`
33+
}
34+
35+
// Release maps an archive name to an archive.
36+
type Release map[string]Archive
37+
38+
// Archive contains the self link to an archive and its hash.
39+
type Archive struct {
40+
Hash string `json:"hash"`
41+
SelfLink string `json:"selfLink"`
42+
}
43+
44+
// DownloadBinaryAssets downloads the given concrete version for the given concrete platform, writing it to the out.
45+
func DownloadBinaryAssets(ctx context.Context, version string, out io.Writer) error {
46+
index, err := getIndex(ctx)
47+
if err != nil {
48+
return err
49+
}
50+
51+
var loc *url.URL
52+
var name string
53+
54+
archives, ok := index.Releases[version]
55+
if !ok {
56+
return fmt.Errorf("error finding binaries for version %s", version)
57+
}
58+
59+
archiveName := fmt.Sprintf("envtest-%s-%s-%s.tar.gz", version, runtime.GOOS, runtime.GOARCH)
60+
archive, ok := archives[archiveName]
61+
if !ok {
62+
return fmt.Errorf("error finding binaries for version %s with archiveName %s", version, archiveName)
63+
}
64+
65+
loc, err = url.Parse(archive.SelfLink)
66+
if err != nil {
67+
return fmt.Errorf("error parsing selfLink %q, %w", loc, err)
68+
}
69+
70+
req, err := http.NewRequestWithContext(ctx, "GET", loc.String(), nil)
71+
if err != nil {
72+
return fmt.Errorf("unable to construct request to fetch %s: %w", name, err)
73+
}
74+
resp, err := http.DefaultClient.Do(req)
75+
if err != nil {
76+
return fmt.Errorf("unable to fetch %s (%s): %w", name, req.URL, err)
77+
}
78+
defer resp.Body.Close()
79+
80+
if resp.StatusCode != 200 {
81+
return fmt.Errorf("unable fetch %s (%s) -- got status %q", name, req.URL, resp.Status)
82+
}
83+
84+
return readBody(resp, out, name, archive.Hash)
85+
}
86+
87+
func getIndex(ctx context.Context) (*Index, error) {
88+
loc, err := url.Parse(DefaultIndexURL)
89+
if err != nil {
90+
return nil, fmt.Errorf("unable to parse index URL: %w", err)
91+
}
92+
93+
req, err := http.NewRequestWithContext(ctx, "GET", loc.String(), nil)
94+
if err != nil {
95+
return nil, fmt.Errorf("unable to construct request to get index: %w", err)
96+
}
97+
98+
resp, err := http.DefaultClient.Do(req)
99+
if err != nil {
100+
return nil, fmt.Errorf("unable to perform request to get index: %w", err)
101+
}
102+
103+
defer resp.Body.Close()
104+
if resp.StatusCode != 200 {
105+
return nil, fmt.Errorf("unable to get index -- got status %q", resp.Status)
106+
}
107+
108+
responseBody, err := io.ReadAll(resp.Body)
109+
if err != nil {
110+
return nil, fmt.Errorf("unable to get index -- unable to read body %w", err)
111+
}
112+
113+
var index Index
114+
if err := yaml.Unmarshal(responseBody, &index); err != nil {
115+
return nil, fmt.Errorf("unable to unmarshal index: %w", err)
116+
}
117+
return &index, nil
118+
}
119+
120+
func readBody(resp *http.Response, out io.Writer, archiveName string, expectedHash string) error {
121+
// stream in chunks to do the checksum, don't load the whole thing into
122+
// memory to avoid causing issues with big files.
123+
buf := make([]byte, 32*1024) // 32KiB, same as io.Copy
124+
hasher := sha512.New()
125+
126+
for cont := true; cont; {
127+
amt, err := resp.Body.Read(buf)
128+
if err != nil && !errors.Is(err, io.EOF) {
129+
return fmt.Errorf("unable read next chunk of %s: %w", archiveName, err)
130+
}
131+
if amt > 0 {
132+
// checksum never returns errors according to docs
133+
hasher.Write(buf[:amt])
134+
if _, err := out.Write(buf[:amt]); err != nil {
135+
return fmt.Errorf("unable write next chunk of %s: %w", archiveName, err)
136+
}
137+
}
138+
cont = amt > 0 && !errors.Is(err, io.EOF)
139+
}
140+
141+
actualHash := hex.EncodeToString(hasher.Sum(nil))
142+
if actualHash != expectedHash {
143+
return fmt.Errorf("checksum mismatch for %s: %s (computed) != %s (reported)", archiveName, actualHash, expectedHash)
144+
}
145+
146+
return nil
147+
}

pkg/envtest/server.go

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ limitations under the License.
1717
package envtest
1818

1919
import (
20+
"archive/tar"
21+
"bytes"
22+
"compress/gzip"
2023
"context"
2124
"fmt"
25+
"io"
2226
"os"
27+
"path"
28+
"path/filepath"
2329
"strings"
2430
"time"
2531

@@ -31,8 +37,8 @@ import (
3137
"k8s.io/client-go/kubernetes/scheme"
3238
"k8s.io/client-go/rest"
3339
"sigs.k8s.io/controller-runtime/pkg/client"
34-
3540
"sigs.k8s.io/controller-runtime/pkg/client/config"
41+
"sigs.k8s.io/controller-runtime/pkg/envtest/internal"
3642
logf "sigs.k8s.io/controller-runtime/pkg/internal/log"
3743
"sigs.k8s.io/controller-runtime/pkg/internal/testing/controlplane"
3844
"sigs.k8s.io/controller-runtime/pkg/internal/testing/process"
@@ -147,6 +153,12 @@ type Environment struct {
147153
// values are merged.
148154
CRDDirectoryPaths []string
149155

156+
// DownloadBinaryAssets indicates that the binaries should be downloaded.
157+
DownloadBinaryAssets bool
158+
159+
// DownloadBinaryAssetsVersion is the version of binaries to download.
160+
DownloadBinaryAssetsVersion string
161+
150162
// BinaryAssetsDirectory is the path where the binaries required for the envtest are
151163
// located in the local environment. This field can be overridden by setting KUBEBUILDER_ASSETS.
152164
BinaryAssetsDirectory string
@@ -233,9 +245,20 @@ func (te *Environment) Start() (*rest.Config, error) {
233245
}
234246
}
235247

236-
apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory)
237-
te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory)
238-
te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory)
248+
if te.DownloadBinaryAssets { // FIXME: remove tmp files afterwards?
249+
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.TODO(), te.BinaryAssetsDirectory, te.DownloadBinaryAssetsVersion)
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
apiServer.Path = apiServerPath
255+
te.ControlPlane.Etcd.Path = etcdPath
256+
te.ControlPlane.KubectlPath = kubectlPath
257+
} else {
258+
apiServer.Path = process.BinPathFinder("kube-apiserver", te.BinaryAssetsDirectory)
259+
te.ControlPlane.Etcd.Path = process.BinPathFinder("etcd", te.BinaryAssetsDirectory)
260+
te.ControlPlane.KubectlPath = process.BinPathFinder("kubectl", te.BinaryAssetsDirectory)
261+
}
239262

240263
if err := te.defaultTimeouts(); err != nil {
241264
return nil, fmt.Errorf("failed to default controlplane timeouts: %w", err)
@@ -303,6 +326,82 @@ func (te *Environment) Start() (*rest.Config, error) {
303326
return te.Config, nil
304327
}
305328

329+
func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAssetsVersion string) (string, string, string, error) {
330+
var downloadDir string
331+
if binaryAssetsDirectory != "" {
332+
downloadDir = binaryAssetsDirectory
333+
if !fileExists(downloadDir) {
334+
if err := os.Mkdir(binaryAssetsDirectory, 0700); err != nil {
335+
return "", "", "", fmt.Errorf("failed to create dir for envtest binaries %q: %w", binaryAssetsDirectory, err)
336+
}
337+
}
338+
} else {
339+
var err error
340+
if downloadDir, err = os.MkdirTemp("", "envtest-binaries-"); err != nil {
341+
return "", "", "", fmt.Errorf("failed to create tmp dir for envtest binaries: %w", err)
342+
}
343+
}
344+
345+
apiServerPath := path.Join(downloadDir, "kube-apiserver")
346+
etcdPath := path.Join(downloadDir, "etcd")
347+
kubectlPath := path.Join(downloadDir, "kubectl")
348+
349+
if fileExists(apiServerPath) && fileExists(etcdPath) && fileExists(kubectlPath) {
350+
return apiServerPath, etcdPath, kubectlPath, nil
351+
}
352+
353+
buf := &bytes.Buffer{}
354+
if err := internal.DownloadBinaryAssets(ctx, binaryAssetsVersion, buf); err != nil {
355+
return "", "", "", fmt.Errorf("failed to create tmp file to download envtest binaries: %w", err)
356+
}
357+
358+
gzStream, err := gzip.NewReader(buf)
359+
if err != nil {
360+
return "", "", "", fmt.Errorf("failed to read TODO: %s", err)
361+
}
362+
tarReader := tar.NewReader(gzStream)
363+
364+
var header *tar.Header
365+
for header, err = tarReader.Next(); err == nil; header, err = tarReader.Next() {
366+
if header.Typeflag != tar.TypeReg { // Skipping non-regular file entry in archive
367+
continue
368+
}
369+
370+
// just dump all files to the main path, ignoring the prefixed directory
371+
// paths -- they're redundant. We also ignore bits for the most part (except for X),
372+
// preferfing our own scheme.
373+
fileName := filepath.Base(header.Name)
374+
375+
perms := 0555 & header.Mode // make sure we're at most r+x
376+
377+
binOut, err := os.OpenFile(path.Join(downloadDir, fileName), os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_TRUNC, os.FileMode(perms))
378+
if err != nil {
379+
if os.IsExist(err) {
380+
continue
381+
}
382+
return "", "", "", fmt.Errorf("unable to create file %s from archive to disk for version-platform pair %s: %s", fileName, downloadDir, err)
383+
}
384+
if err := func() error {
385+
defer binOut.Close()
386+
if _, err := io.Copy(binOut, tarReader); err != nil {
387+
return fmt.Errorf("unable to write file %s from archive to disk for version-platform pair %s", fileName, downloadDir)
388+
}
389+
return nil
390+
}(); err != nil {
391+
return "", "", "", err
392+
}
393+
}
394+
395+
return apiServerPath, etcdPath, kubectlPath, nil
396+
}
397+
398+
func fileExists(path string) bool {
399+
if _, err := os.Stat(path); err == nil {
400+
return true
401+
}
402+
return false
403+
}
404+
306405
// AddUser provisions a new user for connecting to this Environment. The user will
307406
// have the specified name & belong to the specified groups.
308407
//

0 commit comments

Comments
 (0)