Skip to content
21 changes: 18 additions & 3 deletions components/kubernetes/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member

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 false when undefined (which is the case for all versions defined in the KindConfig map? I would like to prevent an external dependency if it's not necessary

// Use public Docker Hub for latest/dynamic versions not available in internal mirror
nodeImage = fmt.Sprintf("docker.io/%s:%s", kindNodeImageName, kindVersionConfig.NodeImageVersion)
Copy link
Member

Choose a reason for hiding this comment

The 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.
In any case using docker.io is not an option because we will get rate limited very soon. If that's for local development only, this is acceptable, not in the CI

} 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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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...,
)
Expand Down
175 changes: 173 additions & 2 deletions components/kubernetes/kind_versions.go
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
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The 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"
}

// 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"
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ? 🤔

https://github.com/kubernetes-sigs/kind/releases/latest

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand All @@ -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
}

Expand Down
81 changes: 81 additions & 0 deletions components/kubernetes/kind_versions_test.go
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)
}
Loading