@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "fmt"
2222 "os"
23+ "path"
2324 "path/filepath"
2425 "regexp"
2526 "strings"
@@ -34,6 +35,7 @@ import (
3435
3536 "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
3637 logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
38+ "sigs.k8s.io/cluster-api/internal/goproxy"
3739 "sigs.k8s.io/cluster-api/version"
3840)
3941
@@ -47,21 +49,27 @@ type versionChecker struct {
4749 versionFilePath string
4850 cliVersion func () version.Info
4951 githubClient * github.Client
52+ goproxyClient * goproxy.Client
5053}
5154
5255// newVersionChecker returns a versionChecker. Its behavior has been inspired
5356// by https://github.com/cli/cli.
5457func newVersionChecker (ctx context.Context , vc config.VariablesClient ) (* versionChecker , error ) {
55- var client * github.Client
58+ var githubClient * github.Client
5659 token , err := vc .Get ("GITHUB_TOKEN" )
5760 if err == nil {
5861 ts := oauth2 .StaticTokenSource (
5962 & oauth2.Token {AccessToken : token },
6063 )
6164 tc := oauth2 .NewClient (ctx , ts )
62- client = github .NewClient (tc )
65+ githubClient = github .NewClient (tc )
6366 } else {
64- client = github .NewClient (nil )
67+ githubClient = github .NewClient (nil )
68+ }
69+
70+ var goproxyClient * goproxy.Client
71+ if scheme , host , err := goproxy .GetSchemeAndHost (os .Getenv ("GOPROXY" )); err == nil && scheme != "" && host != "" {
72+ goproxyClient = goproxy .NewClient (scheme , host )
6573 }
6674
6775 configDirectory , err := xdg .ConfigFile (config .ConfigFolderXDG )
@@ -72,7 +80,8 @@ func newVersionChecker(ctx context.Context, vc config.VariablesClient) (*version
7280 return & versionChecker {
7381 versionFilePath : filepath .Join (configDirectory , "version.yaml" ),
7482 cliVersion : version .Get ,
75- githubClient : client ,
83+ githubClient : githubClient ,
84+ goproxyClient : goproxyClient ,
7685 }, nil
7786}
7887
@@ -139,28 +148,46 @@ New clusterctl version available: v%s -> v%s
139148
140149func (v * versionChecker ) getLatestRelease (ctx context.Context ) (* ReleaseInfo , error ) {
141150 log := logf .Log
151+
152+ // Try to get latest clusterctl version number from the local state file.
153+ // NOTE: local state file is ignored if older than 1d.
142154 vs , err := readStateFile (v .versionFilePath )
143155 if err != nil {
144156 return nil , errors .Wrap (err , "unable to read version state file" )
145157 }
158+ if vs != nil {
159+ return & vs .LatestRelease , nil
160+ }
146161
147- // if there is no release info in the state file, pull latest release from github
148- if vs == nil {
149- release , _ , err := v .githubClient .Repositories .GetLatestRelease (ctx , "kubernetes-sigs" , "cluster-api" )
150- if err != nil {
151- log .V (1 ).Info ("⚠️ Unable to get latest github release for clusterctl" )
152- // failing silently here so we don't error out in air-gapped
153- // environments.
154- return nil , nil //nolint:nilerr
162+ // Try to get latest clusterctl version number from go modules.
163+ latest , err := v .goproxyGetLatest (ctx )
164+ if err != nil {
165+ log .V (5 ).Info ("error using Goproxy client to get latest versions for clusterctl, falling back to github client" )
166+ }
167+ if latest != nil {
168+ vs = & VersionState {
169+ LastCheck : time .Now (),
170+ LatestRelease : * latest ,
155171 }
156172
157- vs = & VersionState {
158- LastCheck : time .Now (),
159- LatestRelease : ReleaseInfo {
160- Version : release .GetTagName (),
161- URL : release .GetHTMLURL (),
162- },
173+ if err := writeStateFile (v .versionFilePath , vs ); err != nil {
174+ return nil , errors .Wrap (err , "unable to write version state file" )
163175 }
176+ return & vs .LatestRelease , nil
177+ }
178+
179+ // Otherwise fall back to get latest clusterctl version number from GitHub.
180+ latest , err = v .gitHubGetLatest (ctx )
181+ if err != nil {
182+ log .V (1 ).Info ("⚠️ Unable to get latest github release for clusterctl" )
183+ // failing silently here so we don't error out in air-gapped
184+ // environments.
185+ return nil , nil //nolint:nilerr
186+ }
187+
188+ vs = & VersionState {
189+ LastCheck : time .Now (),
190+ LatestRelease : * latest ,
164191 }
165192
166193 if err := writeStateFile (v .versionFilePath , vs ); err != nil {
@@ -170,6 +197,40 @@ func (v *versionChecker) getLatestRelease(ctx context.Context) (*ReleaseInfo, er
170197 return & vs .LatestRelease , nil
171198}
172199
200+ func (v * versionChecker ) goproxyGetLatest (ctx context.Context ) (* ReleaseInfo , error ) {
201+ if v .goproxyClient == nil {
202+ return nil , nil
203+ }
204+
205+ gomodulePath := path .Join ("sigs.k8s.io" , "cluster-api" )
206+ versions , err := v .goproxyClient .GetVersions (ctx , gomodulePath )
207+ if err != nil {
208+ return nil , err
209+ }
210+
211+ latest := semver.Version {}
212+ for _ , v := range versions {
213+ if v .GT (latest ) {
214+ latest = v
215+ }
216+ }
217+ return & ReleaseInfo {
218+ Version : latest .String (),
219+ URL : gomodulePath ,
220+ }, nil
221+ }
222+
223+ func (v * versionChecker ) gitHubGetLatest (ctx context.Context ) (* ReleaseInfo , error ) {
224+ release , _ , err := v .githubClient .Repositories .GetLatestRelease (ctx , "kubernetes-sigs" , "cluster-api" )
225+ if err != nil {
226+ return nil , err
227+ }
228+ return & ReleaseInfo {
229+ Version : release .GetTagName (),
230+ URL : release .GetHTMLURL (),
231+ }, nil
232+ }
233+
173234func writeStateFile (path string , vs * VersionState ) error {
174235 vsb , err := yaml .Marshal (vs )
175236 if err != nil {
0 commit comments