From bd4b21dd58d51e3196804341d6a6d3c4b15a4dab Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Thu, 17 Jul 2025 14:19:08 -0500 Subject: [PATCH 1/2] envtest: search the assets index for latest of a release series Signed-off-by: Chris Bandy --- pkg/envtest/binaries.go | 12 +++++++++--- pkg/envtest/binaries_test.go | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pkg/envtest/binaries.go b/pkg/envtest/binaries.go index 4c9b1dae38..8b1df61fbe 100644 --- a/pkg/envtest/binaries.go +++ b/pkg/envtest/binaries.go @@ -125,14 +125,14 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse } var binaryAssetsIndex *index - if binaryAssetsVersion == "" { + if binaryAssetsVersion == "" || strings.Count(binaryAssetsVersion, ".") < 2 { var err error binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL) if err != nil { return "", "", "", err } - binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex) + binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, binaryAssetsVersion) if err != nil { return "", "", "", err } @@ -252,13 +252,19 @@ func downloadBinaryAssetsArchive(ctx context.Context, index *index, version stri return readBody(resp, out, archiveName, archive.Hash) } -func latestStableVersionFromIndex(index *index) (string, error) { +func latestStableVersionFromIndex(index *index, prefix string) (string, error) { if len(index.Releases) == 0 { return "", fmt.Errorf("failed to find latest stable version from index: index is empty") } + prefix = strings.TrimPrefix(prefix, "v") parsedVersions := []semver.Version{} for releaseVersion := range index.Releases { + // Filter on prefix. + if !strings.HasPrefix(strings.TrimPrefix(releaseVersion, "v"), prefix) { + continue + } + v, err := semver.ParseTolerant(releaseVersion) if err != nil { return "", fmt.Errorf("failed to parse version %q: %w", releaseVersion, err) diff --git a/pkg/envtest/binaries_test.go b/pkg/envtest/binaries_test.go index e5865cbc70..290187c30d 100644 --- a/pkg/envtest/binaries_test.go +++ b/pkg/envtest/binaries_test.go @@ -68,11 +68,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 +86,25 @@ 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) { + 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")) + }) }) var ( @@ -100,6 +119,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": {}, From f4d164bbb4bdda9fcd5e7b7286d357b9a711a332 Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Fri, 18 Jul 2025 18:33:44 -0500 Subject: [PATCH 2/2] envtest: accept entire "ranges" of asset versions --- pkg/envtest/binaries.go | 57 +++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/pkg/envtest/binaries.go b/pkg/envtest/binaries.go index 8b1df61fbe..d192fcba11 100644 --- a/pkg/envtest/binaries.go +++ b/pkg/envtest/binaries.go @@ -124,15 +124,54 @@ func downloadBinaryAssets(ctx context.Context, binaryAssetsDirectory, binaryAsse } } + var requestedRange semver.Range + if binaryAssetsVersion != "" { + binaryAssetsVersion = strings.TrimPrefix(binaryAssetsVersion, "v") + parsedVersion, errV := semver.ParseTolerant(binaryAssetsVersion) + parsedRange, errR := semver.ParseRange(binaryAssetsVersion) + + switch { + // When an exact version is requested, don't look for it in the release index. + case errV == nil && parsedVersion.String() == binaryAssetsVersion: + requestedRange = nil + + // When the version looks like a range, apply it to the index. + case errR == nil: + requestedRange = parsedRange + + // When the version isn't exact, turn it into a range. + // + // The `setup-envtest` tool interprets a partial version to mean "latest stable with that prefix." + // For example, "1" and "1.2" are akin to "1.x.x" and "1.2.x" in [semver.ParseRange]. + // [semver.ParseTolerant] fills in missing minor or patch with "0", so this replaces those with "x". + // + // That *should* produce a valid range. If it doesn't, use the original value and skip the index. + case errV == nil: + suffix := strings.TrimPrefix(parsedVersion.FinalizeVersion(), binaryAssetsVersion) + suffix = strings.ReplaceAll(suffix, "0", "x") + parsedRange, errR = semver.ParseRange(binaryAssetsVersion + suffix) + + if errR == nil { + requestedRange = parsedRange + } else { + requestedRange = nil + } + } + } else { + // When no version is requested, look for one in the release index. + requestedRange = semver.MustParseRange(">0.0.0") + } + + // When a range a versions is requested, select one from the release index. var binaryAssetsIndex *index - if binaryAssetsVersion == "" || strings.Count(binaryAssetsVersion, ".") < 2 { + if requestedRange != nil { var err error binaryAssetsIndex, err = getIndex(ctx, binaryAssetsIndexURL) if err != nil { return "", "", "", err } - binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, binaryAssetsVersion) + binaryAssetsVersion, err = latestStableVersionFromIndex(binaryAssetsIndex, requestedRange) if err != nil { return "", "", "", err } @@ -219,6 +258,8 @@ func fileExists(path string) bool { } func downloadBinaryAssetsArchive(ctx context.Context, index *index, version string, out io.Writer) error { + version = "v" + strings.TrimPrefix(version, "v") + archives, ok := index.Releases[version] if !ok { return fmt.Errorf("failed to find envtest binaries for version %s", version) @@ -252,26 +293,20 @@ func downloadBinaryAssetsArchive(ctx context.Context, index *index, version stri return readBody(resp, out, archiveName, archive.Hash) } -func latestStableVersionFromIndex(index *index, prefix string) (string, error) { +func latestStableVersionFromIndex(index *index, satisfying semver.Range) (string, error) { if len(index.Releases) == 0 { return "", fmt.Errorf("failed to find latest stable version from index: index is empty") } - prefix = strings.TrimPrefix(prefix, "v") parsedVersions := []semver.Version{} for releaseVersion := range index.Releases { - // Filter on prefix. - if !strings.HasPrefix(strings.TrimPrefix(releaseVersion, "v"), prefix) { - continue - } - v, err := semver.ParseTolerant(releaseVersion) if err != nil { return "", fmt.Errorf("failed to parse version %q: %w", releaseVersion, err) } - // Filter out pre-releases. - if len(v.Pre) > 0 { + // Filter out pre-releases and undesirable versions. + if len(v.Pre) > 0 || !satisfying(v) { continue }