@@ -20,6 +20,7 @@ import (
20
20
"context"
21
21
"fmt"
22
22
"os"
23
+ "path"
23
24
"path/filepath"
24
25
"regexp"
25
26
"strings"
@@ -34,6 +35,7 @@ import (
34
35
35
36
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
36
37
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
38
+ "sigs.k8s.io/cluster-api/internal/goproxy"
37
39
"sigs.k8s.io/cluster-api/version"
38
40
)
39
41
@@ -47,21 +49,27 @@ type versionChecker struct {
47
49
versionFilePath string
48
50
cliVersion func () version.Info
49
51
githubClient * github.Client
52
+ goproxyClient * goproxy.Client
50
53
}
51
54
52
55
// newVersionChecker returns a versionChecker. Its behavior has been inspired
53
56
// by https://github.com/cli/cli.
54
57
func newVersionChecker (ctx context.Context , vc config.VariablesClient ) (* versionChecker , error ) {
55
- var client * github.Client
58
+ var githubClient * github.Client
56
59
token , err := vc .Get ("GITHUB_TOKEN" )
57
60
if err == nil {
58
61
ts := oauth2 .StaticTokenSource (
59
62
& oauth2.Token {AccessToken : token },
60
63
)
61
64
tc := oauth2 .NewClient (ctx , ts )
62
- client = github .NewClient (tc )
65
+ githubClient = github .NewClient (tc )
63
66
} 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 )
65
73
}
66
74
67
75
configDirectory , err := xdg .ConfigFile (config .ConfigFolderXDG )
@@ -72,7 +80,8 @@ func newVersionChecker(ctx context.Context, vc config.VariablesClient) (*version
72
80
return & versionChecker {
73
81
versionFilePath : filepath .Join (configDirectory , "version.yaml" ),
74
82
cliVersion : version .Get ,
75
- githubClient : client ,
83
+ githubClient : githubClient ,
84
+ goproxyClient : goproxyClient ,
76
85
}, nil
77
86
}
78
87
@@ -139,28 +148,46 @@ New clusterctl version available: v%s -> v%s
139
148
140
149
func (v * versionChecker ) getLatestRelease (ctx context.Context ) (* ReleaseInfo , error ) {
141
150
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.
142
154
vs , err := readStateFile (v .versionFilePath )
143
155
if err != nil {
144
156
return nil , errors .Wrap (err , "unable to read version state file" )
145
157
}
158
+ if vs != nil {
159
+ return & vs .LatestRelease , nil
160
+ }
146
161
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 ,
155
171
}
156
172
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" )
163
175
}
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 ,
164
191
}
165
192
166
193
if err := writeStateFile (v .versionFilePath , vs ); err != nil {
@@ -170,6 +197,40 @@ func (v *versionChecker) getLatestRelease(ctx context.Context) (*ReleaseInfo, er
170
197
return & vs .LatestRelease , nil
171
198
}
172
199
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
+
173
234
func writeStateFile (path string , vs * VersionState ) error {
174
235
vsb , err := yaml .Marshal (vs )
175
236
if err != nil {
0 commit comments