Repository: kubernetes-sigs/krew
Pull request #537 (2020-03-06 00:25:52)

Add index list command
Initial implementation of `kubectl krew index list`. This prints the indexes in a table in the same format as `kubectl krew list`:
```
$ KREW_ROOT="/home/kimc/playground" X_KREW_ENABLE_MULTI_INDEX=1 ./bin/krew-linux_amd64 index list
INDEX    URL
default  https://github.com/kubernetes-sigs/krew-index.git
```

I slightly refactored the `exec` function in gitutil to return the output of the command. The existing uses of it don't need it but I'm using the output from `git config --get remote.origin.url` to get the index's remote url. Let me know if there's a better way to get the url from git, that was the only way I knew how.

Related issue: #483 
<!-- For proposed features, make sure there's an issue it's discussed first -->


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/krew/pull/537#" title="Author self-approved">chriskim06</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **ahmetb**
You can assign the PR to them by writing `/assign @ahmetb` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkrew).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/krew/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["ahmetb"]} -->
ahmetb: Needs integration tests. :) 

Patch:
diff --git a/cmd/generate-plugin-overview/generate-plugin-overview.go b/cmd/generate-plugin-overview/generate-plugin-overview.go
index 409e864..96c94a0 100644
--- a/cmd/generate-plugin-overview/generate-plugin-overview.go
+++ b/cmd/generate-plugin-overview/generate-plugin-overview.go
@@ -123,6 +123,7 @@ func findRepo(homePage string) string {
 		`https://kubernetes.github.io/ingress-nginx/kubectl-plugin/`: "kubernetes/ingress-nginx",
 		`https://kudo.dev/`:                                          "kudobuilder/kudo",
 		`https://kubevirt.io`:                                        "kubevirt/kubectl-virt-plugin",
+		`https://popeyecli.io`:                                       "derailed/popeye",
 	}
 	return knownHomePages[homePage]
 }
diff --git a/cmd/krew/cmd/index.go b/cmd/krew/cmd/index.go
new file mode 100644
index 0000000..fe70f30
--- /dev/null
+++ b/cmd/krew/cmd/index.go
@@ -0,0 +1,62 @@
+// Copyright 2019 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 cmd
+
+import (
+	"os"
+
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+
+	"sigs.k8s.io/krew/internal/index/indexoperations"
+	"sigs.k8s.io/krew/pkg/constants"
+)
+
+// indexCmd represents the index command
+var indexCmd = &cobra.Command{
+	Use:    "index",
+	Short:  "Manage custom plugin indexes",
+	Long:   "Manage which repositories are used to discover and install plugins from.",
+	Args:   cobra.NoArgs,
+	Hidden: true, // TODO(chriskim06) remove this once multi-index is enabled
+}
+
+var indexListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List configured indexes",
+	Long: `Print a list of configured indexes.
+
+This command prints a list of indexes. It shows the name and the remote URL for
+each configured index in table format.`,
+	Args: cobra.NoArgs,
+	RunE: func(_ *cobra.Command, _ []string) error {
+		indexes, err := indexoperations.ListIndexes(paths.IndexBase())
+		if err != nil {
+			return errors.Wrap(err, "failed to list indexes")
+		}
+		var rows [][]string
+		for _, index := range indexes {
+			rows = append(rows, []string{index.Name, index.URL})
+		}
+		return printTable(os.Stdout, []string{"INDEX", "URL"}, rows)
+	},
+}
+
+func init() {
+	if _, ok := os.LookupEnv(constants.EnableMultiIndexSwitch); ok {
+		indexCmd.AddCommand(indexListCmd)
+		rootCmd.AddCommand(indexCmd)
+	}
+}
diff --git a/internal/gitutil/git.go b/internal/gitutil/git.go
index 6a496dc..73a6b9d 100644
--- a/internal/gitutil/git.go
+++ b/internal/gitutil/git.go
@@ -31,7 +31,8 @@ func EnsureCloned(uri, destinationPath string) error {
 	if ok, err := IsGitCloned(destinationPath); err != nil {
 		return err
 	} else if !ok {
-		return exec("", "clone", "-v", uri, destinationPath)
+		_, err = Exec("", "clone", "-v", uri, destinationPath)
+		return err
 	}
 	return nil
 }
@@ -49,15 +50,15 @@ func IsGitCloned(gitPath string) (bool, error) {
 // and also will create a pristine working directory by removing
 // untracked files and directories.
 func updateAndCleanUntracked(destinationPath string) error {
-	if err := exec(destinationPath, "fetch", "-v"); err != nil {
+	if _, err := Exec(destinationPath, "fetch", "-v"); err != nil {
 		return errors.Wrapf(err, "fetch index at %q failed", destinationPath)
 	}
 
-	if err := exec(destinationPath, "reset", "--hard", "@{upstream}"); err != nil {
+	if _, err := Exec(destinationPath, "reset", "--hard", "@{upstream}"); err != nil {
 		return errors.Wrapf(err, "reset index at %q failed", destinationPath)
 	}
 
-	err := exec(destinationPath, "clean", "-xfd")
+	_, err := Exec(destinationPath, "clean", "-xfd")
 	return errors.Wrapf(err, "clean index at %q failed", destinationPath)
 }
 
@@ -69,7 +70,12 @@ func EnsureUpdated(uri, destinationPath string) error {
 	return updateAndCleanUntracked(destinationPath)
 }
 
-func exec(pwd string, args ...string) error {
+// GetRemoteURL returns the url of the remote origin
+func GetRemoteURL(dir string) (string, error) {
+	return Exec(dir, "config", "--get", "remote.origin.url")
+}
+
+func Exec(pwd string, args ...string) (string, error) {
 	klog.V(4).Infof("Going to run git %s", strings.Join(args, " "))
 	cmd := osexec.Command("git", args...)
 	cmd.Dir = pwd
@@ -80,7 +86,7 @@ func exec(pwd string, args ...string) error {
 	}
 	cmd.Stdout, cmd.Stderr = w, w
 	if err := cmd.Run(); err != nil {
-		return errors.Wrapf(err, "command execution failure, output=%q", buf.String())
+		return "", errors.Wrapf(err, "command execution failure, output=%q", buf.String())
 	}
-	return nil
+	return strings.TrimSpace(buf.String()), nil
 }
diff --git a/internal/index/indexoperations/index.go b/internal/index/indexoperations/index.go
new file mode 100644
index 0000000..c66b988
--- /dev/null
+++ b/internal/index/indexoperations/index.go
@@ -0,0 +1,54 @@
+// Copyright 2019 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 indexoperations
+
+import (
+	"io/ioutil"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+
+	"sigs.k8s.io/krew/internal/gitutil"
+)
+
+// Index describes the name and URL of a configured index.
+type Index struct {
+	Name string
+	URL  string
+}
+
+// ListIndexes returns a slice of Index objects. The path argument is used as
+// the base path of the index.
+func ListIndexes(path string) ([]Index, error) {
+	dirs, err := ioutil.ReadDir(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read directory %s", path)
+	}
+
+	indexes := []Index{}
+	for _, dir := range dirs {
+		indexName := dir.Name()
+		remote, err := gitutil.GetRemoteURL(filepath.Join(path, indexName))
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to list the remote URL for index %s", indexName)
+		}
+
+		indexes = append(indexes, Index{
+			Name: indexName,
+			URL:  remote,
+		})
+	}
+	return indexes, nil
+}


Test patch:
diff --git a/internal/index/indexoperations/index_test.go b/internal/index/indexoperations/index_test.go
new file mode 100644
index 0000000..7fd07cd
--- /dev/null
+++ b/internal/index/indexoperations/index_test.go
@@ -0,0 +1,66 @@
+// Copyright 2019 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 indexoperations
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	"sigs.k8s.io/krew/internal/environment"
+	"sigs.k8s.io/krew/internal/gitutil"
+	"sigs.k8s.io/krew/internal/testutil"
+)
+
+func TestListIndexes(t *testing.T) {
+	tmpDir, cleanup := testutil.NewTempDir(t)
+	defer cleanup()
+
+	wantIndexes := []Index{
+		{
+			Name: "custom",
+			URL:  "https://github.com/custom/index.git",
+		},
+		{
+			Name: "default",
+			URL:  "https://github.com/default/index.git",
+		},
+	}
+
+	for _, index := range wantIndexes {
+		path := tmpDir.Path("index/" + index.Name)
+		if err := os.MkdirAll(path, os.ModePerm); err != nil {
+			t.Fatalf("cannot create directory %q: %s", filepath.Dir(path), err)
+		}
+		_, err := gitutil.Exec(path, "init")
+		if err != nil {
+			t.Fatalf("error initializing git repo: %s", err)
+		}
+		_, err = gitutil.Exec(path, "remote", "add", "origin", index.URL)
+		if err != nil {
+			t.Fatalf("error setting remote origin: %s", err)
+		}
+	}
+
+	gotIndexes, err := ListIndexes(environment.NewPaths(tmpDir.Root()).IndexBase())
+	if err != nil {
+		t.Errorf("error listing indexes: %v", err)
+	}
+	if diff := cmp.Diff(wantIndexes, gotIndexes); diff != "" {
+		t.Errorf("output does not match: %s", diff)
+	}
+}