@@ -5,25 +5,25 @@ package main
55
66import (
77 "bytes"
8- "encoding/json "
8+ "context "
99 "errors"
1010 "flag"
1111 "fmt"
1212 "go/format"
13- "io"
14- "net/http"
1513 "os"
14+ "os/signal"
1615 "path"
1716 "regexp"
1817 "sort"
1918 "strings"
2019 "text/template"
2120
2221 "github.com/blang/semver/v4"
22+ "github.com/google/go-github/v72/github"
2323)
2424
2525var (
26- // Command-line flags
26+ // Command-line flags.
2727 outputFile = flag .String ("output" , "api/versions/coredns.go" , "Output file path" )
2828 minKubernetesVersion = flag .String (
2929 "min-kubernetes-version" ,
3333)
3434
3535const (
36- constantsURLTemplate = "https://raw.githubusercontent.com/kubernetes/kubernetes/v%s/cmd/kubeadm/app/constants/constants.go"
37- tagsAPIURL = "https://api.github.com/repos/kubernetes/kubernetes/tags?per_page=100&page=%d"
36+ kubeadmConstantsFilePathInRepo = "cmd/kubeadm/app/constants/constants.go"
3837)
3938
4039var goTemplate = `// Copyright 2024 Nutanix. All rights reserved.
@@ -51,14 +50,14 @@ import (
5150 "github.com/blang/semver/v4"
5251)
5352
54- // Kubernetes versions
53+ // Kubernetes versions.
5554const (
5655{{- range .KubernetesConstants }}
5756 {{ .Name }} = "{{ .Version }}"
5857{{- end }}
5958)
6059
61- // CoreDNS versions
60+ // CoreDNS versions.
6261const (
6362{{- range .CoreDNSConstants }}
6463 {{ .Name }} = "{{ .Version }}"
@@ -114,13 +113,21 @@ func main() {
114113 os .Exit (1 )
115114 }
116115
117- versions , err := fetchKubernetesVersions (minSemverVersion )
116+ ctx , cancelFunc := signal .NotifyContext (context .Background (), os .Interrupt )
117+ defer cancelFunc ()
118+
119+ ghClient := github .NewClient (nil )
120+ if ghToken := os .Getenv ("GH_TOKEN" ); ghToken != "" {
121+ ghClient = ghClient .WithAuthToken (ghToken )
122+ }
123+
124+ versions , err := fetchKubernetesVersions (ctx , minSemverVersion , ghClient )
118125 if err != nil {
119126 fmt .Fprintf (os .Stderr , "Error fetching Kubernetes versions: %v\n " , err )
120- os .Exit (1 )
127+ os .Exit (1 ) //nolint:gocritic // This will still be a clean exit.
121128 }
122129
123- versionMap , err := fetchCoreDNSVersions (versions )
130+ versionMap , err := fetchCoreDNSVersions (ctx , versions , ghClient )
124131 if err != nil {
125132 fmt .Fprintf (os .Stderr , "Error fetching CoreDNS versions: %v\n " , err )
126133 os .Exit (1 )
@@ -134,47 +141,51 @@ func main() {
134141 fmt .Printf ("Successfully generated %s\n " , * outputFile )
135142}
136143
137- // Fetch Kubernetes versions from GitHub branches
138- func fetchKubernetesVersions (minVersion semver.Version ) ([]semver.Version , error ) {
139- minorVersionToPatchVersion := make (map [string ]semver.Version )
140- page := 1
144+ // Fetch Kubernetes versions from GitHub branches.
145+ func fetchKubernetesVersions (
146+ ctx context.Context ,
147+ minVersion semver.Version ,
148+ ghClient * github.Client ,
149+ ) ([]semver.Version , error ) {
150+ listOptions := & github.ListOptions {
151+ PerPage : 100 ,
152+ }
141153
154+ minorVersionToPatchVersion := make (map [string ]semver.Version )
142155 for {
143- url := fmt .Sprintf (tagsAPIURL , page )
144- tagNames , err := fetchTagNames (url )
156+ tags , resp , err := ghClient .Repositories .ListTags (ctx , "kubernetes" , "kubernetes" , listOptions )
145157 if err != nil {
146- return nil , err
158+ return nil , fmt . Errorf ( "failed to list Kubernetes versions: %w" , err )
147159 }
148160
149- if len (tagNames ) == 0 {
150- break
151- }
161+ for _ , tag := range tags {
162+ if ! strings .HasPrefix (tag .GetName (), "v" ) {
163+ continue // Skip tags without 'v' prefix
164+ }
152165
153- for _ , tag := range tagNames {
154- if strings .HasPrefix (tag , "v" ) {
155- v , err := semver .ParseTolerant (tag )
156- if err != nil {
157- continue // Skip invalid version
158- }
159-
160- if v .Pre != nil {
161- continue // Skip pre-release versions
162- }
163-
164- if v .LT (minVersion ) {
165- continue // Skip versions below the minimum
166- }
167-
168- // Store the highest patch version for each minor version
169- minorVersion := fmt .Sprintf ("v%d.%d" , v .Major , v .Minor )
170- if existingPatchVersionForMinor , exists := minorVersionToPatchVersion [minorVersion ]; ! exists ||
171- v .GT (existingPatchVersionForMinor ) {
172- minorVersionToPatchVersion [minorVersion ] = v
173- }
166+ v , err := semver .ParseTolerant (tag .GetName ())
167+ if err != nil {
168+ continue // Skip invalid versions
169+ }
170+ if v .Pre != nil {
171+ continue // Skip pre-release versions
172+ }
173+ if v .LT (minVersion ) {
174+ continue // Skip versions below the minimum
175+ }
176+ // Store the highest patch version for each minor version
177+ minorVersion := fmt .Sprintf ("v%d.%d" , v .Major , v .Minor )
178+ if existingPatchVersionForMinor , exists := minorVersionToPatchVersion [minorVersion ]; ! exists ||
179+ v .GT (existingPatchVersionForMinor ) {
180+ minorVersionToPatchVersion [minorVersion ] = v
174181 }
175182 }
176183
177- page ++
184+ if resp .NextPage == 0 {
185+ break
186+ }
187+
188+ listOptions .Page = resp .NextPage
178189 }
179190
180191 if len (minorVersionToPatchVersion ) == 0 {
@@ -191,56 +202,58 @@ func fetchKubernetesVersions(minVersion semver.Version) ([]semver.Version, error
191202 return versions , nil
192203}
193204
194- // Fetch branch names from GitHub API
195- func fetchTagNames (url string ) ([]string , error ) {
196- resp , err := http .Get (url )
197- if err != nil {
198- return nil , fmt .Errorf ("HTTP GET error: %w" , err )
199- }
200- defer resp .Body .Close ()
201-
202- if resp .StatusCode != http .StatusOK {
203- return nil , fmt .Errorf ("non-200 HTTP status: %d" , resp .StatusCode )
204- }
205-
206- var tags []struct {
207- Name string `json:"name"`
208- }
209-
210- if err := json .NewDecoder (resp .Body ).Decode (& tags ); err != nil {
211- return nil , fmt .Errorf ("decoding JSON error: %w" , err )
212- }
213-
214- tagNames := make ([]string , 0 , len (tags ))
215- for _ , tag := range tags {
216- tagNames = append (tagNames , tag .Name )
217- }
205+ func fetchCoreDNSVersions (
206+ ctx context.Context , versions []semver.Version , ghClient * github.Client ,
207+ ) (map [string ]string , error ) {
208+ versionMap := make (map [string ]string , len (versions ))
218209
219- return tagNames , nil
220- }
221-
222- func fetchCoreDNSVersions (versions []semver.Version ) (map [string ]string , error ) {
223- versionMap := make (map [string ]string )
224210 re := regexp .MustCompile (`CoreDNSVersion\s*=\s*"([^"]+)"` )
225211
226212 for _ , k8sVersion := range versions {
227- url := fmt .Sprintf (constantsURLTemplate , k8sVersion )
228- coreDNSVersionStr , err := extractCoreDNSVersion (url , re )
213+ fileContent , _ , _ , err := ghClient .Repositories .GetContents (
214+ ctx ,
215+ "kubernetes" ,
216+ "kubernetes" ,
217+ kubeadmConstantsFilePathInRepo ,
218+ & github.RepositoryContentGetOptions {
219+ Ref : "v" + k8sVersion .String (),
220+ },
221+ )
229222 if err != nil {
230- fmt .Fprintf (os .Stderr , "Warning: Failed for Kubernetes %s: %v\n " , k8sVersion , err )
231- continue
223+ return nil , fmt .Errorf (
224+ "failed to get Kubeadm constants file contents for Kubernetes version v%s: %w" ,
225+ k8sVersion ,
226+ err ,
227+ )
232228 }
233229
230+ decodedContent , err := fileContent .GetContent ()
231+ if err != nil {
232+ return nil , fmt .Errorf (
233+ "failed to decode Kubeadm constants file contents for Kubernetes version v%s: %w" ,
234+ k8sVersion ,
235+ err ,
236+ )
237+ }
238+
239+ matches := re .FindStringSubmatch (decodedContent )
240+ if len (matches ) != 2 {
241+ return nil , errors .New (
242+ "CoreDNS version not found in Kubeadm constants file for Kubernetes version " + k8sVersion .String (),
243+ )
244+ }
245+
246+ coreDNSVersionStr := matches [1 ]
247+
234248 // Parse and normalize CoreDNS version
235249 v , err := semver .ParseTolerant (coreDNSVersionStr )
236250 if err != nil {
237- fmt .Fprintf (
238- os .Stderr ,
239- "Warning: Invalid CoreDNS version '%s' for Kubernetes %s\n " ,
251+ return nil , fmt .Errorf (
252+ "invalid CoreDNS version '%s' for Kubernetes %s: %w" ,
240253 coreDNSVersionStr ,
241254 k8sVersion ,
255+ err ,
242256 )
243- continue
244257 }
245258
246259 coreDNSVersion := "v" + v .String ()
@@ -257,31 +270,6 @@ func fetchCoreDNSVersions(versions []semver.Version) (map[string]string, error)
257270 return versionMap , nil
258271}
259272
260- func extractCoreDNSVersion (url string , re * regexp.Regexp ) (string , error ) {
261- resp , err := http .Get (url )
262- if err != nil {
263- return "" , fmt .Errorf ("HTTP GET error: %w" , err )
264- }
265- defer resp .Body .Close ()
266-
267- if resp .StatusCode != http .StatusOK {
268- return "" , fmt .Errorf ("non-200 HTTP status: %d" , resp .StatusCode )
269- }
270-
271- bodyBytes , err := io .ReadAll (resp .Body )
272- if err != nil {
273- return "" , fmt .Errorf ("reading body error: %w" , err )
274- }
275-
276- matches := re .FindStringSubmatch (string (bodyBytes ))
277- if len (matches ) != 2 {
278- return "" , errors .New ("CoreDNSVersion not found" )
279- }
280-
281- coreDNSVersion := matches [1 ]
282- return coreDNSVersion , nil
283- }
284-
285273func generateGoFile (versionMap map [string ]string , outputPath string ) error {
286274 data := prepareTemplateData (versionMap )
287275
@@ -304,7 +292,7 @@ func generateGoFile(versionMap map[string]string, outputPath string) error {
304292 return fmt .Errorf ("creating directories error: %w" , err )
305293 }
306294
307- if err := os .WriteFile (outputPath , formattedSrc , 0o644 ); err != nil {
295+ if err := os .WriteFile (outputPath , formattedSrc , 0o644 ); err != nil { //nolint:gosec // The output file should be world readable.
308296 return fmt .Errorf ("writing file error: %w" , err )
309297 }
310298
@@ -316,15 +304,12 @@ func prepareTemplateData(versionMap map[string]string) map[string]interface{} {
316304 Name string
317305 Version string
318306 }
319- var k8sConstants []Const
320- var coreDNSConstants []Const
321307
322308 type versionMapEntry struct {
323309 KubernetesVersion string
324310 KubernetesConst string
325311 CoreDNSConst string
326312 }
327- var versionMapList []versionMapEntry
328313
329314 // Maps for deduplication
330315 k8sConstMap := make (map [string ]string )
@@ -337,13 +322,15 @@ func prepareTemplateData(versionMap map[string]string) map[string]interface{} {
337322 }
338323
339324 // Generate constants for CoreDNS versions
325+ coreDNSConstants := make ([]Const , 0 , len (uniqueCoreDNSVersions ))
340326 for coreDNSVersion := range uniqueCoreDNSVersions {
341327 constName := versionToConst ("CoreDNS" , coreDNSVersion )
342328 coreDNSConstMap [coreDNSVersion ] = constName
343329 coreDNSConstants = append (coreDNSConstants , Const {Name : constName , Version : coreDNSVersion })
344330 }
345331
346332 // Generate constants and mapping for Kubernetes versions
333+ k8sConstants := make ([]Const , 0 , len (versionMap ))
347334 for k8sVersion := range versionMap {
348335 if _ , exists := k8sConstMap [k8sVersion ]; ! exists {
349336 constName := versionToConst ("Kubernetes" , k8sVersion )
@@ -353,6 +340,7 @@ func prepareTemplateData(versionMap map[string]string) map[string]interface{} {
353340 }
354341
355342 // Map Kubernetes constants to CoreDNS constants
343+ versionMapList := make ([]versionMapEntry , 0 , len (versionMap ))
356344 for k8sVersion , coreDNSVersion := range versionMap {
357345 versionMapList = append (versionMapList , versionMapEntry {
358346 KubernetesVersion : k8sVersion ,
0 commit comments