diff --git a/cmd/krew/cmd/root.go b/cmd/krew/cmd/root.go index 93856a1f..2422e64f 100644 --- a/cmd/krew/cmd/root.go +++ b/cmd/krew/cmd/root.go @@ -25,8 +25,11 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + "sigs.k8s.io/krew/pkg/constants" "sigs.k8s.io/krew/pkg/environment" "sigs.k8s.io/krew/pkg/gitutil" + "sigs.k8s.io/krew/pkg/installation" + "sigs.k8s.io/krew/pkg/installation/receipt" "sigs.k8s.io/krew/pkg/receiptsmigration" ) @@ -40,19 +43,9 @@ var rootCmd = &cobra.Command{ Short: "krew is the kubectl plugin manager", Long: `krew is the kubectl plugin manager. You can invoke krew through kubectl: "kubectl krew [command]..."`, - SilenceUsage: true, - SilenceErrors: true, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - isMigrated, err := receiptsmigration.Done(paths) - if err != nil { - return err - } - if isMigrated || cmd.Use == "receipts-upgrade" { - return nil - } - fmt.Fprintln(os.Stderr, "You need to perform a migration to continue using krew.\nPlease run `kubectl krew system receipts-upgrade`") - return fmt.Errorf("krew home outdated") - }, + SilenceUsage: true, + SilenceErrors: true, + PersistentPreRunE: preRun, } // Execute adds all child commands to the root command and sets flags appropriately. @@ -87,6 +80,42 @@ func init() { } } +func preRun(cmd *cobra.Command, _ []string) error { + // detect if receipts migration (v0.2.x->v0.3.x) is complete + isMigrated, err := receiptsmigration.Done(paths) + if err != nil { + return err + } + if !isMigrated && cmd.Use != "receipts-upgrade" { + fmt.Fprintln(os.Stderr, "You need to perform a migration to continue using krew.\nPlease run `kubectl krew system receipts-upgrade`") + return fmt.Errorf("krew home outdated") + } + + if installation.IsWindows() { + glog.V(4).Infof("detected windows, will check for old krew installations to clean up") + err := cleanupStaleKrewInstallations() + if err != nil { + glog.Warningf("Failed to clean up old installations of krew (on windows).") + glog.Warningf("You may need to clean them up manually. Error: %v", err) + } + } + return nil +} + +func cleanupStaleKrewInstallations() error { + r, err := receipt.Load(paths.PluginInstallReceiptPath(constants.KrewPluginName)) + if os.IsNotExist(err) { + glog.V(1).Infof("could not find krew's own plugin receipt, skipping cleanup of stale krew installations") + return nil + } else if err != nil { + return errors.Wrap(err, "cannot load krew's own plugin receipt") + } + v := r.Spec.Version + + glog.V(1).Infof("Clean up krew stale installations, current=%s", v) + return installation.CleanupStaleKrewInstallations(paths.PluginInstallPath(constants.KrewPluginName), v) +} + func checkIndex(_ *cobra.Command, _ []string) error { if ok, err := gitutil.IsGitCloned(paths.IndexPath()); err != nil { return errors.Wrap(err, "failed to check local index git repository") diff --git a/cmd/krew/cmd/version.go b/cmd/krew/cmd/version.go index b6523abb..f4fb07e6 100644 --- a/cmd/krew/cmd/version.go +++ b/cmd/krew/cmd/version.go @@ -15,15 +15,11 @@ package cmd import ( - "fmt" "os" - "github.com/golang/glog" - "github.com/pkg/errors" "github.com/spf13/cobra" "sigs.k8s.io/krew/pkg/constants" - "sigs.k8s.io/krew/pkg/environment" "sigs.k8s.io/krew/pkg/version" ) @@ -34,8 +30,6 @@ var versionCmd = &cobra.Command{ Long: `Show version information and diagnostics about krew itself. Remarks: - - IsPlugin is true if krew is executed as a plugin - - ExecutedVersion is the version of the currently executed binary. This is detected through the path. - GitTag describes the release name krew is built from. - GitCommit describes the git revision ID which krew is built from. - IndexURI is the URI where the index is updated from. @@ -45,19 +39,7 @@ Remarks: - DownloadPath is the directory for temporarily downloading plugins. - BinPath is the directory for the symbolic links to the installed plugin executables.`, RunE: func(cmd *cobra.Command, args []string) error { - selfPath, err := os.Executable() - if err != nil { - glog.Fatalf("failed to get the own executable path") - } - - executedVersion, runningAsPlugin, err := environment.GetExecutedVersion(paths.InstallPath(), selfPath, environment.Realpath) - if err != nil { - return errors.Wrap(err, "failed to find current krew version") - } - conf := [][]string{ - {"IsPlugin", fmt.Sprintf("%v", runningAsPlugin)}, - {"ExecutedVersion", executedVersion}, {"GitTag", version.GitTag()}, {"GitCommit", version.GitCommit()}, {"IndexURI", constants.IndexURI}, diff --git a/integration_test/version_test.go b/integration_test/version_test.go index 8bf56e2d..dfc2e639 100644 --- a/integration_test/version_test.go +++ b/integration_test/version_test.go @@ -29,9 +29,7 @@ func TestKrewVersion(t *testing.T) { output := test.Krew("version").RunOrFailOutput() requiredSubstrings := []string{ - "IsPlugin", fmt.Sprintf(`BasePath\s+%s`, test.Root()), - "ExecutedVersion", "GitTag", "GitCommit", `IndexURI\s+https://github.com/kubernetes-sigs/krew-index.git`, diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 80da7e96..bfa8cd8d 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -18,6 +18,7 @@ const ( CurrentAPIVersion = "krew.googlecontainertools.github.com/v1alpha2" PluginKind = "Plugin" ManifestExtension = ".yaml" + KrewPluginName = "krew" // plugin name of krew itself // IndexURI points to the upstream index. IndexURI = "https://github.com/kubernetes-sigs/krew-index.git" diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go index a8925b38..65acd4c0 100644 --- a/pkg/environment/environment.go +++ b/pkg/environment/environment.go @@ -23,7 +23,6 @@ import ( "k8s.io/client-go/util/homedir" "sigs.k8s.io/krew/pkg/constants" - "sigs.k8s.io/krew/pkg/pathutil" ) // Paths contains all important environment paths @@ -107,32 +106,6 @@ func (p Paths) PluginVersionInstallPath(plugin, version string) string { return filepath.Join(p.InstallPath(), plugin, version) } -// GetExecutedVersion returns the currently executed version. If krew is -// not executed as an plugin it will return a nil error and an empty string. -func GetExecutedVersion(installPath string, executionPath string, pathResolver func(string) (string, error)) (string, bool, error) { - path, err := pathResolver(executionPath) - if err != nil { - return "", false, errors.Wrap(err, "failed to resolve path") - } - - currentBinaryPath, err := filepath.Abs(path) - if err != nil { - return "", false, err - } - - pluginsPath, err := filepath.Abs(filepath.Join(installPath, "krew")) - if err != nil { - return "", false, err - } - - elems, ok := pathutil.IsSubPath(pluginsPath, currentBinaryPath) - if !ok || len(elems) < 2 { - return "", false, nil - } - - return elems[0], true, nil -} - // Realpath evaluates symbolic links. If the path is not a symbolic link, it // returns the cleaned path. Symbolic links with relative paths return error. func Realpath(path string) (string, error) { diff --git a/pkg/environment/environment_test.go b/pkg/environment/environment_test.go index 21b7e151..e87dd0b4 100644 --- a/pkg/environment/environment_test.go +++ b/pkg/environment/environment_test.go @@ -80,79 +80,6 @@ func TestPaths(t *testing.T) { } } -func TestGetExecutedVersion(t *testing.T) { - type args struct { - paths Paths - executionPath string - } - tests := []struct { - name string - args args - want string - inPath bool - wantErr bool - }{ - { - name: "is in krew path", - args: args{ - paths: newPaths(filepath.FromSlash("/plugins")), - executionPath: filepath.FromSlash("/plugins/store/krew/deadbeef/krew.exe"), - }, - want: "deadbeef", - inPath: true, - wantErr: false, - }, - { - name: "is not in krew path", - args: args{ - paths: newPaths(filepath.FromSlash("/plugins")), - executionPath: filepath.FromSlash("/plugins/store/NOTKREW/deadbeef/krew.exe"), - }, - want: "", - inPath: false, - wantErr: false, - }, - { - name: "is in longer krew path", - args: args{ - paths: newPaths(filepath.FromSlash("/plugins")), - executionPath: filepath.FromSlash("/plugins/store/krew/deadbeef/foo/krew.exe"), - }, - want: "deadbeef", - inPath: true, - wantErr: false, - }, - { - name: "is in smaller krew path", - args: args{ - paths: newPaths(filepath.FromSlash("/plugins")), - executionPath: filepath.FromSlash("/krew.exe"), - }, - want: "", - inPath: false, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Logf("installpath=%s", tt.args.paths.InstallPath()) - got, isVersion, err := GetExecutedVersion(tt.args.paths.InstallPath(), tt.args.executionPath, func(s string) (string, error) { - return s, nil - }) - if (err != nil) != tt.wantErr { - t.Errorf("GetExecutedVersion() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("GetExecutedVersion() got = %v, want %v", got, tt.want) - } - if isVersion != tt.inPath { - t.Errorf("GetExecutedVersion() isVersion = %v, want %v", isVersion, tt.inPath) - } - }) - } -} - func TestRealpath(t *testing.T) { tmpDir, cleanup := testutil.NewTempDir(t) defer cleanup() diff --git a/pkg/installation/install.go b/pkg/installation/install.go index 21b8487d..15891680 100644 --- a/pkg/installation/install.go +++ b/pkg/installation/install.go @@ -15,6 +15,7 @@ package installation import ( + "io/ioutil" "os" "path/filepath" "runtime" @@ -23,6 +24,7 @@ import ( "github.com/golang/glog" "github.com/pkg/errors" + "sigs.k8s.io/krew/pkg/constants" "sigs.k8s.io/krew/pkg/download" "sigs.k8s.io/krew/pkg/environment" "sigs.k8s.io/krew/pkg/index" @@ -44,10 +46,6 @@ type installOperation struct { binDir string } -const ( - krewPluginName = "krew" -) - // Plugin lifecycle errors var ( ErrIsAlreadyInstalled = errors.New("can't install, the newest version is already installed") @@ -151,9 +149,9 @@ func downloadAndExtract(extractDir, uri, sha256sum, overrideFile string) error { // Uninstall will uninstall a plugin. func Uninstall(p environment.Paths, name string) error { - if name == krewPluginName { + if name == constants.KrewPluginName { glog.Errorf("Removing krew through krew is not supported.") - if !isWindows() { // assume POSIX-like + if !IsWindows() { // assume POSIX-like glog.Errorf("If you’d like to uninstall krew altogether, run:\n\trm -rf -- %q", p.BasePath()) } return errors.New("self-uninstall not allowed") @@ -169,7 +167,7 @@ func Uninstall(p environment.Paths, name string) error { glog.V(1).Infof("Deleting plugin %s", name) - symlinkPath := filepath.Join(p.BinPath(), pluginNameToBin(name, isWindows())) + symlinkPath := filepath.Join(p.BinPath(), pluginNameToBin(name, IsWindows())) glog.V(3).Infof("Unlink %q", symlinkPath) if err := removeLink(symlinkPath); err != nil { return errors.Wrap(err, "could not uninstall symlink of plugin") @@ -187,7 +185,7 @@ func Uninstall(p environment.Paths, name string) error { } func createOrUpdateLink(binDir string, binary string, plugin string) error { - dst := filepath.Join(binDir, pluginNameToBin(plugin, isWindows())) + dst := filepath.Join(binDir, pluginNameToBin(plugin, IsWindows())) if err := removeLink(dst); err != nil { return errors.Wrap(err, "failed to remove old symlink") @@ -226,7 +224,8 @@ func removeLink(path string) error { return nil } -func isWindows() bool { +// IsWindows sees if KREW_OS or runtime.GOOS to find out if current execution mode is win32. +func IsWindows() bool { goos := runtime.GOOS if env := os.Getenv("KREW_OS"); env != "" { goos = env @@ -244,3 +243,24 @@ func pluginNameToBin(name string, isWindows bool) string { } return name } + +// CleanupStaleKrewInstallations removes the versions that aren't the current version. +func CleanupStaleKrewInstallations(dir string, currentVersion string) error { + ls, err := ioutil.ReadDir(dir) + if err != nil { + return errors.Wrap(err, "failed to read krew store directory") + } + glog.V(2).Infof("Found %d entries in krew store directory", len(ls)) + for _, d := range ls { + glog.V(2).Infof("Found a krew installation: %s (%s)", d.Name(), d.Mode()) + if d.IsDir() && d.Name() != currentVersion { + glog.V(1).Infof("Deleting stale krew install directory: %s", d.Name()) + p := filepath.Join(dir, d.Name()) + if err := os.RemoveAll(p); err != nil { + return errors.Wrapf(err, "failed to remove stale krew version at path '%s'", p) + } + glog.V(1).Infof("Stale installation directory removed") + } + } + return nil +} diff --git a/pkg/installation/install_test.go b/pkg/installation/install_test.go index a0f938f8..d450ebdc 100644 --- a/pkg/installation/install_test.go +++ b/pkg/installation/install_test.go @@ -195,25 +195,25 @@ func Test_removeLink_regularFileExists(t *testing.T) { } } -func Test_isWindows(t *testing.T) { +func TestIsWindows(t *testing.T) { expected := runtime.GOOS == "windows" - got := isWindows() + got := IsWindows() if expected != got { - t.Fatalf("isWindows()=%v; expected=%v (on %s)", got, expected, runtime.GOOS) + t.Fatalf("IsWindows()=%v; expected=%v (on %s)", got, expected, runtime.GOOS) } } -func Test_isWindows_envOverride(t *testing.T) { +func TestIsWindows_envOverride(t *testing.T) { defer os.Unsetenv("KREW_OS") os.Setenv("KREW_OS", "windows") - if !isWindows() { - t.Fatalf("isWindows()=false when KREW_OS=windows") + if !IsWindows() { + t.Fatalf("IsWindows()=false when KREW_OS=windows") } os.Setenv("KREW_OS", "not-windows") - if isWindows() { - t.Fatalf("isWindows()=true when KREW_OS != windows") + if IsWindows() { + t.Fatalf("IsWindows()=true when KREW_OS != windows") } } @@ -297,3 +297,39 @@ func Test_applyDefaults(t *testing.T) { }) } } + +func TestCleanupStaleKrewInstallations(t *testing.T) { + dir, close := testutil.NewTempDir(t) + defer close() + + testFiles := []string{ + "dir1/f1.txt", + "dir2/f2.txt", + "dir3/subdir/f3.txt", + "file1.txt", + "file2.txt", + } + for _, tf := range testFiles { + dir.Write(filepath.FromSlash(tf), nil) + } + + err := CleanupStaleKrewInstallations(dir.Root(), "dir2") + if err != nil { + t.Fatal(err) + } + + ls, err := ioutil.ReadDir(dir.Root()) + if err != nil { + t.Fatal(err) + } + + got := make([]string, 0, len(ls)) + for _, l := range ls { + got = append(got, l.Name()) + } + + expected := []string{"dir2", "file1.txt", "file2.txt"} + if diff := cmp.Diff(expected, got); diff != "" { + t.Fatal(diff) + } +} diff --git a/pkg/installation/upgrade.go b/pkg/installation/upgrade.go index a7ec43b0..d39e6bf5 100644 --- a/pkg/installation/upgrade.go +++ b/pkg/installation/upgrade.go @@ -15,13 +15,13 @@ package installation import ( - "io/ioutil" "os" "path/filepath" "github.com/golang/glog" "github.com/pkg/errors" + "sigs.k8s.io/krew/pkg/constants" "sigs.k8s.io/krew/pkg/environment" "sigs.k8s.io/krew/pkg/index" "sigs.k8s.io/krew/pkg/installation/receipt" @@ -83,56 +83,21 @@ func Upgrade(p environment.Paths, plugin index.Plugin) error { } // Clean old installations - glog.V(4).Infof("Starting old version cleanup") - return removePluginVersionFromFS(p, plugin, newVersion, curVersion) + glog.V(2).Infof("Starting old version cleanup") + return cleanupInstallation(p, plugin, curVersion) } -// removePluginVersionFromFS will remove a plugin directly if it not krew. +// cleanupInstallation will remove a plugin directly if it not krew. // // Krew on Windows needs special care because active directories can't be -// deleted. This method will unlink old krew versions and during next run clean +// deleted. This method will mark old krew versions and during next run clean // the directory. -func removePluginVersionFromFS(p environment.Paths, plugin index.Plugin, newVersion, oldVersion string) error { - // Cleanup if we haven't updated krew during this execution. - if plugin.Name == krewPluginName { - glog.V(1).Infof("Handling removal for older version of krew") - execPath, err := os.Executable() - if err != nil { - return errors.Wrap(err, "could not get krew's own executable path") - } - executedKrewVersion, _, err := environment.GetExecutedVersion(p.InstallPath(), execPath, environment.Realpath) - if err != nil { - return errors.Wrap(err, "failed to find current krew version") - } - glog.V(1).Infof("Detected running krew version=%s", executedKrewVersion) - return handleKrewRemove(p, plugin, newVersion, executedKrewVersion) +func cleanupInstallation(p environment.Paths, plugin index.Plugin, oldVersion string) error { + if plugin.Name == constants.KrewPluginName && IsWindows() { + glog.V(1).Infof("not removing old version of krew during upgrade on windows (should be cleaned up on the next run)") + return nil } glog.V(1).Infof("Remove old plugin installation under %q", p.PluginVersionInstallPath(plugin.Name, oldVersion)) return os.RemoveAll(p.PluginVersionInstallPath(plugin.Name, oldVersion)) } - -// handleKrewRemove will remove and unlink old krew versions. -func handleKrewRemove(p environment.Paths, plugin index.Plugin, newVersion, currentKrewVersion string) error { - dir, err := ioutil.ReadDir(p.PluginInstallPath(plugin.Name)) - if err != nil { - return errors.Wrap(err, "can't read plugin dir") - } - for _, f := range dir { - pluginVersionPath := p.PluginVersionInstallPath(plugin.Name, f.Name()) - if !f.IsDir() { - continue - } - // Delete old dir - if f.Name() != newVersion && f.Name() != currentKrewVersion { - glog.V(1).Infof("Remove old krew installation under %q", pluginVersionPath) - if err = os.RemoveAll(pluginVersionPath); err != nil { - return errors.Wrapf(err, "can't remove plugin oldVersion=%q, path=%q", f.Name(), pluginVersionPath) - } - } else if f.Name() != newVersion { - glog.V(1).Infof("Unlink krew installation under %q", pluginVersionPath) - // TODO(ahmetb,lbb) is this part implemented??? - } - } - return nil -}