-
Notifications
You must be signed in to change notification settings - Fork 1.2k
✨ envtest: search the assets index for latest of a release series #3280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cbandy
wants to merge
2
commits into
kubernetes-sigs:main
Choose a base branch
from
cbandy:envtest-versions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+184
−21
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,10 +32,9 @@ import ( | |
"path" | ||
"path/filepath" | ||
"runtime" | ||
"sort" | ||
"strings" | ||
|
||
"github.com/blang/semver/v4" | ||
"k8s.io/apimachinery/pkg/util/version" | ||
"sigs.k8s.io/yaml" | ||
) | ||
|
||
|
@@ -111,6 +110,25 @@ type archive struct { | |
SelfLink string `json:"selfLink"` | ||
} | ||
|
||
// interpretKubernetesVersion returns: | ||
// 1. the SemVer form of s when it refers to a specific Kubernetes release, or | ||
// 2. the major and minor portions of s when it refers to a release series, or | ||
// 3. zero values | ||
func interpretKubernetesVersion(s string) (exact string, major, minor uint) { | ||
if v, err := version.ParseSemantic(s); err == nil { | ||
return v.String(), 0, 0 | ||
} | ||
|
||
// See two parseable components and nothing else. | ||
if v, err := version.ParseGeneric(s); err == nil && len(v.Components()) == 2 { | ||
if v.String() == strings.TrimPrefix(s, "v") { | ||
return "", v.Major(), v.Minor() | ||
} | ||
} | ||
|
||
return "", 0, 0 | ||
} | ||
|
||
func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAssetsVersion, binaryAssetsIndexURL string) (string, string, string, error) { | ||
if binaryAssetsIndexURL == "" { | ||
binaryAssetsIndexURL = DefaultBinaryAssetsIndexURL | ||
|
@@ -124,15 +142,22 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse | |
} | ||
} | ||
|
||
exact, major, minor := interpretKubernetesVersion(binaryAssetsVersion) | ||
|
||
var binaryAssetsIndex *index | ||
if binaryAssetsVersion == "" { | ||
if binaryAssetsVersion != "" && exact != "" { | ||
// Look for these specific binaries locally before downloading them from the release index. | ||
// Use the canonical form of the version from here on. | ||
binaryAssetsVersion = "v" + exact | ||
} else if binaryAssetsVersion == "" || major != 0 || minor != 0 { | ||
// Select a stable version from the release index before continuing. | ||
var err error | ||
binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL) | ||
if err != nil { | ||
return "", "", "", err | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add an else branch here that returns to cover error cases? |
||
|
||
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex) | ||
binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, major, minor) | ||
if err != nil { | ||
return "", "", "", err | ||
} | ||
|
@@ -252,34 +277,50 @@ func downloadBinaryAssetsArchive(ctx context.Context, index *index, version stri | |
return readBody(resp, out, archiveName, archive.Hash) | ||
} | ||
|
||
func latestStableVersionFromIndex(index *index) (string, error) { | ||
// latestStableVersionFromIndex returns the version with highest [precedence] in index that is not a prerelease. | ||
// When either major or minor are not zero, the returned version will have those major and minor versions. | ||
// Note that the version cannot be limited to 0.0.x this way. | ||
// | ||
// It is an error when there is no appropriate version in index. | ||
// | ||
// [precedence]: https://semver.org/spec/v2.0.0.html#spec-item-11 | ||
func latestStableVersionFromIndex(index *index, major, minor uint) (string, error) { | ||
if len(index.Releases) == 0 { | ||
return "", fmt.Errorf("failed to find latest stable version from index: index is empty") | ||
} | ||
|
||
parsedVersions := []semver.Version{} | ||
var found *version.Version | ||
for releaseVersion := range index.Releases { | ||
v, err := semver.ParseTolerant(releaseVersion) | ||
v, err := version.ParseSemantic(releaseVersion) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to parse version %q: %w", releaseVersion, err) | ||
} | ||
|
||
// Filter out pre-releases. | ||
if len(v.Pre) > 0 { | ||
if len(v.PreRelease()) > 0 { | ||
continue | ||
} | ||
|
||
// Filter on release series, if any. | ||
if (major != 0 || minor != 0) && (v.Major() != major || v.Minor() != minor) { | ||
continue | ||
} | ||
|
||
parsedVersions = append(parsedVersions, v) | ||
if found == nil || v.GreaterThan(found) { | ||
found = v | ||
} | ||
} | ||
|
||
if len(parsedVersions) == 0 { | ||
return "", fmt.Errorf("failed to find latest stable version from index: index does not have stable versions") | ||
if found == nil { | ||
search := "any" | ||
if major != 0 || minor != 0 { | ||
search = fmt.Sprint(major, ".", minor) | ||
} | ||
|
||
return "", fmt.Errorf("failed to find latest stable version from index: index does not have %s stable versions", search) | ||
} | ||
|
||
sort.Slice(parsedVersions, func(i, j int) bool { | ||
return parsedVersions[i].GT(parsedVersions[j]) | ||
}) | ||
return "v" + parsedVersions[0].String(), nil | ||
return "v" + found.String(), nil | ||
} | ||
|
||
func getIndex(ctx context.Context, indexURL string) (*index, error) { | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,13 +28,97 @@ import ( | |
"os" | ||
"path" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
"github.com/onsi/gomega/ghttp" | ||
"sigs.k8s.io/yaml" | ||
) | ||
|
||
func TestInterpretKubernetesVersion(t *testing.T) { | ||
t.Parallel() | ||
|
||
testCases := []struct { | ||
name string | ||
inputs []string | ||
|
||
expectExact bool | ||
expectSeriesMajor uint | ||
expectSeriesMinor uint | ||
}{ | ||
{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a test case for the empty string |
||
name: `SemVer and "v" prefix are exact`, | ||
inputs: []string{ | ||
"1.2.3", "v1.2.3", "v1.30.2", "v1.31.0-beta.0", "v1.33.0-alpha.2", | ||
}, | ||
expectExact: true, | ||
}, | ||
{ | ||
name: "leading zeroes are not a version", | ||
inputs: []string{ | ||
"01.2.0", "00001.2.3", "1.2.03", "v01.02.0003", | ||
}, | ||
}, | ||
{ | ||
name: "weird stuff is not a version", | ||
inputs: []string{ | ||
"asdf", "version", "vegeta4", "the.1", "2ne1", "=7.8.9", "10.x", "*", | ||
"0.0001", "1.00002", "v1.2anything", "1.2.x", "1.2.z", "1.2.*", | ||
}, | ||
}, | ||
{ | ||
name: "one number is not a version", | ||
inputs: []string{ | ||
"1", "v1", "v001", "1.", "v1.", "1.x", | ||
}, | ||
}, | ||
{ | ||
name: "two numbers are a release series", | ||
inputs: []string{"0.1", "v0.1"}, | ||
|
||
expectSeriesMajor: 0, | ||
expectSeriesMinor: 1, | ||
}, | ||
{ | ||
name: "two numbers are a release series", | ||
inputs: []string{"1.2", "v1.2"}, | ||
|
||
expectSeriesMajor: 1, | ||
expectSeriesMinor: 2, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
for _, input := range tc.inputs { | ||
exact, major, minor := interpretKubernetesVersion(input) | ||
|
||
if tc.expectExact { | ||
if expected := strings.TrimPrefix(input, "v"); exact != expected { | ||
t.Errorf("expected canonical %q for %q, got %q", expected, input, exact) | ||
} | ||
if major != 0 || minor != 0 { | ||
t.Errorf("expected no release series for %q, got (%v, %v)", input, major, minor) | ||
} | ||
continue | ||
} | ||
|
||
if major != tc.expectSeriesMajor { | ||
t.Errorf("expected major %v for %q, got %v", tc.expectSeriesMajor, input, major) | ||
} | ||
if minor != tc.expectSeriesMinor { | ||
t.Errorf("expected minor %v for %q, got %v", tc.expectSeriesMinor, input, minor) | ||
} | ||
if exact != "" { | ||
t.Errorf("expected no canonical version for %q, got %q", input, exact) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
var _ = Describe("Test download binaries", func() { | ||
var downloadDirectory string | ||
var server *ghttp.Server | ||
|
@@ -68,11 +152,11 @@ var _ = Describe("Test download binaries", func() { | |
Expect(actualFiles).To(ConsistOf("some-file")) | ||
}) | ||
|
||
It("should download v1.32.0 binaries", func(ctx SpecContext) { | ||
It("should download binaries of an exact version", func(ctx SpecContext) { | ||
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "v1.31.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
// Verify latest stable version (v1.32.0) was downloaded | ||
// Verify exact version (v1.31.0) was downloaded | ||
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)) | ||
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver"))) | ||
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd"))) | ||
|
@@ -86,6 +170,38 @@ var _ = Describe("Test download binaries", func() { | |
} | ||
Expect(actualFiles).To(ConsistOf("some-file")) | ||
}) | ||
|
||
It("should download binaries of latest stable version of a release series", func(ctx SpecContext) { | ||
cbandy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
apiServerPath, etcdPath, kubectlPath, err := downloadBinaryAssets(ctx, downloadDirectory, "1.31", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) | ||
Expect(err).ToNot(HaveOccurred()) | ||
|
||
// Verify stable version (v1.31.4) was downloaded | ||
versionDownloadDirectory := path.Join(downloadDirectory, fmt.Sprintf("1.31.4-%s-%s", runtime.GOOS, runtime.GOARCH)) | ||
Expect(apiServerPath).To(Equal(path.Join(versionDownloadDirectory, "kube-apiserver"))) | ||
Expect(etcdPath).To(Equal(path.Join(versionDownloadDirectory, "etcd"))) | ||
Expect(kubectlPath).To(Equal(path.Join(versionDownloadDirectory, "kubectl"))) | ||
|
||
dirEntries, err := os.ReadDir(versionDownloadDirectory) | ||
Expect(err).ToNot(HaveOccurred()) | ||
var actualFiles []string | ||
for _, e := range dirEntries { | ||
actualFiles = append(actualFiles, e.Name()) | ||
} | ||
Expect(actualFiles).To(ConsistOf("some-file")) | ||
}) | ||
|
||
It("should error when the asset version is not a version", func(ctx SpecContext) { | ||
_, _, _, err := downloadBinaryAssets(ctx, downloadDirectory, "wonky", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) | ||
Expect(err).To(MatchError("failed to find envtest binaries for version wonky")) | ||
}) | ||
|
||
It("should error when the asset version is not in the index", func(ctx SpecContext) { | ||
_, _, _, err := downloadBinaryAssets(ctx, downloadDirectory, "v1.5.0", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) | ||
Expect(err).To(MatchError("failed to find envtest binaries for version v1.5.0")) | ||
|
||
_, _, _, err = downloadBinaryAssets(ctx, downloadDirectory, "v1.5", fmt.Sprintf("http://%s/%s", server.Addr(), "envtest-releases.yaml")) | ||
Expect(err).To(MatchError("failed to find latest stable version from index: index does not have 1.5 stable versions")) | ||
}) | ||
}) | ||
|
||
var ( | ||
|
@@ -100,6 +216,15 @@ var ( | |
"envtest-v1.32.0-linux-s390x.tar.gz": {}, | ||
"envtest-v1.32.0-windows-amd64.tar.gz": {}, | ||
}, | ||
"v1.31.4": map[string]archive{ | ||
"envtest-v1.31.4-darwin-amd64.tar.gz": {}, | ||
"envtest-v1.31.4-darwin-arm64.tar.gz": {}, | ||
"envtest-v1.31.4-linux-amd64.tar.gz": {}, | ||
"envtest-v1.31.4-linux-arm64.tar.gz": {}, | ||
"envtest-v1.31.4-linux-ppc64le.tar.gz": {}, | ||
"envtest-v1.31.4-linux-s390x.tar.gz": {}, | ||
"envtest-v1.31.4-windows-amd64.tar.gz": {}, | ||
}, | ||
"v1.31.0": map[string]archive{ | ||
"envtest-v1.31.0-darwin-amd64.tar.gz": {}, | ||
"envtest-v1.31.0-darwin-arm64.tar.gz": {}, | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.