Skip to content

Commit 20c0e20

Browse files
Randomly check for krew updates (#494)
* Randomly check for krew updates As this is a convenience function, silently ignore errors if they occur. Show notifications right before the command completes Add env var KREW_NO_UPGRADE_CHECK as switch to disable the upgrade check. * Explicitly disable upgrade check for integration tests * Comments * Move show/noshow logic into one place * Move tag fetch utils to `cmd/internal` package * Comments Use semver.Less to compare version tags * add logs for diagnosing update checks Signed-off-by: Ahmet Alp Balkan <[email protected]> * Run goimports Co-authored-by: Ahmet Alp Balkan <[email protected]>
1 parent 678c239 commit 20c0e20

File tree

5 files changed

+215
-0
lines changed

5 files changed

+215
-0
lines changed

cmd/krew/cmd/internal/fetch_tag.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2020 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal
16+
17+
import (
18+
"encoding/json"
19+
"net/http"
20+
21+
"github.com/pkg/errors"
22+
"k8s.io/klog"
23+
)
24+
25+
const (
26+
githubVersionURL = "https://api.github.com/repos/kubernetes-sigs/krew/releases/latest"
27+
)
28+
29+
// for testing
30+
var versionURL = githubVersionURL
31+
32+
// FetchLatestTag fetches the tag name of the latest release from GitHub.
33+
func FetchLatestTag() (string, error) {
34+
klog.V(4).Infof("Fetching latest tag from GitHub")
35+
response, err := http.Get(versionURL)
36+
if err != nil {
37+
return "", errors.Wrapf(err, "could not GET the latest release")
38+
}
39+
defer response.Body.Close()
40+
41+
var res struct {
42+
Tag string `json:"tag_name"`
43+
}
44+
klog.V(4).Infof("Parsing response from GitHub")
45+
if err := json.NewDecoder(response.Body).Decode(&res); err != nil {
46+
return "", errors.Wrapf(err, "could not parse the response from GitHub")
47+
}
48+
klog.V(4).Infof("Fetched latest tag name (%s) from GitHub", res.Tag)
49+
return res.Tag, nil
50+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2020 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal
16+
17+
import (
18+
"net/http"
19+
"net/http/httptest"
20+
"testing"
21+
)
22+
23+
func Test_fetchLatestTag_GitHubAPI(t *testing.T) {
24+
tag, err := FetchLatestTag()
25+
if err != nil {
26+
t.Error(err)
27+
}
28+
if tag == "" {
29+
t.Errorf("Expected a latest tag in the response")
30+
}
31+
}
32+
33+
func Test_fetchLatestTag(t *testing.T) {
34+
tests := []struct {
35+
name string
36+
expected string
37+
response string
38+
shouldErr bool
39+
}{
40+
{
41+
name: "broken json",
42+
response: `{"tag_name"::]`,
43+
shouldErr: true,
44+
},
45+
{
46+
name: "field missing",
47+
response: `{}`,
48+
},
49+
{
50+
name: "should get the correct tag",
51+
response: `{"tag_name": "some_tag"}`,
52+
expected: "some_tag",
53+
},
54+
}
55+
56+
for _, test := range tests {
57+
t.Run(test.name, func(tt *testing.T) {
58+
server := httptest.NewServer(http.HandlerFunc(
59+
func(w http.ResponseWriter, _ *http.Request) {
60+
_, _ = w.Write([]byte(test.response))
61+
},
62+
))
63+
64+
defer server.Close()
65+
66+
versionURL = server.URL
67+
defer func() { versionURL = githubVersionURL }()
68+
69+
tag, err := FetchLatestTag()
70+
if test.shouldErr && err == nil {
71+
tt.Error("Expected an error but found none")
72+
}
73+
if !test.shouldErr && err != nil {
74+
tt.Errorf("Expected no error but found: %s", err)
75+
}
76+
if tag != test.expected {
77+
tt.Errorf("Expected %s, got %s", test.expected, tag)
78+
}
79+
})
80+
}
81+
}
82+
83+
func Test_fetchLatestTagFailure(t *testing.T) {
84+
versionURL = "http://localhost/nirvana"
85+
defer func() { versionURL = githubVersionURL }()
86+
87+
_, err := FetchLatestTag()
88+
if err == nil {
89+
t.Error("Expected an error but found none")
90+
}
91+
}

cmd/krew/cmd/root.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ package cmd
1717
import (
1818
"flag"
1919
"fmt"
20+
"math/rand"
2021
"os"
22+
"time"
2123

2224
"github.com/fatih/color"
2325
"github.com/mattn/go-isatty"
@@ -31,12 +33,26 @@ import (
3133
"sigs.k8s.io/krew/internal/gitutil"
3234
"sigs.k8s.io/krew/internal/installation"
3335
"sigs.k8s.io/krew/internal/installation/receipt"
36+
"sigs.k8s.io/krew/internal/installation/semver"
3437
"sigs.k8s.io/krew/internal/receiptsmigration"
38+
"sigs.k8s.io/krew/internal/version"
3539
"sigs.k8s.io/krew/pkg/constants"
3640
)
3741

42+
const (
43+
upgradeNotification = "A newer version of krew is available (%s -> %s).\nRun \"kubectl krew upgrade\" to get the newest version!\n"
44+
45+
// upgradeCheckRate is the percentage of krew runs for which the upgrade check is performed.
46+
upgradeCheckRate = 0.4
47+
)
48+
3849
var (
3950
paths environment.Paths // krew paths used by the process
51+
52+
// latestTag is updated by a go-routine with the latest tag from GitHub.
53+
// An empty string indicates that the API request was skipped or
54+
// has not completed.
55+
latestTag = ""
4056
)
4157

4258
// rootCmd represents the base command when called without any subcommands
@@ -48,6 +64,7 @@ You can invoke krew through kubectl: "kubectl krew [command]..."`,
4864
SilenceUsage: true,
4965
SilenceErrors: true,
5066
PersistentPreRunE: preRun,
67+
PersistentPostRun: showUpgradeNotification,
5168
}
5269

5370
// Execute adds all child commands to the root command and sets flags appropriately.
@@ -64,6 +81,7 @@ func Execute() {
6481

6582
func init() {
6683
klog.InitFlags(nil)
84+
rand.Seed(time.Now().UnixNano())
6785

6886
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
6987
_ = flag.CommandLine.Parse([]string{}) // convince pkg/flag we parsed the flags
@@ -95,6 +113,21 @@ func preRun(cmd *cobra.Command, _ []string) error {
95113
klog.Fatal(err)
96114
}
97115

116+
go func() {
117+
if _, disabled := os.LookupEnv("KREW_NO_UPGRADE_CHECK"); disabled ||
118+
isDevelopmentBuild() || // no upgrade check for dev builds
119+
upgradeCheckRate < rand.Float64() { // only do the upgrade check randomly
120+
klog.V(1).Infof("skipping upgrade check")
121+
return
122+
}
123+
var err error
124+
klog.V(1).Infof("starting upgrade check")
125+
latestTag, err = internal.FetchLatestTag()
126+
if err != nil {
127+
klog.V(1).Infoln("WARNING:", err)
128+
}
129+
}()
130+
98131
// detect if receipts migration (v0.2.x->v0.3.x) is complete
99132
isMigrated, err := receiptsmigration.Done(paths)
100133
if err != nil {
@@ -113,9 +146,32 @@ func preRun(cmd *cobra.Command, _ []string) error {
113146
klog.Warningf("You may need to clean them up manually. Error: %v", err)
114147
}
115148
}
149+
116150
return nil
117151
}
118152

153+
func showUpgradeNotification(*cobra.Command, []string) {
154+
if latestTag == "" {
155+
klog.V(4).Infof("Upgrade check was skipped or has not finished")
156+
return
157+
}
158+
latestVer, err := semver.Parse(latestTag)
159+
if err != nil {
160+
klog.V(4).Infof("Could not parse remote tag as semver: %s", err)
161+
return
162+
}
163+
currentVer, err := semver.Parse(version.GitTag())
164+
if err != nil {
165+
klog.V(4).Infof("Could not parse current tag as semver: %s", err)
166+
return
167+
}
168+
if semver.Less(currentVer, latestVer) {
169+
color.New(color.Bold).Fprintf(os.Stderr, upgradeNotification, version.GitTag(), latestTag)
170+
} else {
171+
klog.V(4).Infof("upgrade check found no new versions (%s>=%s", currentVer, latestVer)
172+
}
173+
}
174+
119175
func cleanupStaleKrewInstallations() error {
120176
r, err := receipt.Load(paths.PluginInstallReceiptPath(constants.KrewPluginName))
121177
if os.IsNotExist(err) {
@@ -152,3 +208,10 @@ func ensureDirs(paths ...string) error {
152208
func isTerminal(f *os.File) bool {
153209
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
154210
}
211+
212+
// isDevelopmentBuild tries to parse this builds tag as semver.
213+
// If it fails, this usually means that this is a development build.
214+
func isDevelopmentBuild() bool {
215+
_, err := semver.Parse(version.GitTag())
216+
return err != nil
217+
}

docs/USER_GUIDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,13 @@ And delete the directory listed in `BasePath:` field. On macOS/Linux systems,
122122
deleting the installation location can be done by executing:
123123

124124
rm -rf ~/.krew
125+
126+
127+
## Disabling update checks
128+
129+
When using krew, it will occasionally check if a new version of krew is available
130+
by calling the GitHub API. If you want to opt out of this feature, you can set
131+
the `KREW_NO_UPGRADE_CHECK` environment variable. To permanently disable this,
132+
add the following to your `~/.bashrc`, `~/.bash_profile`, or `~/.zshrc`:
133+
134+
export KREW_NO_UPGRADE_CHECK=1

integration_test/testutil_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func NewTest(t *testing.T) (*ITest, func()) {
7070
fmt.Sprintf("PATH=%s", augmentPATH(t, binDir)),
7171
"KREW_OS=linux",
7272
"KREW_ARCH=amd64",
73+
"KREW_NO_UPGRADE_CHECK=1",
7374
},
7475
tempDir: tempDir,
7576
}, cleanup

0 commit comments

Comments
 (0)