Skip to content

Commit 1a1dbef

Browse files
committed
Fix review findings
1 parent 4a70d59 commit 1a1dbef

File tree

6 files changed

+293
-15
lines changed

6 files changed

+293
-15
lines changed

examples/scratch-env/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010

1111
require (
1212
github.com/beorn7/perks v1.0.1 // indirect
13+
github.com/blang/semver/v4 v4.0.0 // indirect
1314
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1415
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1516
github.com/emicklei/go-restful/v3 v3.11.0 // indirect

examples/scratch-env/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
22
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3+
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
4+
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
35
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
46
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
57
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module sigs.k8s.io/controller-runtime
33
go 1.23.0
44

55
require (
6+
github.com/blang/semver/v4 v4.0.0
67
github.com/evanphx/json-patch/v5 v5.9.11
78
github.com/fsnotify/fsnotify v1.7.0
89
github.com/go-logr/logr v1.4.2
@@ -35,7 +36,6 @@ require (
3536
cel.dev/expr v0.19.1 // indirect
3637
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
3738
github.com/beorn7/perks v1.0.1 // indirect
38-
github.com/blang/semver/v4 v4.0.0 // indirect
3939
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
4040
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4141
github.com/davecgh/go-spew v1.1.1 // indirect

pkg/envtest/binaries.go

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1-
// SPDX-License-Identifier: Apache-2.0
2-
// Copyright 2021 The Kubernetes Authors
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
316

417
package envtest
518

@@ -19,8 +32,10 @@ import (
1932
"path"
2033
"path/filepath"
2134
"runtime"
35+
"sort"
2236
"strings"
2337

38+
"github.com/blang/semver/v4"
2439
"sigs.k8s.io/yaml"
2540
)
2641

@@ -48,7 +63,7 @@ func SetupEnvtestDefaultBinaryAssetsDirectory() (string, error) {
4863
if baseDir == "" {
4964
return "", errors.New("%LocalAppData% is not defined")
5065
}
51-
case "darwin", "ios":
66+
case "darwin":
5267
homeDir := os.Getenv("HOME")
5368
if homeDir == "" {
5469
return "", errors.New("$HOME is not defined")
@@ -109,6 +124,20 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse
109124
}
110125
}
111126

127+
var binaryAssetsIndex *index
128+
if binaryAssetsVersion == "" {
129+
var err error
130+
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL)
131+
if err != nil {
132+
return "", "", "", err
133+
}
134+
135+
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex)
136+
if err != nil {
137+
return "", "", "", err
138+
}
139+
}
140+
112141
// Storing the envtest binaries in a directory structure that is compatible with setup-envtest.
113142
// This makes it possible to share the envtest binaries with setup-envtest if the BinaryAssetsDirectory is set to SetupEnvtestDefaultBinaryAssetsDirectory().
114143
downloadDir := path.Join(downloadRootDir, fmt.Sprintf("%s-%s-%s", strings.TrimPrefix(binaryAssetsVersion, "v"), runtime.GOOS, runtime.GOARCH))
@@ -127,8 +156,17 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse
127156
return apiServerPath, etcdPath, kubectlPath, nil
128157
}
129158

159+
// Get Index if we didn't have to get it above to get the latest stable version.
160+
if binaryAssetsIndex == nil {
161+
var err error
162+
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL)
163+
if err != nil {
164+
return "", "", "", err
165+
}
166+
}
167+
130168
buf := &bytes.Buffer{}
131-
if err := downloadBinaryAssetsArchive(ctx, binaryAssetsIndexURL, binaryAssetsVersion, buf); err != nil {
169+
if err := downloadBinaryAssetsArchive(ctx, binaryAssetsIndex, binaryAssetsVersion, buf); err != nil {
132170
return "", "", "", err
133171
}
134172

@@ -180,12 +218,7 @@ func fileExists(path string) bool {
180218
return false
181219
}
182220

183-
func downloadBinaryAssetsArchive(ctx context.Context, indexURL, version string, out io.Writer) error {
184-
index, err := getIndex(ctx, indexURL)
185-
if err != nil {
186-
return err
187-
}
188-
221+
func downloadBinaryAssetsArchive(ctx context.Context, index *index, version string, out io.Writer) error {
189222
archives, ok := index.Releases[version]
190223
if !ok {
191224
return fmt.Errorf("failed to find envtest binaries for version %s", version)
@@ -219,6 +252,36 @@ func downloadBinaryAssetsArchive(ctx context.Context, indexURL, version string,
219252
return readBody(resp, out, archiveName, archive.Hash)
220253
}
221254

255+
func latestStableVersionFromIndex(index *index) (string, error) {
256+
if len(index.Releases) == 0 {
257+
return "", fmt.Errorf("failed to find latest stable version from index: index is empty")
258+
}
259+
260+
parsedVersions := []semver.Version{}
261+
for releaseVersion := range index.Releases {
262+
v, err := semver.ParseTolerant(releaseVersion)
263+
if err != nil {
264+
return "", fmt.Errorf("failed to parse version %q: %w", releaseVersion, err)
265+
}
266+
267+
// Filter out pre-releases.
268+
if len(v.Pre) > 0 {
269+
continue
270+
}
271+
272+
parsedVersions = append(parsedVersions, v)
273+
}
274+
275+
if len(parsedVersions) == 0 {
276+
return "", fmt.Errorf("failed to find latest stable version from index: index does not have stable versions")
277+
}
278+
279+
sort.Slice(parsedVersions, func(i, j int) bool {
280+
return parsedVersions[i].GT(parsedVersions[j])
281+
})
282+
return "v" + parsedVersions[0].String(), nil
283+
}
284+
222285
func getIndex(ctx context.Context, indexURL string) (*index, error) {
223286
loc, err := url.Parse(indexURL)
224287
if err != nil {

pkg/envtest/binaries_test.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package envtest
18+
19+
import (
20+
"archive/tar"
21+
"bytes"
22+
"compress/gzip"
23+
"context"
24+
"crypto/rand"
25+
"crypto/sha512"
26+
"encoding/hex"
27+
"fmt"
28+
"net/http"
29+
"os"
30+
"path"
31+
"runtime"
32+
33+
. "github.com/onsi/ginkgo/v2"
34+
. "github.com/onsi/gomega"
35+
"github.com/onsi/gomega/ghttp"
36+
"sigs.k8s.io/yaml"
37+
)
38+
39+
var _ = Describe("Test download binaries", func() {
40+
var downloadDirectory string
41+
var server *ghttp.Server
42+
43+
BeforeEach(func() {
44+
downloadDirectory = GinkgoT().TempDir()
45+
46+
server = ghttp.NewServer()
47+
DeferCleanup(func() {
48+
server.Close()
49+
})
50+
setupServer(server)
51+
})
52+
53+
It("should download binaries of latest stable version", func() {
54+
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.Background(), downloadDirectory, "", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
55+
Expect(err).ToNot(HaveOccurred())
56+
57+
// Verify latest stable version (v1.32.0) was downloaded
58+
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.32.0-%s-%s", runtime.GOOS, runtime.GOARCH))
59+
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver")))
60+
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd")))
61+
Expect(kubectlPath).To(Equal(path.Join(versionDownloadDirectory, "kubectl")))
62+
63+
dirEntries, err := os.ReadDir(versionDownloadDirectory)
64+
Expect(err).ToNot(HaveOccurred())
65+
var actualFiles []string
66+
for _, e := range dirEntries {
67+
actualFiles = append(actualFiles, e.Name())
68+
}
69+
Expect(actualFiles).To(ConsistOf("some-file"))
70+
})
71+
72+
It("should download v1.32.0 binaries", func() {
73+
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(context.Background(), downloadDirectory, "v1.31.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
74+
Expect(err).ToNot(HaveOccurred())
75+
76+
// Verify latest stable version (v1.32.0) was downloaded
77+
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH))
78+
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver")))
79+
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd")))
80+
Expect(kubectlPath).To(Equal(path.Join(versionDownloadDirectory, "kubectl")))
81+
82+
dirEntries, err := os.ReadDir(versionDownloadDirectory)
83+
Expect(err).ToNot(HaveOccurred())
84+
var actualFiles []string
85+
for _, e := range dirEntries {
86+
actualFiles = append(actualFiles, e.Name())
87+
}
88+
Expect(actualFiles).To(ConsistOf("some-file"))
89+
})
90+
})
91+
92+
var (
93+
envtestBinaryArchives = index{
94+
Releases: map[string]release{
95+
"v1.32.0": map[string]archive{
96+
"envtest-v1.32.0-darwin-amd64.tar.gz": {},
97+
"envtest-v1.32.0-darwin-arm64.tar.gz": {},
98+
"envtest-v1.32.0-linux-amd64.tar.gz": {},
99+
"envtest-v1.32.0-linux-arm64.tar.gz": {},
100+
"envtest-v1.32.0-linux-ppc64le.tar.gz": {},
101+
"envtest-v1.32.0-linux-s390x.tar.gz": {},
102+
"envtest-v1.32.0-windows-amd64.tar.gz": {},
103+
},
104+
"v1.31.0": map[string]archive{
105+
"envtest-v1.31.0-darwin-amd64.tar.gz": {},
106+
"envtest-v1.31.0-darwin-arm64.tar.gz": {},
107+
"envtest-v1.31.0-linux-amd64.tar.gz": {},
108+
"envtest-v1.31.0-linux-arm64.tar.gz": {},
109+
"envtest-v1.31.0-linux-ppc64le.tar.gz": {},
110+
"envtest-v1.31.0-linux-s390x.tar.gz": {},
111+
"envtest-v1.31.0-windows-amd64.tar.gz": {},
112+
},
113+
},
114+
}
115+
)
116+
117+
func setupServer(server *ghttp.Server) {
118+
itemsHTTP := makeArchives(envtestBinaryArchives)
119+
120+
// The index from itemsHTTP contains only relative SelfLinks.
121+
// finalIndex will contain the full links based on server.Addr().
122+
finalIndex := index{
123+
Releases: map[string]release{},
124+
}
125+
126+
for releaseVersion, releases := range itemsHTTP.index.Releases {
127+
finalIndex.Releases[releaseVersion] = release{}
128+
129+
for archiveName, a := range releases {
130+
finalIndex.Releases[releaseVersion][archiveName] = archive{
131+
Hash: a.Hash,
132+
SelfLink: fmt.Sprintf("http://%s/%s", server.Addr(), a.SelfLink),
133+
}
134+
content := itemsHTTP.contents[archiveName]
135+
136+
// Note: Using the relative path from archive here instead of the full path.
137+
server.RouteToHandler("GET", "/"+a.SelfLink, func(resp http.ResponseWriter, req *http.Request) {
138+
resp.WriteHeader(http.StatusOK)
139+
Expect(resp.Write(content)).To(Equal(len(content)))
140+
})
141+
}
142+
}
143+
144+
indexYAML, err := yaml.Marshal(finalIndex)
145+
Expect(err).ToNot(HaveOccurred())
146+
147+
server.RouteToHandler("GET", "/envtest-releases.yaml", ghttp.RespondWith(
148+
http.StatusOK,
149+
indexYAML,
150+
))
151+
}
152+
153+
type itemsHTTP struct {
154+
index index
155+
contents map[string][]byte
156+
}
157+
158+
func makeArchives(i index) itemsHTTP {
159+
// This creates a new copy of the index so modifying the index
160+
// in some tests doesn't affect others.
161+
res := itemsHTTP{
162+
index: index{
163+
Releases: map[string]release{},
164+
},
165+
contents: map[string][]byte{},
166+
}
167+
168+
for releaseVersion, releases := range i.Releases {
169+
res.index.Releases[releaseVersion] = release{}
170+
for archiveName := range releases {
171+
var chunk [1024 * 48]byte // 1.5 times our chunk read size in GetVersion
172+
copy(chunk[:], archiveName)
173+
if _, err := rand.Read(chunk[len(archiveName):]); err != nil {
174+
panic(err)
175+
}
176+
content, hash := makeArchive(chunk[:])
177+
178+
res.index.Releases[releaseVersion][archiveName] = archive{
179+
Hash: hash,
180+
// Note: Only storing the name of the archive for now.
181+
// This will be expanded later to a full URL once the server is running.
182+
SelfLink: archiveName,
183+
}
184+
res.contents[archiveName] = content
185+
}
186+
}
187+
return res
188+
}
189+
190+
func makeArchive(contents []byte) ([]byte, string) {
191+
out := new(bytes.Buffer)
192+
gzipWriter := gzip.NewWriter(out)
193+
tarWriter := tar.NewWriter(gzipWriter)
194+
err := tarWriter.WriteHeader(&tar.Header{
195+
Name: "controller-tools/envtest/some-file",
196+
Size: int64(len(contents)),
197+
Mode: 0777, // so we can check that we fix this later
198+
})
199+
if err != nil {
200+
panic(err)
201+
}
202+
_, err = tarWriter.Write(contents)
203+
if err != nil {
204+
panic(err)
205+
}
206+
tarWriter.Close()
207+
gzipWriter.Close()
208+
content := out.Bytes()
209+
// controller-tools is using sha512
210+
hash := sha512.Sum512(content)
211+
hashEncoded := hex.EncodeToString(hash[:])
212+
return content, hashEncoded
213+
}

pkg/envtest/server.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,12 @@ type Environment struct {
148148
CRDDirectoryPaths []string
149149

150150
// DownloadBinaryAssets indicates that the envtest binaries should be downloaded.
151-
// If this field is set:
152-
// * DownloadBinaryAssetsVersion must also be set
153-
// * If BinaryAssetsDirectory is also set, it is used to store the downloaded binaries,
154-
// otherwise a tmp directory is created.
151+
// If BinaryAssetsDirectory is also set, it is used to store the downloaded binaries,
152+
// otherwise a tmp directory is created.
155153
DownloadBinaryAssets bool
156154

157155
// DownloadBinaryAssetsVersion is the version of envtest binaries to download.
156+
// Defaults to the latest stable version (i.e. excluding alpha / beta / RC versions).
158157
DownloadBinaryAssetsVersion string
159158

160159
// DownloadBinaryAssetsIndexURL is the index used to discover envtest binaries to download.

0 commit comments

Comments
 (0)