-
Notifications
You must be signed in to change notification settings - Fork 0
Add dynamic resolution of kind version #1700
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
base: main
Are you sure you want to change the base?
Changes from all commits
f02290c
a5dffc7
f72f2aa
57e5b9a
15a8ed0
af88a76
6038f3c
0f5c05d
2819f6f
1999fd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,7 +66,14 @@ func NewKindClusterWithConfig(env config.Env, vm *remote.Host, name string, kube | |
| return err | ||
| } | ||
|
|
||
| nodeImage := fmt.Sprintf("%s/%s:%s", env.InternalDockerhubMirror(), kindNodeImageName, kindVersionConfig.NodeImageVersion) | ||
| var nodeImage string | ||
| if kindVersionConfig.UsePublicRegistry { | ||
| // Use public Docker Hub for latest/dynamic versions not available in internal mirror | ||
| nodeImage = fmt.Sprintf("docker.io/%s:%s", kindNodeImageName, kindVersionConfig.NodeImageVersion) | ||
|
Member
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. Using the internal mirror should be transparent, you can target an arbitrary dockerhub image through the mirror. If it is not there yet it should be pulled. |
||
| } else { | ||
| // Use internal mirror for cached/static versions | ||
| nodeImage = fmt.Sprintf("%s/%s:%s", env.InternalDockerhubMirror(), kindNodeImageName, kindVersionConfig.NodeImageVersion) | ||
| } | ||
| createCluster, err := runner.Command( | ||
| commonEnvironment.CommonNamer().ResourceName("kind-create-cluster"), | ||
| &command.Args{ | ||
|
|
@@ -129,7 +136,14 @@ func NewLocalKindCluster(env config.Env, name string, kubeVersion string, opts . | |
| return err | ||
| } | ||
|
|
||
| nodeImage := fmt.Sprintf("%s/%s:%s", env.InternalDockerhubMirror(), kindNodeImageName, kindVersionConfig.NodeImageVersion) | ||
| var nodeImage string | ||
| if kindVersionConfig.UsePublicRegistry { | ||
| // Use public Docker Hub for latest/dynamic versions not available in internal mirror | ||
| nodeImage = fmt.Sprintf("docker.io/%s:%s", kindNodeImageName, kindVersionConfig.NodeImageVersion) | ||
| } else { | ||
| // Use internal mirror for cached/static versions | ||
| nodeImage = fmt.Sprintf("%s/%s:%s", env.InternalDockerhubMirror(), kindNodeImageName, kindVersionConfig.NodeImageVersion) | ||
| } | ||
| createCluster, err := runner.Command( | ||
| commonEnvironment.CommonNamer().ResourceName("kind-create-cluster"), | ||
| &command.Args{ | ||
|
|
@@ -166,10 +180,11 @@ func InstallKindBinary(env config.Env, vm *remote.Host, kindVersion string, opts | |
| if kindArch == os.AMD64Arch { | ||
| kindArch = "amd64" | ||
| } | ||
| kindBinaryURL := fmt.Sprintf("https://kind.sigs.k8s.io/dl/%s/kind-linux-%s", kindVersion, kindArch) | ||
| return vm.OS.Runner().Command( | ||
| env.CommonNamer().ResourceName("kind-install"), | ||
| &command.Args{ | ||
| Create: pulumi.Sprintf(`curl --retry 10 -fsSLo ./kind "https://kind.sigs.k8s.io/dl/%s/kind-linux-%s" && sudo install kind /usr/local/bin/kind`, kindVersion, kindArch), | ||
| Create: pulumi.Sprintf(`curl --retry 10 -fsSLo ./kind "%s" && sudo install kind /usr/local/bin/kind`, kindBinaryURL), | ||
| }, | ||
| opts..., | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,37 @@ | ||
| package kubernetes | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "regexp" | ||
| "sort" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/Masterminds/semver" | ||
| ) | ||
|
|
||
| // KindConfig contains the kind version and the kind node image to use | ||
| type KindConfig struct { | ||
| KindVersion string | ||
| NodeImageVersion string | ||
| KindVersion string | ||
| NodeImageVersion string | ||
| KubeVersion string // Clean Kubernetes version for semantic parsing | ||
| UsePublicRegistry bool // If true, pull from docker.io instead of internal mirror | ||
| } | ||
|
|
||
| // DockerHubTag represents a tag from Docker Hub API | ||
| type DockerHubTag struct { | ||
| Name string `json:"name"` | ||
| Digest string `json:"digest"` | ||
| FullSize int64 `json:"full_size"` | ||
| TagStatus string `json:"tag_status"` | ||
| } | ||
|
|
||
| // DockerHubResponse represents the response from Docker Hub API | ||
| type DockerHubResponse struct { | ||
| Results []DockerHubTag `json:"results"` | ||
| Next string `json:"next"` | ||
| } | ||
|
|
||
| // Source: https://github.com/kubernetes-sigs/kind/releases | ||
|
|
@@ -86,8 +107,156 @@ var kubeToKindVersion = map[string]KindConfig{ | |
| }, | ||
| } | ||
|
|
||
| // getKindVersionForKubernetes determines the appropriate Kind version for a given Kubernetes version | ||
| // Based on Kind release compatibility: https://github.com/kubernetes-sigs/kind/releases | ||
| // Used as fallback if dynamic resolution fails | ||
| func getKindVersionForKubernetes(kubeVersion *semver.Version) string { | ||
|
Member
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. Why not using the map just above that was exactly made to do that AFAIU? |
||
| major := kubeVersion.Major() | ||
| minor := kubeVersion.Minor() | ||
|
|
||
| // For Kubernetes 1.34+, use Kind v0.30.0+ | ||
| if major == 1 && minor >= 34 { | ||
| return "v0.30.0" | ||
| } | ||
|
|
||
| // For older versions, use Kind v0.26.0 | ||
| if major == 1 && minor >= 30 { | ||
| return "v0.26.0" | ||
| } | ||
|
|
||
| // For very old versions, use an older Kind version | ||
| return "v0.22.0" | ||
frank-spano marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // GitHubRelease represents a GitHub release | ||
| type GitHubRelease struct { | ||
| TagName string `json:"tag_name"` | ||
| Draft bool `json:"draft"` | ||
| PreRelease bool `json:"prerelease"` | ||
| } | ||
|
|
||
| // getLatestKindVersionDynamic fetches the latest Kind version from GitHub releases API | ||
| func getLatestKindVersionDynamic() (string, error) { | ||
| client := &http.Client{Timeout: 30 * time.Second} | ||
|
|
||
| // Fetch releases from GitHub API | ||
| githubURL := "https://api.github.com/repos/kubernetes-sigs/kind/releases" | ||
|
Contributor
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. have you evaluate this link option ? possibly it results in some alpha version or what and so we still need to check whole list, but may be it solves our needs ? 🤔
Contributor
Author
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. hmm it does redirect to the latest version, which I should think probably be a proper release version? On the other hand, I don't think iterating through the list of releases is that big of a deal. Let me think about it, maybe we can the link you provided, and if it doesn't resolve to a proper release version we can call the api for all releases.
Contributor
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. yeah that would be a good in-between solution. |
||
|
|
||
| resp, err := client.Get(githubURL) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to fetch GitHub releases: %v", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return "", fmt.Errorf("gitHub API returned status %d", resp.StatusCode) | ||
| } | ||
|
|
||
| var releases []GitHubRelease | ||
| if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { | ||
| return "", fmt.Errorf("failed to decode GitHub response: %v", err) | ||
| } | ||
|
|
||
| // Find the latest non-draft, non-prerelease version | ||
| versionRegex := regexp.MustCompile(`^v\d+\.\d+\.\d+$`) | ||
| var versions []*semver.Version | ||
|
|
||
| for _, release := range releases { | ||
| if release.Draft || release.PreRelease { | ||
| continue | ||
| } | ||
|
|
||
| if versionRegex.MatchString(release.TagName) { | ||
| if version, err := semver.NewVersion(release.TagName); err == nil { | ||
| versions = append(versions, version) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if len(versions) == 0 { | ||
| return "", fmt.Errorf("no valid Kind versions found in GitHub releases") | ||
| } | ||
|
|
||
| sort.Sort(sort.Reverse(semver.Collection(versions))) | ||
| latestVersion := versions[0] | ||
|
|
||
| fmt.Printf("Found %d valid Kind versions, latest is: %s\n", len(versions), latestVersion.String()) | ||
| return "v" + latestVersion.String(), nil | ||
| } | ||
|
|
||
| // getLatestKindVersion fetches the latest Kubernetes version from Docker Hub | ||
| func getLatestKindVersionConfig() (*KindConfig, error) { | ||
| client := &http.Client{Timeout: 30 * time.Second} | ||
|
|
||
| // Fetch tags from Docker Hub API | ||
| dockerHubURL := "https://hub.docker.com/v2/repositories/kindest/node/tags?page_size=100" | ||
| resp, err := client.Get(dockerHubURL) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to fetch Docker Hub tags: %v", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("docker Hub API returned status %d", resp.StatusCode) | ||
| } | ||
|
|
||
| var dockerResp DockerHubResponse | ||
| if err := json.NewDecoder(resp.Body).Decode(&dockerResp); err != nil { | ||
| return nil, fmt.Errorf("failed to decode Docker Hub response: %v", err) | ||
| } | ||
|
|
||
| // Filter and sort versions - look for active tags | ||
| kubeVersionRegex := regexp.MustCompile(`^v(\d+\.\d+\.\d+)$`) | ||
| var versions []*semver.Version | ||
| tagToDigest := make(map[string]string) | ||
|
|
||
| for _, tag := range dockerResp.Results { | ||
| // Only process active tags | ||
| if tag.TagStatus != "active" { | ||
| continue | ||
| } | ||
|
|
||
| matches := kubeVersionRegex.FindStringSubmatch(tag.Name) | ||
| if len(matches) >= 2 { | ||
| if version, err := semver.NewVersion(matches[1]); err == nil { | ||
| versions = append(versions, version) | ||
| // Create full tag with digest (format: v1.33.2@sha256:...) | ||
| fullTag := fmt.Sprintf("%s@%s", tag.Name, tag.Digest) | ||
| tagToDigest[version.String()] = fullTag | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if len(versions) == 0 { | ||
| return nil, fmt.Errorf("no valid active Kubernetes versions found in Docker Hub") | ||
| } | ||
|
|
||
| sort.Sort(sort.Reverse(semver.Collection(versions))) | ||
| latestVersion := versions[0] | ||
| fullTag := tagToDigest[latestVersion.String()] | ||
|
|
||
| // Attempt to use latest kind version | ||
| kindVersion, err := getLatestKindVersionDynamic() | ||
| if err != nil { | ||
| kindVersion = getKindVersionForKubernetes(latestVersion) | ||
| } | ||
| fmt.Printf("Selected Kind version %s for Kubernetes %s\n", kindVersion, latestVersion.String()) | ||
|
|
||
| return &KindConfig{ | ||
| KindVersion: kindVersion, | ||
| NodeImageVersion: fullTag, | ||
| KubeVersion: latestVersion.String(), // Clean version for semantic parsing | ||
| UsePublicRegistry: true, // Latest versions must be pulled from Docker Hub | ||
| }, nil | ||
| } | ||
|
|
||
| // GetKindVersionConfig returns the kind version and the kind node image to use based on kubernetes version | ||
| func GetKindVersionConfig(kubeVersion string) (*KindConfig, error) { | ||
| // Handle "latest" as a special case | ||
| if kubeVersion == "latest" { | ||
| return getLatestKindVersionConfig() | ||
|
Member
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 catch the error here, and just raise a warning in case of failure in the child methods? |
||
| } | ||
|
|
||
| kubeSemVer, err := semver.NewVersion(kubeVersion) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -98,6 +267,8 @@ func GetKindVersionConfig(kubeVersion string) (*KindConfig, error) { | |
| return nil, fmt.Errorf("unsupported kubernetes version. Supported versions are %s", strings.Join(kubeSupportedVersions(), ", ")) | ||
| } | ||
|
|
||
| // Ensure KubeVersion is populated for static configs too | ||
| kindVersionConfig.KubeVersion = kubeVersion | ||
| return &kindVersionConfig, nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package kubernetes | ||
|
|
||
| import ( | ||
| "regexp" | ||
| "testing" | ||
|
|
||
| "github.com/Masterminds/semver" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestGetKindVersionConfig(t *testing.T) { | ||
| t.Run("existing version", func(t *testing.T) { | ||
| config, err := GetKindVersionConfig("1.32.0") | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "v0.26.0", config.KindVersion) | ||
| assert.Contains(t, config.NodeImageVersion, "v1.32.0@sha256:") | ||
| }) | ||
|
|
||
| t.Run("latest version", func(t *testing.T) { | ||
| config, err := GetKindVersionConfig("latest") | ||
| require.NoError(t, err) | ||
|
|
||
| // Should return a valid Kind version | ||
| assert.Regexp(t, regexp.MustCompile(`^v\d+\.\d+\.\d+$`), config.KindVersion) | ||
|
|
||
| // Should return a version with SHA digest | ||
| assert.Regexp(t, regexp.MustCompile(`^v\d+\.\d+\.\d+@sha256:[a-f0-9]{64}$`), config.NodeImageVersion) | ||
|
|
||
| // Latest should be higher than any hardcoded version | ||
| assert.True(t, config.NodeImageVersion >= "v1.32.0", "Latest version should be >= v1.32.0") | ||
|
|
||
| t.Logf("Latest Kubernetes version: %s", config.NodeImageVersion) | ||
| }) | ||
|
|
||
| t.Run("invalid version", func(t *testing.T) { | ||
| _, err := GetKindVersionConfig("invalid") | ||
| assert.Error(t, err) | ||
| }) | ||
|
|
||
| t.Run("unsupported version", func(t *testing.T) { | ||
| _, err := GetKindVersionConfig("999.999.999") | ||
| assert.Error(t, err) | ||
| }) | ||
| } | ||
|
|
||
| func TestGetLatestKindVersionConfig(t *testing.T) { | ||
| config, err := getLatestKindVersionConfig() | ||
| require.NoError(t, err) | ||
|
|
||
| // Should return a valid Kind version | ||
| assert.Regexp(t, regexp.MustCompile(`^v\d+\.\d+\.\d+$`), config.KindVersion) | ||
|
|
||
| // Should return a Kubernetes version with SHA digest | ||
| assert.Regexp(t, regexp.MustCompile(`^v\d+\.\d+\.\d+@sha256:[a-f0-9]{64}$`), config.NodeImageVersion) | ||
|
|
||
| t.Logf("Fetched latest: %s with Kind version: %s", config.NodeImageVersion, config.KindVersion) | ||
| } | ||
|
|
||
| func TestGetLatestKindVersionDynamic(t *testing.T) { | ||
| t.Skip("Skipping test that requires network access") | ||
|
|
||
| version, err := getLatestKindVersionDynamic() | ||
| assert.NoError(t, err) | ||
| assert.NotEmpty(t, version) | ||
| assert.Regexp(t, `^v\d+\.\d+\.\d+$`, version, "Version should match semver format with 'v' prefix") | ||
|
|
||
| t.Logf("Dynamic Kind version: %s", version) | ||
| } | ||
|
|
||
| func TestGetKindVersionForKubernetes(t *testing.T) { | ||
| // Test the static version mapping function | ||
| kubeVersion, err := semver.NewVersion("1.30.0") | ||
| require.NoError(t, err) | ||
|
|
||
| kindVersion := getKindVersionForKubernetes(kubeVersion) | ||
| assert.NotEmpty(t, kindVersion) | ||
| assert.Regexp(t, `^v\d+\.\d+\.\d+$`, kindVersion, "Kind version should match semver format with 'v' prefix") | ||
|
|
||
| t.Logf("Static Kind version for k8s %s: %s", kubeVersion.String(), kindVersion) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you confirm this is always
falsewhen undefined (which is the case for all versions defined in theKindConfigmap? I would like to prevent an external dependency if it's not necessary