Skip to content

Commit a1fb6f4

Browse files
feat(api): Add kubernetes version to coredns version mapping
Added script and makefile target to update coredns version mapping. `make coredns.sync` will fetch the list of releases from upstream release branches and create a mapping k8s releases to coredns versions.
1 parent 18b5adf commit a1fb6f4

File tree

3 files changed

+392
-0
lines changed

3 files changed

+392
-0
lines changed

api/versions/coredns.go

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hack/tools/coredns-versions/main.go

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"flag"
8+
"fmt"
9+
"go/format"
10+
"io"
11+
"net/http"
12+
"os"
13+
"path"
14+
"regexp"
15+
"sort"
16+
"strings"
17+
"text/template"
18+
19+
"golang.org/x/mod/semver"
20+
)
21+
22+
var (
23+
// Command-line flags
24+
outputFile = flag.String("output", "api/versions/coredns.go", "Output file path")
25+
minKubernetesVersion = flag.String("min-kubernetes-version", "v1.22", "Minimum Kubernetes version to include (semver format)")
26+
)
27+
28+
const (
29+
constantsURLTemplate = "https://raw.githubusercontent.com/kubernetes/kubernetes/%s/cmd/kubeadm/app/constants/constants.go"
30+
branchesAPIURL = "https://api.github.com/repos/kubernetes/kubernetes/branches?per_page=100&page=%d"
31+
)
32+
33+
var goTemplate = `// Code generated by script; DO NOT EDIT. Run 'make coredns.sync' instead
34+
35+
package versions
36+
37+
// Kubernetes versions
38+
const (
39+
{{- range .KubernetesConstants }}
40+
{{ .Name }} = "{{ .Version }}"
41+
{{- end }}
42+
)
43+
44+
// CoreDNS versions
45+
const (
46+
{{- range .CoreDNSConstants }}
47+
{{ .Name }} = "{{ .Version }}"
48+
{{- end }}
49+
)
50+
51+
// kubernetesToCoreDNSVersion maps Kubernetes versions to CoreDNS versions.
52+
var kubernetesToCoreDNSVersion = map[string]string{
53+
{{- range .VersionMap }}
54+
{{ .KubernetesConst }}: {{ .CoreDNSConst }},
55+
{{- end }}
56+
}
57+
58+
// GetCoreDNSVersion returns the CoreDNS version for a given Kubernetes version.
59+
// If the Kubernetes version is not found, it returns an empty string and false.
60+
func GetCoreDNSVersion(kubernetesVersion string) (string, bool) {
61+
version, found := kubernetesToCoreDNSVersion[kubernetesVersion]
62+
return version, found
63+
}
64+
65+
// GetKubernetesToCoreDNSVersionMap returns a copy of the Kubernetes to CoreDNS version mapping.
66+
func GetKubernetesToCoreDNSVersionMap() map[string]string {
67+
copyMap := make(map[string]string, len(kubernetesToCoreDNSVersion))
68+
for k, v := range kubernetesToCoreDNSVersion {
69+
copyMap[k] = v
70+
}
71+
72+
return copyMap
73+
}
74+
`
75+
76+
func main() {
77+
flag.Parse()
78+
79+
// Ensure minKubernetesVersion is in semver format (prefixed with 'v')
80+
if !strings.HasPrefix(*minKubernetesVersion, "v") {
81+
*minKubernetesVersion = "v" + *minKubernetesVersion
82+
}
83+
84+
versions, err := fetchKubernetesVersions(*minKubernetesVersion)
85+
if err != nil {
86+
fmt.Fprintf(os.Stderr, "Error fetching Kubernetes versions: %v\n", err)
87+
os.Exit(1)
88+
}
89+
90+
versionMap, err := fetchCoreDNSVersions(versions)
91+
if err != nil {
92+
fmt.Fprintf(os.Stderr, "Error fetching CoreDNS versions: %v\n", err)
93+
os.Exit(1)
94+
}
95+
96+
if err := generateGoFile(versionMap, *outputFile); err != nil {
97+
fmt.Fprintf(os.Stderr, "Error generating Go file: %v\n", err)
98+
os.Exit(1)
99+
}
100+
101+
fmt.Printf("Successfully generated %s\n", *outputFile)
102+
}
103+
104+
// Fetch Kubernetes versions from GitHub branches
105+
func fetchKubernetesVersions(minVersion string) ([]string, error) {
106+
var versions []string
107+
page := 1
108+
for {
109+
url := fmt.Sprintf(branchesAPIURL, page)
110+
branchNames, err := fetchBranchNames(url)
111+
if err != nil {
112+
return nil, err
113+
}
114+
if len(branchNames) == 0 {
115+
break
116+
}
117+
for _, branch := range branchNames {
118+
if strings.HasPrefix(branch, "release-1.") {
119+
version := strings.TrimPrefix(branch, "release-")
120+
semverVersion := "v" + version
121+
if semver.Compare(semverVersion, minVersion) >= 0 {
122+
versions = append(versions, version)
123+
}
124+
}
125+
}
126+
page++
127+
}
128+
if len(versions) == 0 {
129+
return nil, errors.New("no Kubernetes versions found")
130+
}
131+
// Remove duplicates and sort
132+
versionSet := make(map[string]struct{})
133+
for _, v := range versions {
134+
versionSet[v] = struct{}{}
135+
}
136+
versions = nil
137+
for v := range versionSet {
138+
versions = append(versions, v)
139+
}
140+
sort.Slice(versions, func(i, j int) bool {
141+
return semver.Compare("v"+versions[i], "v"+versions[j]) < 0
142+
})
143+
return versions, nil
144+
}
145+
146+
// Fetch branch names from GitHub API
147+
func fetchBranchNames(url string) ([]string, error) {
148+
resp, err := http.Get(url)
149+
if err != nil {
150+
return nil, fmt.Errorf("HTTP GET error: %w", err)
151+
}
152+
defer resp.Body.Close()
153+
154+
if resp.StatusCode != http.StatusOK {
155+
return nil, fmt.Errorf("non-200 HTTP status: %d", resp.StatusCode)
156+
}
157+
158+
var branches []struct {
159+
Name string `json:"name"`
160+
}
161+
if err := json.NewDecoder(resp.Body).Decode(&branches); err != nil {
162+
return nil, fmt.Errorf("decoding JSON error: %w", err)
163+
}
164+
165+
var branchNames []string
166+
for _, branch := range branches {
167+
branchNames = append(branchNames, branch.Name)
168+
}
169+
return branchNames, nil
170+
}
171+
172+
func fetchCoreDNSVersions(versions []string) (map[string]string, error) {
173+
versionMap := make(map[string]string)
174+
re := regexp.MustCompile(`CoreDNSVersion\s*=\s*"([^"]+)"`)
175+
176+
for _, k8sVersion := range versions {
177+
branch := "release-" + k8sVersion
178+
url := fmt.Sprintf(constantsURLTemplate, branch)
179+
coreDNSVersion, err := extractCoreDNSVersion(url, re)
180+
if err != nil {
181+
fmt.Fprintf(os.Stderr, "Warning: Failed for Kubernetes %s: %v\n", k8sVersion, err)
182+
continue
183+
}
184+
versionMap[k8sVersion] = coreDNSVersion
185+
}
186+
187+
if len(versionMap) == 0 {
188+
return nil, errors.New("no CoreDNS versions found")
189+
}
190+
191+
return versionMap, nil
192+
}
193+
194+
func extractCoreDNSVersion(url string, re *regexp.Regexp) (string, error) {
195+
resp, err := http.Get(url)
196+
if err != nil {
197+
return "", fmt.Errorf("HTTP GET error: %w", err)
198+
}
199+
defer resp.Body.Close()
200+
201+
if resp.StatusCode != http.StatusOK {
202+
return "", fmt.Errorf("non-200 HTTP status: %d", resp.StatusCode)
203+
}
204+
205+
bodyBytes, err := io.ReadAll(resp.Body)
206+
if err != nil {
207+
return "", fmt.Errorf("reading body error: %w", err)
208+
}
209+
210+
matches := re.FindStringSubmatch(string(bodyBytes))
211+
if len(matches) != 2 {
212+
return "", errors.New("CoreDNSVersion not found")
213+
}
214+
215+
return matches[1], nil
216+
}
217+
218+
func generateGoFile(versionMap map[string]string, outputPath string) error {
219+
data := prepareTemplateData(versionMap)
220+
221+
tmpl, err := template.New("versionMapping").Parse(goTemplate)
222+
if err != nil {
223+
return fmt.Errorf("parsing template error: %w", err)
224+
}
225+
226+
var buf bytes.Buffer
227+
if err := tmpl.Execute(&buf, data); err != nil {
228+
return fmt.Errorf("executing template error: %w", err)
229+
}
230+
231+
formattedSrc, err := format.Source(buf.Bytes())
232+
if err != nil {
233+
return fmt.Errorf("formatting source code error: %w", err)
234+
}
235+
236+
if err := os.MkdirAll(path.Dir(outputPath), os.ModePerm); err != nil {
237+
return fmt.Errorf("creating directories error: %w", err)
238+
}
239+
240+
if err := os.WriteFile(outputPath, formattedSrc, 0644); err != nil {
241+
return fmt.Errorf("writing file error: %w", err)
242+
}
243+
244+
return nil
245+
}
246+
247+
func prepareTemplateData(versionMap map[string]string) map[string]interface{} {
248+
type Const struct {
249+
Name string
250+
Version string
251+
}
252+
var k8sConstants []Const
253+
var coreDNSConstants []Const
254+
type versionMapEntry struct {
255+
KubernetesVersion string
256+
KubernetesConst string
257+
CoreDNSConst string
258+
}
259+
var versionMapList []versionMapEntry
260+
261+
// Maps for deduplication
262+
k8sConstMap := make(map[string]string)
263+
coreDNSConstMap := make(map[string]string)
264+
265+
// Collect unique CoreDNS versions
266+
uniqueCoreDNSVersions := make(map[string]struct{})
267+
for _, coreDNSVersion := range versionMap {
268+
uniqueCoreDNSVersions[coreDNSVersion] = struct{}{}
269+
}
270+
271+
// Generate constants for Kubernetes versions
272+
for k8sVersion := range versionMap {
273+
constName := versionToConst("Kubernetes", k8sVersion)
274+
k8sConstMap[k8sVersion] = constName
275+
}
276+
277+
// Generate constants for CoreDNS versions
278+
for coreDNSVersion := range uniqueCoreDNSVersions {
279+
constName := versionToConst("CoreDNS", coreDNSVersion)
280+
coreDNSConstMap[coreDNSVersion] = constName
281+
}
282+
283+
// Prepare constants slices
284+
for k8sVersion, constName := range k8sConstMap {
285+
k8sConstants = append(k8sConstants, Const{Name: constName, Version: k8sVersion})
286+
}
287+
for coreDNSVersion, constName := range coreDNSConstMap {
288+
coreDNSConstants = append(coreDNSConstants, Const{Name: constName, Version: coreDNSVersion})
289+
}
290+
291+
// Sort constants
292+
sort.Slice(k8sConstants, func(i, j int) bool {
293+
return semver.Compare("v"+k8sConstants[i].Version, "v"+k8sConstants[j].Version) < 0
294+
})
295+
sort.Slice(coreDNSConstants, func(i, j int) bool {
296+
return semver.Compare(coreDNSConstants[i].Version, coreDNSConstants[j].Version) < 0
297+
})
298+
299+
// Map Kubernetes constants to CoreDNS constants
300+
for k8sVersion, coreDNSVersion := range versionMap {
301+
versionMapList = append(versionMapList, versionMapEntry{
302+
KubernetesVersion: k8sVersion,
303+
KubernetesConst: k8sConstMap[k8sVersion],
304+
CoreDNSConst: coreDNSConstMap[coreDNSVersion],
305+
})
306+
}
307+
308+
// Sort version map
309+
sort.Slice(versionMapList, func(i, j int) bool {
310+
return semver.Compare("v"+versionMapList[i].KubernetesVersion, "v"+versionMapList[j].KubernetesVersion) < 0
311+
})
312+
313+
data := map[string]interface{}{
314+
"KubernetesConstants": k8sConstants,
315+
"CoreDNSConstants": coreDNSConstants,
316+
"VersionMap": versionMapList,
317+
}
318+
319+
return data
320+
}
321+
322+
func versionToConst(prefix, version string) string {
323+
// Remove 'v' prefix if present
324+
version = strings.TrimPrefix(version, "v")
325+
// Replace dots with underscores
326+
version = strings.ReplaceAll(version, ".", "_")
327+
// Prepend the prefix and 'V'
328+
return prefix + "_V" + version
329+
}

make/apis.mk

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,8 @@ api.sync.%: ; $(info $(M) syncing external API: $(PROVIDER_MODULE_$*)/$(PROVIDER
5555
$(PROVIDER_API_DIR)
5656
find $(PROVIDER_API_DIR) -type d -exec chmod 0755 {} \;
5757
find $(PROVIDER_API_DIR) -type f -exec chmod 0644 {} \;
58+
59+
.PHONY: coredns.sync
60+
coredns.sync: ## Syncs the Kubernetes version to CoreDNS version mapping used in the cluster upgrade
61+
coredns.sync: ; $(info $(M) syncing CoreDNS version mapping)
62+
go run hack/tools/coredns-versions/main.go

0 commit comments

Comments
 (0)