Skip to content

Commit f96e891

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 f96e891

File tree

2 files changed

+180
-14
lines changed

2 files changed

+180
-14
lines changed

pkg/envtest/binaries.go

Lines changed: 53 additions & 12 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+
// interpretKubernetesVersion 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. zero values
117+
func interpretKubernetesVersion(s string) (exact string, major, minor uint) {
118+
if v, err := version.ParseSemantic(s); err == nil {
119+
return v.String(), 0, 0
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()
126+
}
127+
}
128+
129+
return "", 0, 0
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,22 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse
124142
}
125143
}
126144

145+
exact, major, minor := interpretKubernetesVersion(binaryAssetsVersion)
146+
127147
var binaryAssetsIndex *index
128-
if binaryAssetsVersion == "" {
148+
if binaryAssetsVersion != "" && exact != "" {
149+
// Look for these specific binaries locally before downloading them from the release index.
150+
// Use the canonical form of the version from here on.
151+
binaryAssetsVersion = "v" + exact
152+
} else if binaryAssetsVersion == "" || major != 0 || minor != 0 {
153+
// Select a stable version from the release index before continuing.
129154
var err error
130155
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL)
131156
if err != nil {
132157
return "", "", "", err
133158
}
134159

135-
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex)
160+
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, major, minor)
136161
if err != nil {
137162
return "", "", "", err
138163
}
@@ -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: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,97 @@ 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 TestInterpretKubernetesVersion(t *testing.T) {
41+
t.Parallel()
42+
43+
testCases := []struct {
44+
name string
45+
inputs []string
46+
47+
expectExact bool
48+
expectSeriesMajor uint
49+
expectSeriesMinor uint
50+
}{
51+
{
52+
name: `SemVer and "v" prefix are exact`,
53+
inputs: []string{
54+
"1.2.3", "v1.2.3", "v1.30.2", "v1.31.0-beta.0", "v1.33.0-alpha.2",
55+
},
56+
expectExact: true,
57+
},
58+
{
59+
name: "leading zeroes are not a version",
60+
inputs: []string{
61+
"01.2.0", "00001.2.3", "1.2.03", "v01.02.0003",
62+
},
63+
},
64+
{
65+
name: "weird stuff is not a version",
66+
inputs: []string{
67+
"asdf", "version", "vegeta4", "the.1", "2ne1", "=7.8.9", "10.x", "*",
68+
"0.0001", "1.00002", "v1.2anything", "1.2.x", "1.2.z", "1.2.*",
69+
},
70+
},
71+
{
72+
name: "one number is not a version",
73+
inputs: []string{
74+
"1", "v1", "v001", "1.", "v1.", "1.x",
75+
},
76+
},
77+
{
78+
name: "two numbers are a release series",
79+
inputs: []string{"0.1", "v0.1"},
80+
81+
expectSeriesMajor: 0,
82+
expectSeriesMinor: 1,
83+
},
84+
{
85+
name: "two numbers are a release series",
86+
inputs: []string{"1.2", "v1.2"},
87+
88+
expectSeriesMajor: 1,
89+
expectSeriesMinor: 2,
90+
},
91+
}
92+
93+
for _, tc := range testCases {
94+
t.Run(tc.name, func(t *testing.T) {
95+
for _, input := range tc.inputs {
96+
exact, major, minor := interpretKubernetesVersion(input)
97+
98+
if tc.expectExact {
99+
if expected := strings.TrimPrefix(input, "v"); exact != expected {
100+
t.Errorf("expected canonical %q for %q, got %q", expected, input, exact)
101+
}
102+
if major != 0 || minor != 0 {
103+
t.Errorf("expected no release series for %q, got (%v, %v)", input, major, minor)
104+
}
105+
return
106+
}
107+
108+
if major != tc.expectSeriesMajor {
109+
t.Errorf("expected major %v for %q, got %v", tc.expectSeriesMajor, input, major)
110+
}
111+
if minor != tc.expectSeriesMinor {
112+
t.Errorf("expected minor %v for %q, got %v", tc.expectSeriesMinor, input, minor)
113+
}
114+
if exact != "" {
115+
t.Errorf("expected no canonical version for %q, got %q", input, exact)
116+
}
117+
}
118+
})
119+
}
120+
}
121+
38122
var _ = Describe("Test download binaries", func() {
39123
var downloadDirectory string
40124
var server *ghttp.Server
@@ -68,11 +152,11 @@ var _ = Describe("Test download binaries", func() {
68152
Expect(actualFiles).To(ConsistOf("some-file"))
69153
})
70154

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

75-
// Verify latest stable version (v1.32.0) was downloaded
159+
// Verify exact version (v1.31.0) was downloaded
76160
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH))
77161
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver")))
78162
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd")))
@@ -86,6 +170,38 @@ var _ = Describe("Test download binaries", func() {
86170
}
87171
Expect(actualFiles).To(ConsistOf("some-file"))
88172
})
173+
174+
It("should download binaries of latest stable version of a release series", func(ctx SpecContext) {
175+
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "1.31", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
176+
Expect(err).ToNot(HaveOccurred())
177+
178+
// Verify stable version (v1.31.4) was downloaded
179+
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.4-%s-%s", runtime.GOOS, runtime.GOARCH))
180+
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver")))
181+
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd")))
182+
Expect(kubectlPath).To(Equal(path.Join(versionDownloadDirectory, "kubectl")))
183+
184+
dirEntries, err := os.ReadDir(versionDownloadDirectory)
185+
Expect(err).ToNot(HaveOccurred())
186+
var actualFiles []string
187+
for _, e := range dirEntries {
188+
actualFiles = append(actualFiles, e.Name())
189+
}
190+
Expect(actualFiles).To(ConsistOf("some-file"))
191+
})
192+
193+
It("should error when the asset version is not a version", func(ctx SpecContext) {
194+
_, _, _, err := downloadBinaryAssets(ctx, downloadDirectory, "wonky", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
195+
Expect(err).To(MatchError("failed to find envtest binaries for version wonky"))
196+
})
197+
198+
It("should error when the asset version is not in the index", func(ctx SpecContext) {
199+
_, _, _, err := downloadBinaryAssets(ctx, downloadDirectory, "v1.5.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
200+
Expect(err).To(MatchError("failed to find envtest binaries for version v1.5.0"))
201+
202+
_, _, _, err = downloadBinaryAssets(ctx, downloadDirectory, "v1.5", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml"))
203+
Expect(err).To(MatchError("failed to find latest stable version from index: index does not have 1.5 stable versions"))
204+
})
89205
})
90206

91207
var (
@@ -100,6 +216,15 @@ var (
100216
"envtest-v1.32.0-linux-s390x.tar.gz": {},
101217
"envtest-v1.32.0-windows-amd64.tar.gz": {},
102218
},
219+
"v1.31.4": map[string]archive{
220+
"envtest-v1.31.4-darwin-amd64.tar.gz": {},
221+
"envtest-v1.31.4-darwin-arm64.tar.gz": {},
222+
"envtest-v1.31.4-linux-amd64.tar.gz": {},
223+
"envtest-v1.31.4-linux-arm64.tar.gz": {},
224+
"envtest-v1.31.4-linux-ppc64le.tar.gz": {},
225+
"envtest-v1.31.4-linux-s390x.tar.gz": {},
226+
"envtest-v1.31.4-windows-amd64.tar.gz": {},
227+
},
103228
"v1.31.0": map[string]archive{
104229
"envtest-v1.31.0-darwin-amd64.tar.gz": {},
105230
"envtest-v1.31.0-darwin-arm64.tar.gz": {},

0 commit comments

Comments
 (0)