diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a0714bec..23b7f9139 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,6 +63,11 @@ repos: language: system files: "^devbox.(yaml|lock)$" pass_filenames: false + - id: check-coredns-versions + name: check-coredns-versions + entry: make coredns.sync + language: system + files: "^api/versions/coredns.go$" - repo: https://github.com/tekwizely/pre-commit-golang rev: v1.0.0-rc.1 hooks: diff --git a/api/go.mod b/api/go.mod index f70593750..00d5b1aab 100644 --- a/api/go.mod +++ b/api/go.mod @@ -11,9 +11,11 @@ replace github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/c require ( github.com/aws/aws-sdk-go v1.55.5 + github.com/blang/semver/v4 v4.0.0 github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/common v0.7.0 github.com/nutanix-cloud-native/prism-go-client v0.5.1 github.com/onsi/gomega v1.34.2 + github.com/stretchr/testify v1.9.0 k8s.io/api v0.30.5 k8s.io/apiextensions-apiserver v0.30.5 k8s.io/apimachinery v0.30.5 @@ -24,7 +26,6 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect @@ -51,6 +52,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/common v0.45.0 // indirect diff --git a/api/versions/coredns.go b/api/versions/coredns.go new file mode 100644 index 000000000..0f05bc000 --- /dev/null +++ b/api/versions/coredns.go @@ -0,0 +1,77 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by script; DO NOT EDIT. Run 'make coredns.sync' instead + +package versions + +import ( + "fmt" + "maps" + + "github.com/blang/semver/v4" +) + +// Kubernetes versions +const ( + Kubernetes_V1_22 = "v1.22" + Kubernetes_V1_23 = "v1.23" + Kubernetes_V1_24 = "v1.24" + Kubernetes_V1_25 = "v1.25" + Kubernetes_V1_26 = "v1.26" + Kubernetes_V1_27 = "v1.27" + Kubernetes_V1_28 = "v1.28" + Kubernetes_V1_29 = "v1.29" + Kubernetes_V1_30 = "v1.30" + Kubernetes_V1_31 = "v1.31" +) + +// CoreDNS versions +const ( + CoreDNS_V1_8_4 = "v1.8.4" + CoreDNS_V1_8_6 = "v1.8.6" + CoreDNS_V1_9_3 = "v1.9.3" + CoreDNS_V1_10_1 = "v1.10.1" + CoreDNS_V1_11_1 = "v1.11.1" + CoreDNS_V1_11_3 = "v1.11.3" +) + +// kubernetesToCoreDNSVersion maps Kubernetes versions to CoreDNS versions. +// This map is unexported to prevent external modification. +var kubernetesToCoreDNSVersion = map[string]string{ + Kubernetes_V1_22: CoreDNS_V1_8_4, + Kubernetes_V1_23: CoreDNS_V1_8_6, + Kubernetes_V1_24: CoreDNS_V1_8_6, + Kubernetes_V1_25: CoreDNS_V1_9_3, + Kubernetes_V1_26: CoreDNS_V1_9_3, + Kubernetes_V1_27: CoreDNS_V1_10_1, + Kubernetes_V1_28: CoreDNS_V1_10_1, + Kubernetes_V1_29: CoreDNS_V1_11_1, + Kubernetes_V1_30: CoreDNS_V1_11_3, + Kubernetes_V1_31: CoreDNS_V1_11_3, +} + +// GetCoreDNSVersion returns the CoreDNS version for a given Kubernetes version. +// It accepts versions with or without the "v" prefix and handles full semver versions. +// The function maps based on the major and minor versions (e.g., "v1.27"). +// If the Kubernetes version is not found, it returns an empty string and false. +func GetCoreDNSVersion(kubernetesVersion string) (string, bool) { + // Parse the version using semver + v, err := semver.ParseTolerant(kubernetesVersion) + if err != nil { + return "", false + } + + // Construct "vMAJOR.MINOR" format + majorMinor := fmt.Sprintf("v%d.%d", v.Major, v.Minor) + + // Lookup the CoreDNS version using the major and minor version + version, found := kubernetesToCoreDNSVersion[majorMinor] + return version, found +} + +// GetKubernetesToCoreDNSVersionMap returns a copy of the Kubernetes to CoreDNS version mapping. +// The map keys are Kubernetes versions in "vMAJOR.MINOR" format. +func GetKubernetesToCoreDNSVersionMap() map[string]string { + return maps.Clone(kubernetesToCoreDNSVersion) +} diff --git a/api/versions/coredns_test.go b/api/versions/coredns_test.go new file mode 100644 index 000000000..df7f7c46e --- /dev/null +++ b/api/versions/coredns_test.go @@ -0,0 +1,51 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package versions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReturnsCorrectCoreDNSVersionForValidKubernetesVersion(t *testing.T) { + version, found := GetCoreDNSVersion("v1.27") + assert.True(t, found) + assert.Equal(t, "v1.10.1", version) +} + +func TestReturnsCorrectCoreDNSVersionForValidKubernetesVersionWithoutVPrefix(t *testing.T) { + version, found := GetCoreDNSVersion("1.27") + assert.True(t, found) + assert.Equal(t, "v1.10.1", version) +} + +func TestReturnsFalseForInvalidKubernetesVersion(t *testing.T) { + version, found := GetCoreDNSVersion("v2.99") + assert.False(t, found) + assert.Equal(t, "", version) +} + +func TestReturnsFalseForMalformedKubernetesVersion(t *testing.T) { + version, found := GetCoreDNSVersion("invalid-version") + assert.False(t, found) + assert.Equal(t, "", version) +} + +func TestReturnsCorrectMappingForGetKubernetesToCoreDNSVersionMap(t *testing.T) { + mapping := GetKubernetesToCoreDNSVersionMap() + expected := map[string]string{ + "v1.22": "v1.8.4", + "v1.23": "v1.8.6", + "v1.24": "v1.8.6", + "v1.25": "v1.9.3", + "v1.26": "v1.9.3", + "v1.27": "v1.10.1", + "v1.28": "v1.10.1", + "v1.29": "v1.11.1", + "v1.30": "v1.11.3", + "v1.31": "v1.11.3", + } + assert.Equal(t, expected, mapping) +} diff --git a/hack/tools/coredns-versions/main.go b/hack/tools/coredns-versions/main.go new file mode 100644 index 000000000..3227d31a5 --- /dev/null +++ b/hack/tools/coredns-versions/main.go @@ -0,0 +1,412 @@ +// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "go/format" + "io" + "net/http" + "os" + "path" + "regexp" + "sort" + "strings" + "text/template" + + "github.com/blang/semver/v4" +) + +var ( + // Command-line flags + outputFile = flag.String("output", "api/versions/coredns.go", "Output file path") + minKubernetesVersion = flag.String("min-kubernetes-version", "v1.22.0", "Minimum Kubernetes version to include (semver format)") +) + +const ( + constantsURLTemplate = "https://raw.githubusercontent.com/kubernetes/kubernetes/%s/cmd/kubeadm/app/constants/constants.go" + branchesAPIURL = "https://api.github.com/repos/kubernetes/kubernetes/branches?per_page=100&page=%d" +) + +var goTemplate = `// Copyright 2024 Nutanix. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by script; DO NOT EDIT. Run 'make coredns.sync' instead + +package versions + +import ( + "fmt" + "maps" + + "github.com/blang/semver/v4" +) + +// Kubernetes versions +const ( +{{- range .KubernetesConstants }} + {{ .Name }} = "{{ .Version }}" +{{- end }} +) + +// CoreDNS versions +const ( +{{- range .CoreDNSConstants }} + {{ .Name }} = "{{ .Version }}" +{{- end }} +) + +// kubernetesToCoreDNSVersion maps Kubernetes versions to CoreDNS versions. +// This map is unexported to prevent external modification. +var kubernetesToCoreDNSVersion = map[string]string{ +{{- range .VersionMap }} + {{ .KubernetesConst }}: {{ .CoreDNSConst }}, +{{- end }} +} + +// GetCoreDNSVersion returns the CoreDNS version for a given Kubernetes version. +// It accepts versions with or without the "v" prefix and handles full semver versions. +// The function maps based on the major and minor versions (e.g., "v1.27"). +// If the Kubernetes version is not found, it returns an empty string and false. +func GetCoreDNSVersion(kubernetesVersion string) (string, bool) { + // Parse the version using semver + v, err := semver.ParseTolerant(kubernetesVersion) + if err != nil { + return "", false + } + + // Construct "vMAJOR.MINOR" format + majorMinor := fmt.Sprintf("v%d.%d", v.Major, v.Minor) + + // Lookup the CoreDNS version using the major and minor version + version, found := kubernetesToCoreDNSVersion[majorMinor] + return version, found +} + +// GetKubernetesToCoreDNSVersionMap returns a copy of the Kubernetes to CoreDNS version mapping. +// The map keys are Kubernetes versions in "vMAJOR.MINOR" format. +func GetKubernetesToCoreDNSVersionMap() map[string]string { + return maps.Clone(kubernetesToCoreDNSVersion) +} +` + +func main() { + flag.Parse() + + // Ensure minKubernetesVersion is in semver format + if !strings.HasPrefix(*minKubernetesVersion, "v") { + *minKubernetesVersion = "v" + *minKubernetesVersion + } + + // Parse the minimum Kubernetes version + minSemverVersion, err := semver.ParseTolerant(*minKubernetesVersion) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid min-kubernetes-version: %v\n", err) + os.Exit(1) + } + + versions, err := fetchKubernetesVersions(minSemverVersion) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching Kubernetes versions: %v\n", err) + os.Exit(1) + } + + versionMap, err := fetchCoreDNSVersions(versions) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching CoreDNS versions: %v\n", err) + os.Exit(1) + } + + if err := generateGoFile(versionMap, *outputFile); err != nil { + fmt.Fprintf(os.Stderr, "Error generating Go file: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Successfully generated %s\n", *outputFile) +} + +// Fetch Kubernetes versions from GitHub branches +func fetchKubernetesVersions(minVersion semver.Version) ([]string, error) { + var versions []string + page := 1 + + for { + url := fmt.Sprintf(branchesAPIURL, page) + branchNames, err := fetchBranchNames(url) + if err != nil { + return nil, err + } + + if len(branchNames) == 0 { + break + } + + for _, branch := range branchNames { + if strings.HasPrefix(branch, "release-1.") { + versionStr := strings.TrimPrefix(branch, "release-") + semverStr := versionStr + ".0" + v, err := semver.ParseTolerant(semverStr) + if err != nil { + continue // Skip invalid version + } + + if v.GE(minVersion) { + versions = append(versions, versionStr) + } + } + } + + page++ + } + + if len(versions) == 0 { + return nil, errors.New("no Kubernetes versions found") + } + + // Remove duplicates and sort + versionSet := make(map[string]struct{}) + for _, v := range versions { + versionSet[v] = struct{}{} + } + + versions = nil + for v := range versionSet { + versions = append(versions, v) + } + + sort.Slice(versions, func(i, j int) bool { + v1, err1 := semver.ParseTolerant(versions[i] + ".0") + v2, err2 := semver.ParseTolerant(versions[j] + ".0") + if err1 != nil || err2 != nil { + return versions[i] < versions[j] // Fallback to string comparison + } + return v1.LT(v2) + }) + + return versions, nil +} + +// Fetch branch names from GitHub API +func fetchBranchNames(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("HTTP GET error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("non-200 HTTP status: %d", resp.StatusCode) + } + + var branches []struct { + Name string `json:"name"` + } + + if err := json.NewDecoder(resp.Body).Decode(&branches); err != nil { + return nil, fmt.Errorf("decoding JSON error: %w", err) + } + + var branchNames []string + for _, branch := range branches { + branchNames = append(branchNames, branch.Name) + } + + return branchNames, nil +} + +func fetchCoreDNSVersions(versions []string) (map[string]string, error) { + versionMap := make(map[string]string) + re := regexp.MustCompile(`CoreDNSVersion\s*=\s*"([^"]+)"`) + + for _, k8sVersion := range versions { + branch := "release-" + k8sVersion + url := fmt.Sprintf(constantsURLTemplate, branch) + coreDNSVersionStr, err := extractCoreDNSVersion(url, re) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed for Kubernetes %s: %v\n", k8sVersion, err) + continue + } + + // Parse and normalize CoreDNS version + v, err := semver.ParseTolerant(coreDNSVersionStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Invalid CoreDNS version '%s' for Kubernetes %s\n", coreDNSVersionStr, k8sVersion) + continue + } + + coreDNSVersion := "v" + v.String() + + // Construct "vMAJOR.MINOR" format for Kubernetes version + k8sSemverStr := k8sVersion + ".0" + k8sSemver, err := semver.ParseTolerant(k8sSemverStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Invalid Kubernetes version '%s'\n", k8sVersion) + continue + } + k8sMajorMinor := fmt.Sprintf("v%d.%d", k8sSemver.Major, k8sSemver.Minor) + + versionMap[k8sMajorMinor] = coreDNSVersion + } + + if len(versionMap) == 0 { + return nil, errors.New("no CoreDNS versions found") + } + + return versionMap, nil +} + +func extractCoreDNSVersion(url string, re *regexp.Regexp) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("HTTP GET error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("non-200 HTTP status: %d", resp.StatusCode) + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("reading body error: %w", err) + } + + matches := re.FindStringSubmatch(string(bodyBytes)) + if len(matches) != 2 { + return "", errors.New("CoreDNSVersion not found") + } + + coreDNSVersion := matches[1] + return coreDNSVersion, nil +} + +func generateGoFile(versionMap map[string]string, outputPath string) error { + data := prepareTemplateData(versionMap) + + tmpl, err := template.New("versionMapping").Parse(goTemplate) + if err != nil { + return fmt.Errorf("parsing template error: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return fmt.Errorf("executing template error: %w", err) + } + + formattedSrc, err := format.Source(buf.Bytes()) + if err != nil { + return fmt.Errorf("formatting source code error: %w", err) + } + + if err := os.MkdirAll(path.Dir(outputPath), os.ModePerm); err != nil { + return fmt.Errorf("creating directories error: %w", err) + } + + if err := os.WriteFile(outputPath, formattedSrc, 0644); err != nil { + return fmt.Errorf("writing file error: %w", err) + } + + return nil +} + +func prepareTemplateData(versionMap map[string]string) map[string]interface{} { + type Const struct { + Name string + Version string + } + var k8sConstants []Const + var coreDNSConstants []Const + + type versionMapEntry struct { + KubernetesVersion string + KubernetesConst string + CoreDNSConst string + } + var versionMapList []versionMapEntry + + // Maps for deduplication + k8sConstMap := make(map[string]string) + coreDNSConstMap := make(map[string]string) + + // Collect unique CoreDNS versions + uniqueCoreDNSVersions := make(map[string]struct{}) + for _, coreDNSVersion := range versionMap { + uniqueCoreDNSVersions[coreDNSVersion] = struct{}{} + } + + // Generate constants for CoreDNS versions + for coreDNSVersion := range uniqueCoreDNSVersions { + constName := versionToConst("CoreDNS", coreDNSVersion) + coreDNSConstMap[coreDNSVersion] = constName + coreDNSConstants = append(coreDNSConstants, Const{Name: constName, Version: coreDNSVersion}) + } + + // Generate constants and mapping for Kubernetes versions + for k8sVersion := range versionMap { + if _, exists := k8sConstMap[k8sVersion]; !exists { + constName := versionToConst("Kubernetes", k8sVersion) + k8sConstMap[k8sVersion] = constName + k8sConstants = append(k8sConstants, Const{Name: constName, Version: k8sVersion}) + } + } + + // Map Kubernetes constants to CoreDNS constants + for k8sVersion, coreDNSVersion := range versionMap { + versionMapList = append(versionMapList, versionMapEntry{ + KubernetesVersion: k8sVersion, + KubernetesConst: k8sConstMap[k8sVersion], + CoreDNSConst: coreDNSConstMap[coreDNSVersion], + }) + } + + // Sort constants + sort.Slice(k8sConstants, func(i, j int) bool { + v1, err1 := semver.ParseTolerant(k8sConstants[i].Version) + v2, err2 := semver.ParseTolerant(k8sConstants[j].Version) + if err1 != nil || err2 != nil { + return k8sConstants[i].Version < k8sConstants[j].Version + } + return v1.LT(v2) + }) + + sort.Slice(coreDNSConstants, func(i, j int) bool { + v1, err1 := semver.ParseTolerant(coreDNSConstants[i].Version) + v2, err2 := semver.ParseTolerant(coreDNSConstants[j].Version) + if err1 != nil || err2 != nil { + return coreDNSConstants[i].Version < coreDNSConstants[j].Version + } + return v1.LT(v2) + }) + + // Sort version map + sort.Slice(versionMapList, func(i, j int) bool { + v1, err1 := semver.ParseTolerant(versionMapList[i].KubernetesVersion) + v2, err2 := semver.ParseTolerant(versionMapList[j].KubernetesVersion) + if err1 != nil || err2 != nil { + return versionMapList[i].KubernetesVersion < versionMapList[j].KubernetesVersion + } + return v1.LT(v2) + }) + + data := map[string]interface{}{ + "KubernetesConstants": k8sConstants, + "CoreDNSConstants": coreDNSConstants, + "VersionMap": versionMapList, + } + + return data +} + +func versionToConst(prefix, version string) string { + // Remove 'v' prefix if present + versionNoV := strings.TrimPrefix(version, "v") + // Replace dots with underscores + versionNoV = strings.ReplaceAll(versionNoV, ".", "_") + // Prepend the prefix and 'V' + return prefix + "_V" + versionNoV +} diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 71afc9722..378af080e 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -6,6 +6,7 @@ module github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/ha go 1.22.5 require ( + github.com/blang/semver/v4 v4.0.0 github.com/d2iq-labs/helm-list-images v0.11.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.30.3 diff --git a/hack/tools/go.sum b/hack/tools/go.sum index 453d4559e..c5f8d01e6 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -126,6 +126,8 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= diff --git a/make/apis.mk b/make/apis.mk index f1ca971e1..0da144c68 100644 --- a/make/apis.mk +++ b/make/apis.mk @@ -55,3 +55,8 @@ api.sync.%: ; $(info $(M) syncing external API: $(PROVIDER_MODULE_$*)/$(PROVIDER $(PROVIDER_API_DIR) find $(PROVIDER_API_DIR) -type d -exec chmod 0755 {} \; find $(PROVIDER_API_DIR) -type f -exec chmod 0644 {} \; + +.PHONY: coredns.sync +coredns.sync: ## Syncs the Kubernetes version to CoreDNS version mapping used in the cluster upgrade +coredns.sync: ; $(info $(M) syncing CoreDNS version mapping) + go run hack/tools/coredns-versions/main.go