Skip to content

Commit 9d3997b

Browse files
authored
✨ envtest: search the assets index for latest of a release series (#3280)
* envtest: replace github.com/blang/semver with k8s.io/apimachinery The former is no longer maintained and the latter is designed to understand Kubernetes versions. This eliminates one direct dependency introduced in b04d5fd. Signed-off-by: Chris Bandy <[email protected]> * envtest: search the assets index for latest of a release series Signed-off-by: Chris Bandy <[email protected]> --------- Signed-off-by: Chris Bandy <[email protected]>
1 parent 9f93124 commit 9d3997b

File tree

5 files changed

+207
-22
lines changed

5 files changed

+207
-22
lines changed

examples/scratch-env/go.mod

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

1111
require (
1212
github.com/beorn7/perks v1.0.1 // indirect
13-
github.com/blang/semver/v4 v4.0.0 // indirect
1413
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1514
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1615
github.com/emicklei/go-restful/v3 v3.12.2 // indirect

examples/scratch-env/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
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=
53
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
64
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
75
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,7 +3,6 @@ module sigs.k8s.io/controller-runtime
33
go 1.24.0
44

55
require (
6-
github.com/blang/semver/v4 v4.0.0
76
github.com/evanphx/json-patch/v5 v5.9.11
87
github.com/fsnotify/fsnotify v1.9.0
98
github.com/go-logr/logr v1.4.2
@@ -37,6 +36,7 @@ require (
3736
cel.dev/expr v0.24.0 // indirect
3837
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
3938
github.com/beorn7/perks v1.0.1 // indirect
39+
github.com/blang/semver/v4 v4.0.0 // indirect
4040
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
4141
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4242
github.com/davecgh/go-spew v1.1.1 // indirect

pkg/envtest/binaries.go

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ import (
3232
"path"
3333
"path/filepath"
3434
"runtime"
35-
"sort"
3635
"strings"
3736

38-
"github.com/blang/semver/v4"
37+
"k8s.io/apimachinery/pkg/util/version"
3938
"sigs.k8s.io/yaml"
4039
)
4140

@@ -111,6 +110,25 @@ type archive struct {
111110
SelfLink string `json:"selfLink"`
112111
}
113112

113+
// parseKubernetesVersion returns:
114+
// 1. the SemVer form of s when it refers to a specific Kubernetes release, or
115+
// 2. the major and minor portions of s when it refers to a release series, or
116+
// 3. an error
117+
func parseKubernetesVersion(s string) (exact string, major, minor uint, err error) {
118+
if v, err := version.ParseSemantic(s); err == nil {
119+
return v.String(), 0, 0, nil
120+
}
121+
122+
// See two parseable components and nothing else.
123+
if v, err := version.ParseGeneric(s); err == nil && len(v.Components()) == 2 {
124+
if v.String() == strings.TrimPrefix(s, "v") {
125+
return "", v.Major(), v.Minor(), nil
126+
}
127+
}
128+
129+
return "", 0, 0, fmt.Errorf("could not parse %q as version", s)
130+
}
131+
114132
func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAssetsVersion, binaryAssetsIndexURL string) (string, string, string, error) {
115133
if binaryAssetsIndexURL == "" {
116134
binaryAssetsIndexURL = DefaultBinaryAssetsIndexURL
@@ -125,14 +143,23 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse
125143
}
126144

127145
var binaryAssetsIndex *index
128-
if binaryAssetsVersion == "" {
129-
var err error
146+
switch exact, major, minor, err := parseKubernetesVersion(binaryAssetsVersion); {
147+
case binaryAssetsVersion != "" && err != nil:
148+
return "", "", "", err
149+
150+
case binaryAssetsVersion != "" && exact != "":
151+
// Look for these specific binaries locally before downloading them from the release index.
152+
// Use the canonical form of the version from here on.
153+
binaryAssetsVersion = "v" + exact
154+
155+
case binaryAssetsVersion == "" || major != 0 || minor != 0:
156+
// Select a stable version from the release index before continuing.
130157
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL)
131158
if err != nil {
132159
return "", "", "", err
133160
}
134161

135-
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex)
162+
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, major, minor)
136163
if err != nil {
137164
return "", "", "", err
138165
}
@@ -252,34 +279,50 @@ func downloadBinaryAssetsArchive(ctx context.Context, index *index, version stri
252279
return readBody(resp, out, archiveName, archive.Hash)
253280
}
254281

255-
func latestStableVersionFromIndex(index *index) (string, error) {
282+
// latestStableVersionFromIndex returns the version with highest [precedence] in index that is not a prerelease.
283+
// When either major or minor are not zero, the returned version will have those major and minor versions.
284+
// Note that the version cannot be limited to 0.0.x this way.
285+
//
286+
// It is an error when there is no appropriate version in index.
287+
//
288+
// [precedence]: https://semver.org/spec/v2.0.0.html#spec-item-11
289+
func latestStableVersionFromIndex(index *index, major, minor uint) (string, error) {
256290
if len(index.Releases) == 0 {
257291
return "", fmt.Errorf("failed to find latest stable version from index: index is empty")
258292
}
259293

260-
parsedVersions := []semver.Version{}
294+
var found *version.Version
261295
for releaseVersion := range index.Releases {
262-
v, err := semver.ParseTolerant(releaseVersion)
296+
v, err := version.ParseSemantic(releaseVersion)
263297
if err != nil {
264298
return "", fmt.Errorf("failed to parse version %q: %w", releaseVersion, err)
265299
}
266300

267301
// Filter out pre-releases.
268-
if len(v.Pre) > 0 {
302+
if len(v.PreRelease()) > 0 {
269303
continue
270304
}
271305

272-
parsedVersions = append(parsedVersions, v)
306+
// Filter on release series, if any.
307+
if (major != 0 || minor != 0) && (v.Major() != major || v.Minor() != minor) {
308+
continue
309+
}
310+
311+
if found == nil || v.GreaterThan(found) {
312+
found = v
313+
}
273314
}
274315

275-
if len(parsedVersions) == 0 {
276-
return "", fmt.Errorf("failed to find latest stable version from index: index does not have stable versions")
316+
if found == nil {
317+
search := "any"
318+
if major != 0 || minor != 0 {
319+
search = fmt.Sprint(major, ".", minor)
320+
}
321+
322+
return "", fmt.Errorf("failed to find latest stable version from index: index does not have %s stable versions", search)
277323
}
278324

279-
sort.Slice(parsedVersions, func(i, j int) bool {
280-
return parsedVersions[i].GT(parsedVersions[j])
281-
})
282-
return "v" + parsedVersions[0].String(), nil
325+
return "v" + found.String(), nil
283326
}
284327

285328
func getIndex(ctx context.Context, indexURL string) (*index, error) {

pkg/envtest/binaries_test.go

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,117 @@ import (
2828
"os"
2929
"path"
3030
"runtime"
31+
"strings"
32+
"testing"
3133

3234
. "github.com/onsi/ginkgo/v2"
3335
. "github.com/onsi/gomega"
3436
"github.com/onsi/gomega/ghttp"
3537
"sigs.k8s.io/yaml"
3638
)
3739

40+
func TestParseKubernetesVersion(t *testing.T) {
41+
t.Parallel()
42+
43+
testCases := []struct {
44+
name string
45+
inputs []string
46+
47+
expectError string
48+
expectExact bool
49+
expectSeriesMajor uint
50+
expectSeriesMinor uint
51+
}{
52+
{
53+
name: `SemVer and "v" prefix are exact`,
54+
inputs: []string{
55+
"1.2.3", "v1.2.3", "v1.30.2", "v1.31.0-beta.0", "v1.33.0-alpha.2",
56+
},
57+
expectExact: true,
58+
},
59+
{
60+
name: "empty string is not a version",
61+
inputs: []string{""},
62+
expectError: "could not parse",
63+
},
64+
{
65+
name: "leading zeroes are not a version",
66+
inputs: []string{
67+
"01.2.0", "00001.2.3", "1.2.03", "v01.02.0003",
68+
},
69+
expectError: "could not parse",
70+
},
71+
{
72+
name: "weird stuff is not a version",
73+
inputs: []string{
74+
"asdf", "version", "vegeta4", "the.1", "2ne1", "=7.8.9", "10.x", "*",
75+
"0.0001", "1.00002", "v1.2anything", "1.2.x", "1.2.z", "1.2.*",
76+
},
77+
expectError: "could not parse",
78+
},
79+
{
80+
name: "one number is not a version",
81+
inputs: []string{
82+
"1", "v1", "v001", "1.", "v1.", "1.x",
83+
},
84+
expectError: "could not parse",
85+
},
86+
{
87+
name: "two numbers are a release series",
88+
inputs: []string{"0.1", "v0.1"},
89+
90+
expectSeriesMajor: 0,
91+
expectSeriesMinor: 1,
92+
},
93+
{
94+
name: "two numbers are a release series",
95+
inputs: []string{"1.2", "v1.2"},
96+
97+
expectSeriesMajor: 1,
98+
expectSeriesMinor: 2,
99+
},
100+
}
101+
102+
for _, tc := range testCases {
103+
t.Run(tc.name, func(t *testing.T) {
104+
for _, input := range tc.inputs {
105+
exact, major, minor, err := parseKubernetesVersion(input)
106+
107+
if tc.expectError != "" && err == nil {
108+
t.Errorf("expected error %q, got none", tc.expectError)
109+
}
110+
if tc.expectError != "" && !strings.Contains(err.Error(), tc.expectError) {
111+
t.Errorf("expected error %q, got %q", tc.expectError, err)
112+
}
113+
if tc.expectError == "" && err != nil {
114+
t.Errorf("expected no error, got %q", err)
115+
continue
116+
}
117+
118+
if tc.expectExact {
119+
if expected := strings.TrimPrefix(input, "v"); exact != expected {
120+
t.Errorf("expected canonical %q for %q, got %q", expected, input, exact)
121+
}
122+
if major != 0 || minor != 0 {
123+
t.Errorf("expected no release series for %q, got (%v, %v)", input, major, minor)
124+
}
125+
continue
126+
}
127+
128+
if major != tc.expectSeriesMajor {
129+
t.Errorf("expected major %v for %q, got %v", tc.expectSeriesMajor, input, major)
130+
}
131+
if minor != tc.expectSeriesMinor {
132+
t.Errorf("expected minor %v for %q, got %v", tc.expectSeriesMinor, input, minor)
133+
}
134+
if exact != "" {
135+
t.Errorf("expected no canonical version for %q, got %q", input, exact)
136+
}
137+
}
138+
})
139+
}
140+
}
141+
38142
var _ = Describe("Test download binaries", func() {
39143
var downloadDirectory string
40144
var server *ghttp.Server
@@ -68,11 +172,11 @@ var _ = Describe("Test download binaries", func() {
68172
Expect(actualFiles).To(ConsistOf("some-file"))
69173
})
70174

71-
It("should download v1.32.0 binaries", func(ctx SpecContext) {
175+
It("should download binaries of an exact version", func(ctx SpecContext) {
72176
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "v1.31.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
73177
Expect(err).ToNot(HaveOccurred())
74178

75-
// Verify latest stable version (v1.32.0) was downloaded
179+
// Verify exact version (v1.31.0) was downloaded
76180
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH))
77181
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver")))
78182
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd")))
@@ -86,6 +190,38 @@ var _ = Describe("Test download binaries", func() {
86190
}
87191
Expect(actualFiles).To(ConsistOf("some-file"))
88192
})
193+
194+
It("should download binaries of latest stable version of a release series", func(ctx SpecContext) {
195+
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "1.31", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
196+
Expect(err).ToNot(HaveOccurred())
197+
198+
// Verify stable version (v1.31.4) was downloaded
199+
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.4-%s-%s", runtime.GOOS, runtime.GOARCH))
200+
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver")))
201+
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd")))
202+
Expect(kubectlPath).To(Equal(path.Join(versionDownloadDirectory, "kubectl")))
203+
204+
dirEntries, err := os.ReadDir(versionDownloadDirectory)
205+
Expect(err).ToNot(HaveOccurred())
206+
var actualFiles []string
207+
for _, e := range dirEntries {
208+
actualFiles = append(actualFiles, e.Name())
209+
}
210+
Expect(actualFiles).To(ConsistOf("some-file"))
211+
})
212+
213+
It("should error when the asset version is not a version", func(ctx SpecContext) {
214+
_, _, _, err := downloadBinaryAssets(ctx, downloadDirectory, "wonky", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
215+
Expect(err).To(MatchError(`could not parse "wonky" as version`))
216+
})
217+
218+
It("should error when the asset version is not in the index", func(ctx SpecContext) {
219+
_, _, _, err := downloadBinaryAssets(ctx, downloadDirectory, "v1.5.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
220+
Expect(err).To(MatchError("failed to find envtest binaries for version v1.5.0"))
221+
222+
_, _, _, err = downloadBinaryAssets(ctx, downloadDirectory, "v1.5", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
223+
Expect(err).To(MatchError("failed to find latest stable version from index: index does not have 1.5 stable versions"))
224+
})
89225
})
90226

91227
var (
@@ -100,6 +236,15 @@ var (
100236
"envtest-v1.32.0-linux-s390x.tar.gz": {},
101237
"envtest-v1.32.0-windows-amd64.tar.gz": {},
102238
},
239+
"v1.31.4": map[string]archive{
240+
"envtest-v1.31.4-darwin-amd64.tar.gz": {},
241+
"envtest-v1.31.4-darwin-arm64.tar.gz": {},
242+
"envtest-v1.31.4-linux-amd64.tar.gz": {},
243+
"envtest-v1.31.4-linux-arm64.tar.gz": {},
244+
"envtest-v1.31.4-linux-ppc64le.tar.gz": {},
245+
"envtest-v1.31.4-linux-s390x.tar.gz": {},
246+
"envtest-v1.31.4-windows-amd64.tar.gz": {},
247+
},
103248
"v1.31.0": map[string]archive{
104249
"envtest-v1.31.0-darwin-amd64.tar.gz": {},
105250
"envtest-v1.31.0-darwin-arm64.tar.gz": {},

0 commit comments

Comments
 (0)