Skip to content

Commit b536944

Browse files
committed
envtest: search the assets index for latest of a release series
Signed-off-by: Chris Bandy <[email protected]>
1 parent 95c76a7 commit b536944

File tree

2 files changed

+202
-16
lines changed

2 files changed

+202
-16
lines changed

pkg/envtest/binaries.go

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
"path"
3333
"path/filepath"
3434
"runtime"
35-
"sort"
3635
"strings"
3736

3837
"k8s.io/apimachinery/pkg/util/version"
@@ -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
@@ -124,15 +142,23 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse
124142
}
125143
}
126144

145+
exact, major, minor, err := parseKubernetesVersion(binaryAssetsVersion)
146+
127147
var binaryAssetsIndex *index
128-
if binaryAssetsVersion == "" {
129-
var err error
148+
if binaryAssetsVersion != "" && err != nil {
149+
return "", "", "", err
150+
} else if 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+
} else if binaryAssetsVersion == "" || major != 0 || minor != 0 {
155+
// Select a stable version from the release index before continuing.
130156
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL)
131157
if err != nil {
132158
return "", "", "", err
133159
}
134160

135-
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex)
161+
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, major, minor)
136162
if err != nil {
137163
return "", "", "", err
138164
}
@@ -158,7 +184,6 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse
158184

159185
// Get Index if we didn't have to get it above to get the latest stable version.
160186
if binaryAssetsIndex == nil {
161-
var err error
162187
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL)
163188
if err != nil {
164189
return "", "", "", err
@@ -252,12 +277,19 @@ func downloadBinaryAssetsArchive(ctx context.Context, index *index, version stri
252277
return readBody(resp, out, archiveName, archive.Hash)
253278
}
254279

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

260-
parsedVersions := []*version.Version{}
292+
var found *version.Version
261293
for releaseVersion := range index.Releases {
262294
v, err := version.ParseSemantic(releaseVersion)
263295
if err != nil {
@@ -269,17 +301,26 @@ func latestStableVersionFromIndex(index *index) (string, error) {
269301
continue
270302
}
271303

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

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

279-
sort.Slice(parsedVersions, func(i, j int) bool {
280-
return parsedVersions[i].GreaterThan(parsedVersions[j])
281-
})
282-
return "v" + parsedVersions[0].String(), nil
323+
return "v" + found.String(), nil
283324
}
284325

285326
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)