Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Randomly check for krew updates #494

Merged
merged 8 commits into from
Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions cmd/krew/cmd/internal/fetch_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2020 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package internal

import (
"encoding/json"
"net/http"

"github.com/pkg/errors"
"k8s.io/klog"
)

const (
githubVersionURL = "https://api.github.com/repos/kubernetes-sigs/krew/releases/latest"
)

// for testing
var versionURL = githubVersionURL

// FetchLatestTag fetches the tag name of the latest release from GitHub.
func FetchLatestTag() (string, error) {
klog.V(4).Infof("Fetching latest tag from GitHub")
response, err := http.Get(versionURL)
if err != nil {
return "", errors.Wrapf(err, "could not GET the latest release")
}
defer response.Body.Close()

var res struct {
Tag string `json:"tag_name"`
}
klog.V(4).Infof("Parsing response from GitHub")
if err := json.NewDecoder(response.Body).Decode(&res); err != nil {
return "", errors.Wrapf(err, "could not parse the response from GitHub")
}
klog.V(4).Infof("Fetched latest tag name (%s) from GitHub", res.Tag)
ahmetb marked this conversation as resolved.
Show resolved Hide resolved
return res.Tag, nil
}
91 changes: 91 additions & 0 deletions cmd/krew/cmd/internal/fetch_tag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2020 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package internal

import (
"net/http"
"net/http/httptest"
"testing"
)

func Test_fetchLatestTag_GitHubAPI(t *testing.T) {
tag, err := FetchLatestTag()
if err != nil {
t.Error(err)
}
if tag == "" {
t.Errorf("Expected a latest tag in the response")
}
}

func Test_fetchLatestTag(t *testing.T) {
tests := []struct {
name string
expected string
response string
shouldErr bool
}{
{
name: "broken json",
response: `{"tag_name"::]`,
shouldErr: true,
},
{
name: "field missing",
response: `{}`,
},
{
name: "should get the correct tag",
response: `{"tag_name": "some_tag"}`,
expected: "some_tag",
},
}

for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(test.response))
},
))

defer server.Close()

versionURL = server.URL
defer func() { versionURL = githubVersionURL }()

tag, err := FetchLatestTag()
if test.shouldErr && err == nil {
tt.Error("Expected an error but found none")
}
if !test.shouldErr && err != nil {
tt.Errorf("Expected no error but found: %s", err)
}
if tag != test.expected {
tt.Errorf("Expected %s, got %s", test.expected, tag)
}
})
}
}

func Test_fetchLatestTagFailure(t *testing.T) {
versionURL = "http://localhost/nirvana"
defer func() { versionURL = githubVersionURL }()

_, err := FetchLatestTag()
if err == nil {
t.Error("Expected an error but found none")
}
}
47 changes: 47 additions & 0 deletions cmd/krew/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ package cmd
import (
"flag"
"fmt"
"math/rand"
"os"
"time"

"github.com/fatih/color"
"github.com/mattn/go-isatty"
Expand All @@ -31,12 +33,26 @@ import (
"sigs.k8s.io/krew/internal/gitutil"
"sigs.k8s.io/krew/internal/installation"
"sigs.k8s.io/krew/internal/installation/receipt"
"sigs.k8s.io/krew/internal/installation/semver"
"sigs.k8s.io/krew/internal/receiptsmigration"
"sigs.k8s.io/krew/internal/version"
"sigs.k8s.io/krew/pkg/constants"
)

const (
upgradeNotification = "A newer version of krew is available (%s -> %s).\nRun \"kubectl krew upgrade\" to get the newest version!\n"

// showRate is the percentage of krew runs for which the upgrade check is performed.
showRate = 0.4
corneliusweig marked this conversation as resolved.
Show resolved Hide resolved
)

var (
paths environment.Paths // krew paths used by the process

// latestTag is updated by a go-routine with the latest tag from GitHub.
// An empty string indicates that the API request was skipped or
// has not completed.
latestTag = ""
)

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

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

func init() {
klog.InitFlags(nil)
rand.Seed(time.Now().UnixNano())

pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
_ = flag.CommandLine.Parse([]string{}) // convince pkg/flag we parsed the flags
Expand Down Expand Up @@ -95,6 +113,19 @@ func preRun(cmd *cobra.Command, _ []string) error {
klog.Fatal(err)
}

go func() {
if _, disabled := os.LookupEnv("KREW_NO_UPGRADE_CHECK"); disabled ||
isDevelopmentBuild() || // no upgrade check for dev builds
showRate < rand.Float64() { // only do the upgrade check randomly
return
ahmetb marked this conversation as resolved.
Show resolved Hide resolved
}
var err error
latestTag, err = internal.FetchLatestTag()
ahmetb marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
klog.V(1).Infoln("WARNING:", err)
}
}()

// detect if receipts migration (v0.2.x->v0.3.x) is complete
isMigrated, err := receiptsmigration.Done(paths)
if err != nil {
Expand All @@ -113,9 +144,18 @@ func preRun(cmd *cobra.Command, _ []string) error {
klog.Warningf("You may need to clean them up manually. Error: %v", err)
}
}

return nil
}

func showUpgradeNotification(*cobra.Command, []string) {
if latestTag == "" || latestTag == version.GitTag() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've mentioned this before: Let's parse both of these into semver, and actually do LessThan check.

Copy link
Contributor Author

@corneliusweig corneliusweig Feb 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I also explained why I think that mere string comparison is better:

We only allow the version numbers of krew releases to advance, so that it is equivalent to check for string equality vs. semver less-than comparison.

But if you feel so strongly about it, let's change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we're gonna see upgrade notifications etc in CI tests.
If it's not a big deal, let's just do that. We already have the code, and we'd simply skip if it fails to parse as semver.

klog.V(4).Infof("Skipping upgrade notification (latest=%q, current=%q)", latestTag, version.GitTag())
return
}
color.New(color.Bold).Fprintf(os.Stderr, upgradeNotification, version.GitTag(), latestTag)
}

func cleanupStaleKrewInstallations() error {
r, err := receipt.Load(paths.PluginInstallReceiptPath(constants.KrewPluginName))
if os.IsNotExist(err) {
Expand Down Expand Up @@ -152,3 +192,10 @@ func ensureDirs(paths ...string) error {
func isTerminal(f *os.File) bool {
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}

// isDevelopmentBuild tries to parse this builds tag as semver.
// If it fails, this usually means that this is a development build.
func isDevelopmentBuild() bool {
_, err := semver.Parse(version.GitTag())
return err != nil
}
10 changes: 10 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,13 @@ And delete the directory listed in `BasePath:` field. On macOS/Linux systems,
deleting the installation location can be done by executing:

rm -rf ~/.krew


## Disabling update checks

When using krew, it will occasionally check if a new version of krew is available
by calling the GitHub API. If you want to opt out of this feature, you can set
the `KREW_NO_UPGRADE_CHECK` environment variable. To permanently disable this,
add the following to your `~/.bashrc`, `~/.bash_profile`, or `~/.zshrc`:

export KREW_NO_UPGRADE_CHECK=1
1 change: 1 addition & 0 deletions integration_test/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func NewTest(t *testing.T) (*ITest, func()) {
fmt.Sprintf("PATH=%s", augmentPATH(t, binDir)),
"KREW_OS=linux",
"KREW_ARCH=amd64",
"KREW_NO_UPGRADE_CHECK=1",
},
tempDir: tempDir,
}, cleanup
Expand Down