Repository: tetratelabs/getenvoy
Pull request #246 (2021-05-27 05:23:45)

Adds `getenvoy installed` to list installed versions
Before, we only had a command to know the available versions. This tells
us what's installed. Later, we can print out which is the default
version.

Rationale for "installed" is that it is intuitive alongside "install" vs other options. Also, the fact that "getenvoy" is not an env-based name makes it look nicer (IMHO)

Ex.
```bash
getenvoy versions (existing command)
getenvoy install 1.18.3 (existing command)
getenvoy installed (new command)
```

Vs. Mixing different output based on a flag like `pyenv install`
```bash
pyenv install -l
pyenv install  3.9.5
pyenv versions
```

Vs. not really describing versions are the thing 'ls'ed like `nvm`
```bash
nvm ls-remote
nvm install v16.2.0
nvm ls
```



codefromthecrypt: Thanks @mathetake much less code now, except tests where there should be more :D

Patch:
diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml
index 4fff9d1..e794f42 100644
--- a/.github/workflows/commit.yaml
+++ b/.github/workflows/commit.yaml
@@ -45,7 +45,7 @@ jobs:
         with:
           # This combines unrelated caches because actions/cache@v2 doesn't support multiple
           # instances, rather a combined path. https://github.com/actions/cache/issues/16
-          path: |
+          path: |  # ~/.getenvoy/versions is cached so that we only re-download once: for TestGetEnvoyInstall
             ~/.getenvoy/versions
             ~/go/pkg/mod
             ~/go/bin/*-v*
diff --git a/internal/cmd/app.go b/internal/cmd/app.go
index 3886470..ba0942f 100644
--- a/internal/cmd/app.go
+++ b/internal/cmd/app.go
@@ -40,7 +40,7 @@ func NewApp(o *globals.GlobalOpts) *cli.App {
 	app.Flags = []cli.Flag{
 		&cli.StringFlag{
 			Name:        "home-dir",
-			Usage:       "GetEnvoy home directory (location of downloaded versions and run archives)",
+			Usage:       "GetEnvoy home directory (location of installed versions and run archives)",
 			DefaultText: globals.DefaultHomeDir,
 			Destination: &homeDir,
 			EnvVars:     []string{"GETENVOY_HOME"},
@@ -70,6 +70,7 @@ func NewApp(o *globals.GlobalOpts) *cli.App {
 		NewRunCmd(o),
 		NewVersionsCmd(o),
 		NewInstallCmd(o),
+		NewInstalledCmd(o),
 		NewDocCmd(),
 	}
 	return app
diff --git a/internal/cmd/installed.go b/internal/cmd/installed.go
new file mode 100644
index 0000000..01831f3
--- /dev/null
+++ b/internal/cmd/installed.go
@@ -0,0 +1,59 @@
+// Copyright 2021 Tetrate
+//
+// 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 (
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"github.com/urfave/cli/v2"
+
+	"github.com/tetratelabs/getenvoy/internal/globals"
+)
+
+// NewInstalledCmd returns command that lists installed Envoy versions
+func NewInstalledCmd(o *globals.GlobalOpts) *cli.Command {
+	return &cli.Command{
+		Name:  "installed",
+		Usage: "List installed Envoy versions",
+		Action: func(c *cli.Context) error {
+			files, err := os.ReadDir(filepath.Join(o.HomeDir, "versions"))
+			if os.IsNotExist(err) {
+				_, err = fmt.Fprintln(o.Out, "No envoy versions installed, yet")
+				return err
+			} else if err != nil {
+				return err
+			}
+
+			rows := []string{"VERSION"}
+			for _, f := range files {
+				if f.IsDir() {
+					rows = append(rows, f.Name())
+				}
+			}
+
+			// Sort so that new versions appear first
+			sort.Slice(rows, func(i, j int) bool {
+				return rows[i] > rows[j]
+			})
+
+			_, err = fmt.Fprintln(o.Out, strings.Join(rows, "\n"))
+			return err
+		},
+	}
+}


Test patch:
diff --git a/e2e/command_test.go b/e2e/command_test.go
index 1cff2c2..5449b2b 100644
--- a/e2e/command_test.go
+++ b/e2e/command_test.go
@@ -37,16 +37,11 @@ type cmdBuilder struct {
 }
 
 // getEnvoy returns a new command builder.
-func getEnvoy(arg0 string) *cmdBuilder { //nolint:golint
-
-	var args []string
-	if arg0 == "--version" {
-		args = []string{arg0}
-	} else {
-		args = []string{"--internal-user-agent", userAgent, arg0}
+// Hard-code "User-Agent" HTTP header so that it doesn't interfere with site analytics.
+func getEnvoy(args ...string) *cmdBuilder { //nolint:golint
+	if args[0] != "--version" {
+		args = append([]string{"--internal-user-agent", userAgent}, args...)
 	}
-
-	// Hard-code "User-Agent" HTTP header so that it doesn't interfere with site analytics.
 	return &cmdBuilder{exec.Command(getEnvoyPath, args...)} //nolint:gosec
 }
 
diff --git a/e2e/getenvoy_install_test.go b/e2e/getenvoy_install_test.go
new file mode 100644
index 0000000..9ef0641
--- /dev/null
+++ b/e2e/getenvoy_install_test.go
@@ -0,0 +1,60 @@
+// Copyright 2021 Tetrate
+//
+// 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 e2e
+
+import (
+	"fmt"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
+	"github.com/tetratelabs/getenvoy/internal/version"
+)
+
+// TestGetEnvoyInstall needs to always execute, so we run it in a separate home directory
+func TestGetEnvoyInstall(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
+	defer removeHomeDir()
+
+	t.Run("not yet installed", func(t *testing.T) {
+		stdout, stderr, err := getEnvoy("--home-dir", homeDir, "install", version.LastKnownEnvoy).exec()
+
+		require.NoError(t, err)
+		require.Regexp(t, `downloading https:.*tar.*`, stdout)
+		require.Empty(t, stderr)
+
+		require.FileExists(t, filepath.Join(homeDir, "versions", version.LastKnownEnvoy, "bin", "envoy"))
+	})
+
+	t.Run("already installed", func(t *testing.T) {
+		stdout, stderr, err := getEnvoy("--home-dir", homeDir, "install", version.LastKnownEnvoy).exec()
+
+		require.NoError(t, err)
+		require.Equal(t, version.LastKnownEnvoy+" is already downloaded\n", stdout)
+		require.Empty(t, stderr)
+	})
+}
+
+func TestGetEnvoyInstall_UnknownVersion(t *testing.T) {
+	stdout, stderr, err := getEnvoy("install", "1.1.1").exec()
+
+	require.EqualError(t, err, "exit status 1")
+	require.Empty(t, stdout)
+	require.Equal(t, fmt.Sprintf(`error: couldn't find version "1.1.1" for platform "%s/%s"
+`, runtime.GOOS, runtime.GOARCH), stderr)
+}
diff --git a/e2e/getenvoy_installed_test.go b/e2e/getenvoy_installed_test.go
new file mode 100644
index 0000000..3f84515
--- /dev/null
+++ b/e2e/getenvoy_installed_test.go
@@ -0,0 +1,55 @@
+// Copyright 2021 Tetrate
+//
+// 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 e2e
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
+)
+
+func TestGetEnvoyInstalled_NothingYet(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
+	defer removeHomeDir()
+
+	stdout, stderr, err := getEnvoy("--home-dir", homeDir, "installed").exec()
+
+	require.NoError(t, err)
+	require.Equal(t, `No envoy versions installed, yet
+`, stdout)
+	require.Empty(t, stderr)
+}
+
+// TestGetEnvoyInstalled verifies output is sorted
+func TestGetEnvoyInstalled(t *testing.T) {
+	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
+	defer removeHomeDir()
+
+	require.NoError(t, os.MkdirAll(filepath.Join(homeDir, "versions", "1.16.1"), 0700))
+	require.NoError(t, os.MkdirAll(filepath.Join(homeDir, "versions", "1.17.2"), 0700))
+
+	stdout, stderr, err := getEnvoy("--home-dir", homeDir, "installed").exec()
+
+	require.NoError(t, err)
+	require.Equal(t, `VERSION
+1.17.2
+1.16.1
+`, stdout)
+	require.Empty(t, stderr)
+}
diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go
index 8178ea8..62c2be2 100644
--- a/internal/cmd/help_test.go
+++ b/internal/cmd/help_test.go
@@ -28,7 +28,7 @@ import (
 )
 
 func TestGetEnvoyHelp(t *testing.T) {
-	for _, command := range []string{"", "install", "versions", "run"} {
+	for _, command := range []string{"", "install", "installed", "versions", "run"} {
 		command := command
 		t.Run(command, func(t *testing.T) {
 			c, stdout, _ := newApp(&globals.GlobalOpts{})
diff --git a/internal/cmd/installed_test.go b/internal/cmd/installed_test.go
new file mode 100644
index 0000000..f15aeb1
--- /dev/null
+++ b/internal/cmd/installed_test.go
@@ -0,0 +1,91 @@
+// Copyright 2021 Tetrate
+//
+// 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_test
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetEnvoyInstalled_NothingYet(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
+	c, stdout, stderr := newApp(o)
+	o.Out = stdout
+	err := c.Run([]string{"getenvoy", "installed"})
+
+	require.NoError(t, err)
+	require.Equal(t, "No envoy versions installed, yet\n", stdout.String())
+	require.Empty(t, stderr)
+}
+
+// TestGetEnvoyInstalled verifies output is sorted
+func TestGetEnvoyInstalled_Sorted(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
+	require.NoError(t, os.MkdirAll(filepath.Join(o.HomeDir, "versions", "1.16.1"), 0700))
+	require.NoError(t, os.MkdirAll(filepath.Join(o.HomeDir, "versions", "1.17.2"), 0700))
+
+	c, stdout, stderr := newApp(o)
+	o.Out = stdout
+	err := c.Run([]string{"getenvoy", "installed"})
+
+	require.NoError(t, err)
+	require.Equal(t, `VERSION
+1.17.2
+1.16.1
+`, stdout.String())
+	require.Empty(t, stderr)
+}
+
+func TestGetEnvoyInstalled_DirIsFile(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
+	require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "versions"), []byte{}, 0700))
+
+	c, stdout, stderr := newApp(o)
+	o.Out = stdout
+	err := c.Run([]string{"getenvoy", "installed"})
+
+	require.Error(t, err)
+	require.Empty(t, stdout)
+	require.Empty(t, stderr)
+}
+
+func TestGetEnvoyInstalled_SkipsFiles(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
+	require.NoError(t, os.MkdirAll(filepath.Join(o.HomeDir, "versions", "1.16.1"), 0700))
+	require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "versions", "1.17.1"), []byte{}, 0700))
+	require.NoError(t, os.MkdirAll(filepath.Join(o.HomeDir, "versions", "1.17.2"), 0700))
+
+	c, stdout, stderr := newApp(o)
+	o.Out = stdout
+	err := c.Run([]string{"getenvoy", "installed"})
+
+	require.NoError(t, err)
+	require.Equal(t, `VERSION
+1.17.2
+1.16.1
+`, stdout.String())
+	require.Empty(t, stderr)
+}
diff --git a/internal/cmd/testdata/getenvoy.md b/internal/cmd/testdata/getenvoy.md
index 172d350..a10ff69 100644
--- a/internal/cmd/testdata/getenvoy.md
+++ b/internal/cmd/testdata/getenvoy.md
@@ -25,7 +25,7 @@ getenvoy [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
 
 **--help, -h**: show help
 
-**--home-dir**="": GetEnvoy home directory (location of downloaded versions and run archives)
+**--home-dir**="": GetEnvoy home directory (location of installed versions and run archives)
 
 **--version, -v**: print the version
 
@@ -44,3 +44,7 @@ List available Envoy versions
 
 Download and install a <version> of Envoy
 
+## installed
+
+List installed Envoy versions
+
diff --git a/internal/cmd/testdata/getenvoy_help.txt b/internal/cmd/testdata/getenvoy_help.txt
index 45a50ab..37aa7a4 100644
--- a/internal/cmd/testdata/getenvoy_help.txt
+++ b/internal/cmd/testdata/getenvoy_help.txt
@@ -8,12 +8,13 @@ VERSION:
    dev
 
 COMMANDS:
-   run       Run Envoy as <version> with <args> as arguments, collecting process state on termination
-   versions  List available Envoy versions
-   install   Download and install a <version> of Envoy
+   run        Run Envoy as <version> with <args> as arguments, collecting process state on termination
+   versions   List available Envoy versions
+   install    Download and install a <version> of Envoy
+   installed  List installed Envoy versions
 
 GLOBAL OPTIONS:
-   --home-dir value            GetEnvoy home directory (location of downloaded versions and run archives) (default: ${HOME}/.getenvoy) [$GETENVOY_HOME]
+   --home-dir value            GetEnvoy home directory (location of installed versions and run archives) (default: ${HOME}/.getenvoy) [$GETENVOY_HOME]
    --envoy-versions-url value  URL of Envoy versions JSON (default: https://getenvoy.io/envoy-versions.json) [$ENVOY_VERSIONS_URL]
    --help, -h                  show help (default: false)
    --version, -v               print the version (default: false)
diff --git a/internal/cmd/testdata/getenvoy_installed_help.txt b/internal/cmd/testdata/getenvoy_installed_help.txt
new file mode 100644
index 0000000..4bbe2c3
--- /dev/null
+++ b/internal/cmd/testdata/getenvoy_installed_help.txt
@@ -0,0 +1,9 @@
+NAME:
+   getenvoy installed - List installed Envoy versions
+
+USAGE:
+   getenvoy installed [command options] [arguments...]
+
+OPTIONS:
+   --help, -h  show help (default: false)
+