Repository: tetratelabs/getenvoy
Pull request #232 (2021-05-24 04:52:53)

Simplifies CLI experience
Simplifies CLI experience

This performs a series of changes similiar to others we've done in order
to polish the experience for a release. Most of these are about reducing
terminology, jargon or code not used to support them.

### `getenvoy list` -> `getenvoy versions`
To reduce the amount of vocab and keep words aligned, this replaces
`getenvoy list` with `getenvoy versions`. The output is only the version
numbers and only those valid for the platform.

### `getenvoy fetch` -> `getenvoy install`
Most tools do not use the term fetch for downloading and extracting
something into the local cache. This uses the term "install" instead,
similar to `nvm` and `brew`.

### Simpler directories
Before, we filed downloaded versions of Envoy in a complicated path
under $ENVOY_HOME. We now follow `nvm`'s example, and simply file under
`$ENVOY_HOME/versions/$version`. This has parity both with the `getenvoy
versions` command and also arg0 of `getenvoy run` and `getenvoy
install`.

`getenvoy run` now archives to `$ENVOY_HOME/runs/$timestamp.tar.gz`

### No more "build" "reference" and "flavor" jargon
Before, we allowed installing and running Envoy on an invalid platforms.
We made a complicated "reference" syntax which allows you to download
invalid platforms, and forced users to use them instead of version
numbers. This completely ends this practice and all associated jargon.

* You can no longer install or run an invalid binary for the host
* There's no more reference or flavor concept, only version
* There is no more teasing people by listing versions they can't run.
* Those who want to fetch versions they can't run can use `jq` and
  `curl`.
* Those who want to have "flavors" can override the manifest URL

### No more `getenvoy run` `--` collisions
Cobra required us to add `--` in the commandline of `getenvoy run`. This
had two problems. One, it is not intuitive and distracting. Two, it
collides with Envoy's `--` arg. As we no longer need Cobra, we can
delete this and with it the collission.

See http://tclap.sourceforge.net/manual.html

follow-up from #195 (reducing jargon and terminology)
follow-up from #203 (making flavor optional first)


codefromthecrypt: Thanks for the review @mathetake I will go through your feedback and anything not voided by the next PR I'll throw in, and comment why if not! https://github.com/tetratelabs/getenvoy/pull/234
codefromthecrypt: ok PTAL when you are free again. thanks @mathetake 

Patch:
diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml
index 4f58abb..56e9863 100644
--- a/.github/workflows/commit.yaml
+++ b/.github/workflows/commit.yaml
@@ -43,7 +43,7 @@ jobs:
           # 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: |
-            ~/.getenvoy/builds
+            ~/.getenvoy/versions
             ~/go/pkg/mod
             ~/go/bin/*-v*
           # '.bingo/*.sum' files generate inconsistently when building `~/go/bin/*-v*`. We key '.bingo/*.mod' instead.
diff --git a/.licenserignore b/.licenserignore
index 44e089a..b095f33 100644
--- a/.licenserignore
+++ b/.licenserignore
@@ -10,6 +10,6 @@ go.sum
 .bingo
 .idea
 
-latest.txt
+envoy.txt
 
-testdata/
\ No newline at end of file
+testdata/
diff --git a/internal/binary/envoy/debug/node.go b/internal/binary/envoy/debug/node.go
index fa99872..95dd921 100644
--- a/internal/binary/envoy/debug/node.go
+++ b/internal/binary/envoy/debug/node.go
@@ -187,7 +187,7 @@ func printProcessTable(out io.Writer, parsed []*proc) error {
 func (n *nodeCollection) networkInterfaces() error {
 	result, err := net.Interfaces()
 	if err != nil {
-		return fmt.Errorf("unable to fetch network Interfaces: %w", err)
+		return fmt.Errorf("unable to list network interfaces: %w", err)
 	}
 
 	if len(result) == 0 {
@@ -221,7 +221,7 @@ var typeMap = map[uint32]string{
 func (n *nodeCollection) activeConnections() error {
 	cs, err := net.Connections("all")
 	if err != nil {
-		return fmt.Errorf("unable to fetch network interfaces: %w", err)
+		return fmt.Errorf("unable to list network connections: %w", err)
 	}
 
 	result := make([]connStat, 0, len(cs))
diff --git a/internal/binary/envoy/fetch.go b/internal/binary/envoy/install.go
similarity index 59%
rename from internal/binary/envoy/fetch.go
rename to internal/binary/envoy/install.go
index 75aa805..6bd95c1 100644
--- a/internal/binary/envoy/fetch.go
+++ b/internal/binary/envoy/install.go
@@ -31,47 +31,65 @@ import (
 
 const binEnvoy = "bin/envoy"
 
-// FetchIfNeeded downloads an Envoy binary corresponding to the given reference and returns a path to it or an error.
-func FetchIfNeeded(o *globals.GlobalOpts, reference string) (string, error) {
-	ref, err := manifest.ParseReference(reference)
-	if err != nil {
-		return "", err
-	}
-
-	platformPath := filepath.Join(o.HomeDir, "builds", ref.Flavor, ref.Version, ref.Platform)
-	envoyPath := filepath.Join(platformPath, binEnvoy)
-	_, err = os.Stat(envoyPath)
+// InstallIfNeeded downloads an Envoy binary corresponding to the given version and returns a path to it or an error.
+func InstallIfNeeded(o *globals.GlobalOpts, p, v string) (string, error) {
+	installPath := filepath.Join(o.HomeDir, "versions", v)
+	envoyPath := filepath.Join(installPath, binEnvoy)
+	_, err := os.Stat(envoyPath)
 	switch {
 	case os.IsNotExist(err):
-		if e := os.MkdirAll(platformPath, 0750); e != nil {
-			return "", fmt.Errorf("unable to create directory %q: %w", platformPath, e)
+		if e := os.MkdirAll(installPath, 0750); e != nil {
+			return "", fmt.Errorf("unable to create directory %q: %w", installPath, e)
 		}
 
-		m, e := manifest.FetchManifest(o.ManifestURL)
+		m, e := manifest.GetManifest(o.ManifestURL)
 		if e != nil {
 			return "", e
 		}
 
-		downloadLocationURL, e := manifest.LocateBuild(ref, m)
+		tarballURL, e := tarballURL(m, p, v)
 		if e != nil {
 			return "", e
 		}
 
-		fmt.Fprintln(o.Out, "downloading", downloadLocationURL) //nolint
-		if e := untarEnvoy(platformPath, downloadLocationURL, o.Out); e != nil {
+		fmt.Fprintln(o.Out, "downloading", tarballURL) //nolint
+		if e := untarEnvoy(installPath, tarballURL, o.Out); e != nil {
 			return "", e
 		}
 	case err == nil:
-		fmt.Fprintln(o.Out, ref, "is already downloaded") //nolint
+		fmt.Fprintln(o.Out, v, "is already downloaded") //nolint
 	default:
 		// TODO: figure out how to get a stat error that isn't file not exist so we can test this
 		return "", err
 	}
-	return verifyEnvoy(platformPath)
+	return verifyEnvoy(installPath)
+}
+
+// tarballURL returns the downloadLocationURL of a tarball for the given runtime.GOOS and Envoy Versionunable to read
+func tarballURL(m *manifest.Manifest, goos, v string) (string, error) {
+	errorNoVersions := fmt.Errorf("couldn't find version %q for platform %q", v, goos)
+	platform := manifest.BuildPlatform(goos)
+	if platform == "" {
+		return "", errorNoVersions
+	}
+
+	// Error if the only released "flavor" or it has no version
+	f, ok := m.Flavors["standard"]
+	if !ok || len(f.Versions) == 0 {
+		return "", errorNoVersions
+	}
+
+	// Error if the version doesn't exist or has no builds for this platform
+	b, ok := f.Versions[v]
+	if !ok || b.Builds[platform] == nil {
+		return "", errorNoVersions
+	}
+
+	return b.Builds[platform].DownloadLocationURL, nil
 }
 
-func verifyEnvoy(platformPath string) (string, error) {
-	envoyPath := filepath.Join(platformPath, binEnvoy)
+func verifyEnvoy(installPath string) (string, error) {
+	envoyPath := filepath.Join(installPath, binEnvoy)
 	stat, err := os.Stat(envoyPath)
 	if err != nil {
 		return "", err
diff --git a/internal/binary/envoy/runtime.go b/internal/binary/envoy/runtime.go
index d27598a..e2d76f9 100644
--- a/internal/binary/envoy/runtime.go
+++ b/internal/binary/envoy/runtime.go
@@ -32,7 +32,7 @@ func NewRuntime(opts *globals.RunOpts) *Runtime {
 	return &Runtime{opts: opts}
 }
 
-// Runtime manages an Envoy lifecycle including fetching (if necessary) and running
+// Runtime manages an Envoy lifecycle
 type Runtime struct {
 	opts *globals.RunOpts
 
diff --git a/internal/binary/envoy/versions.go b/internal/binary/envoy/versions.go
new file mode 100644
index 0000000..13bc2a4
--- /dev/null
+++ b/internal/binary/envoy/versions.go
@@ -0,0 +1,53 @@
+// 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 envoy
+
+import (
+	"fmt"
+	"io"
+	"sort"
+
+	"github.com/tetratelabs/getenvoy/internal/manifest"
+)
+
+// PrintVersions retrieves the manifest from the passed location and writes it to the passed writer
+func PrintVersions(m *manifest.Manifest, goos string, w io.Writer) {
+	p := manifest.BuildPlatform(goos)
+	// print nothing if the only released "flavor" or the platform doesn't exist
+	f, ok := m.Flavors["standard"]
+	if !ok {
+		return
+	}
+
+	// Build a list of versions for this platform
+	var versions []string
+	for _, v := range f.Versions {
+		for _, b := range v.Builds {
+			if p == b.Platform {
+				versions = append(versions, v.Name)
+			}
+		}
+	}
+
+	// Sort lexicographically descending, so that new versions appear first
+	sort.Slice(versions, func(i, j int) bool {
+		return versions[i] > versions[j]
+	})
+
+	// Print the versions
+	for _, v := range versions {
+		fmt.Fprintln(w, v) //nolint
+	}
+}
diff --git a/internal/cmd/app.go b/internal/cmd/app.go
index 3256af8..c76d41c 100644
--- a/internal/cmd/app.go
+++ b/internal/cmd/app.go
@@ -18,11 +18,11 @@ import (
 	"net/url"
 	"os/user"
 	"path/filepath"
+	"regexp"
 
 	"github.com/urfave/cli/v2"
 
 	"github.com/tetratelabs/getenvoy/internal/globals"
-	"github.com/tetratelabs/getenvoy/internal/manifest"
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
@@ -47,7 +47,7 @@ func NewApp(o *globals.GlobalOpts) *cli.App {
 		},
 		&cli.StringFlag{
 			Name:        "manifest",
-			Usage:       "GetEnvoy manifest URL (list of available Envoy builds)",
+			Usage:       "GetEnvoy manifest URL (list of available Envoy versions)",
 			Hidden:      true,
 			DefaultText: globals.DefaultManifestURL,
 			Destination: &manifestURL,
@@ -62,8 +62,8 @@ func NewApp(o *globals.GlobalOpts) *cli.App {
 
 	app.Commands = []*cli.Command{
 		NewRunCmd(o),
-		NewListCmd(o),
-		NewFetchCmd(o),
+		NewVersionsCmd(o),
+		NewInstallCmd(o),
 		NewDocCmd(),
 	}
 	return app
@@ -105,12 +105,13 @@ func setHomeDir(o *globals.GlobalOpts, homeDir string) error {
 	return nil
 }
 
-func validateReferenceArg(c *cli.Context) error {
+func validateVersionArg(c *cli.Context) error {
 	if c.NArg() == 0 {
-		return NewValidationError("missing reference parameter")
+		return NewValidationError("missing <version> argument")
 	}
-	if _, e := manifest.ParseReference(c.Args().First()); e != nil {
-		return NewValidationError(e.Error())
+	v := c.Args().First()
+	if matched, _ := regexp.MatchString("^[1-9][0-9]*\\.[0-9]+\\.[0-9]+$", v); !matched {
+		return NewValidationError("invalid <version> argument: %q should look like %q", v, version.Envoy)
 	}
 	return nil
 }
diff --git a/internal/cmd/errors.go b/internal/cmd/errors.go
index f2d5160..1db52fc 100644
--- a/internal/cmd/errors.go
+++ b/internal/cmd/errors.go
@@ -17,6 +17,8 @@ package cmd
 import "fmt"
 
 // NewValidationError generates an error with a given format string.
+// As noted on ValidationError, this is used by main.go to tell the difference between a validation failure vs a runtime
+// error. We don't want to clutter output with help suggestions if Envoy failed due to a runtime concern.
 func NewValidationError(format string, a ...interface{}) error {
 	return &ValidationError{fmt.Sprintf(format, a...)}
 }
diff --git a/internal/cmd/fetch.go b/internal/cmd/install.go
similarity index 58%
rename from internal/cmd/fetch.go
rename to internal/cmd/install.go
index bf9cb99..96e07f9 100644
--- a/internal/cmd/fetch.go
+++ b/internal/cmd/install.go
@@ -16,6 +16,7 @@ package cmd
 
 import (
 	"fmt"
+	"runtime"
 
 	"github.com/urfave/cli/v2"
 
@@ -24,24 +25,20 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
-// NewFetchCmd create a command responsible for retrieving Envoy binaries
-func NewFetchCmd(o *globals.GlobalOpts) *cli.Command {
+// NewInstallCmd create a command responsible for downloading and extracting Envoy
+func NewInstallCmd(o *globals.GlobalOpts) *cli.Command {
 	return &cli.Command{
-		Name:      "fetch",
-		Usage:     "Download a build of Envoy",
-		ArgsUsage: "<reference>",
-		Description: fmt.Sprintf(`The '<reference>' minimally includes the Envoy version.
-
-Example: Prefetch Envoy prior to invoking `+"`getenvoy run`"+`
-$ getenvoy fetch %[1]s
-
-Example: Fetch Envoy to run on a specific platform
-$ getenvoy fetch %[1]s/linux-glibc
-
-To view all available builds, invoke the "list" command.`, version.Envoy),
-		Before: validateReferenceArg,
+		Name:      "install",
+		Usage:     "Download and install a <version> of Envoy",
+		ArgsUsage: "<version>",
+		Description: fmt.Sprintf(`The '<version>' is from the "versions" command.
+The Envoy <version> will be installed into $GETENVOY_HOME/versions/<version>
+
+Example:
+$ getenvoy install %s`, version.Envoy),
+		Before: validateVersionArg,
 		Action: func(c *cli.Context) error {
-			_, err := envoy.FetchIfNeeded(o, c.Args().First())
+			_, err := envoy.InstallIfNeeded(o, runtime.GOOS, c.Args().First())
 			return err
 		},
 	}
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index 957afa9..bcdbadc 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime"
 	"strconv"
 	"time"
 
@@ -26,26 +27,25 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/binary/envoy"
 	"github.com/tetratelabs/getenvoy/internal/binary/envoy/debug"
 	"github.com/tetratelabs/getenvoy/internal/globals"
-	"github.com/tetratelabs/getenvoy/internal/version"
+	latestversion "github.com/tetratelabs/getenvoy/internal/version"
 )
 
 // NewRunCmd create a command responsible for starting an Envoy process
 func NewRunCmd(o *globals.GlobalOpts) *cli.Command {
 	cmd := &cli.Command{
 		Name:      "run",
-		Usage:     "Run Envoy and collect process state on exit",
-		ArgsUsage: "<reference> -- <envoy-args>",
-		Description: fmt.Sprintf(`The '<reference>' minimally includes the Envoy version.
-The '<envoy-args>' are interpreted by Envoy.
+		Usage:     "Run Envoy as <version> with <args> as arguments, collecting process state on termination",
+		ArgsUsage: "<version> <args>",
+		Description: fmt.Sprintf(`The '<version>' is from the "versions" command and installed if necessary.
+The '<args>' are interpreted by Envoy.
+The Envoy working directory is archived as $GETENVOY_HOME/runs/$epochtime.tar.gz upon termination.
 
 Example:
-$ getenvoy run %[1]s -- --config-path ./bootstrap.yaml
-
-To view all available builds, invoke the "list" command.`, version.Envoy),
-		Before: validateReferenceArg,
+$ getenvoy run %s --config-path ./bootstrap.yaml`, latestversion.Envoy),
+		Before: validateVersionArg,
 		Action: func(c *cli.Context) error {
 			args := c.Args().Slice()
-			if err := initializeRunOpts(o, args[0]); err != nil {
+			if err := initializeRunOpts(o, runtime.GOOS, args[0]); err != nil {
 				return err
 			}
 			r := envoy.NewRuntime(&o.RunOpts)
@@ -57,43 +57,28 @@ To view all available builds, invoke the "list" command.`, version.Envoy),
 				fmt.Fprintln(r.Out, "failed to enable debug option:", err) //nolint
 			}
 
-			envoyArgs := findEnvoyArgs(args)
-			return r.Run(c.Context, envoyArgs)
+			return r.Run(c.Context, args[1:])
 		},
 	}
 	return cmd
 }
 
-// findEnvoyArgs returns any args after "--"
-func findEnvoyArgs(args []string) []string {
-	var envoyArgs []string
-	for i, v := range args {
-		if v == "--" {
-			if len(args) > i+1 {
-				return args[i+1:]
-			}
-			break
-		}
-	}
-	return envoyArgs
-}
-
 // initializeRunOpts allows us to default values when not overridden for tests.
-// The reference parameter corresponds to the globals.GlobalOpts EnvoyPath which is fetched if needed.
+// The version parameter correlates with the globals.GlobalOpts EnvoyPath which is installed if needed.
 // Notably, this creates and sets a globals.GlobalOpts WorkingDirectory for Envoy, and any files that precede it.
-func initializeRunOpts(o *globals.GlobalOpts, reference string) error {
+func initializeRunOpts(o *globals.GlobalOpts, goos, version string) error {
 	runOpts := &o.RunOpts
 	if o.EnvoyPath == "" { // not overridden for tests
-		envoyPath, err := envoy.FetchIfNeeded(o, reference)
+		envoyPath, err := envoy.InstallIfNeeded(o, goos, version)
 		if err != nil {
-			return NewValidationError(err.Error())
+			return err
 		}
 		o.EnvoyPath = envoyPath
 	}
 	if runOpts.WorkingDir == "" { // not overridden for tests
 		// Historically, the directory run files wrote to was called DebugStore
 		runID := strconv.FormatInt(time.Now().UnixNano(), 10)
-		workingDir := filepath.Join(filepath.Join(o.HomeDir, "debug"), runID)
+		workingDir := filepath.Join(filepath.Join(o.HomeDir, "runs"), runID)
 
 		// When the directory is implicitly generated, we should create it to avoid late errors.
 		if err := os.MkdirAll(workingDir, 0750); err != nil {
diff --git a/internal/cmd/list.go b/internal/cmd/versions.go
similarity index 68%
rename from internal/cmd/list.go
rename to internal/cmd/versions.go
index 3ea19e5..3d0bfe1 100644
--- a/internal/cmd/list.go
+++ b/internal/cmd/versions.go
@@ -15,25 +15,27 @@
 package cmd
 
 import (
-	"os"
+	"runtime"
 
 	"github.com/urfave/cli/v2"
 
+	"github.com/tetratelabs/getenvoy/internal/binary/envoy"
 	"github.com/tetratelabs/getenvoy/internal/globals"
 	"github.com/tetratelabs/getenvoy/internal/manifest"
 )
 
-// NewListCmd returns command that lists available Envoy binaries
-func NewListCmd(o *globals.GlobalOpts) *cli.Command {
+// NewVersionsCmd returns command that lists available Envoy versions for the current platform.
+func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command {
 	return &cli.Command{
-		Name:  "list",
-		Usage: "List available Envoy builds",
+		Name:  "versions",
+		Usage: "List available Envoy versions",
 		Action: func(c *cli.Context) error {
-			m, err := manifest.FetchManifest(o.ManifestURL)
+			m, err := manifest.GetManifest(o.ManifestURL)
 			if err != nil {
 				return err
 			}
-			return manifest.Print(m, os.Stdout)
+			envoy.PrintVersions(m, runtime.GOOS, c.App.Writer)
+			return nil
 		},
 	}
 }
diff --git a/internal/globals/globals.go b/internal/globals/globals.go
index 99f3c83..d839e4c 100644
--- a/internal/globals/globals.go
+++ b/internal/globals/globals.go
@@ -18,11 +18,11 @@ import "io"
 
 // RunOpts support invocations of "getenvoy run"
 type RunOpts struct {
-	// EnvoyPath is the exec.Cmd path to "envoy". Defaults to "$HomeDir/builds/$flavor/$version/$platform/bin/envoy"
+	// EnvoyPath is the exec.Cmd path to "envoy". Defaults to "$HomeDir/versions/$version/bin/envoy"
 	EnvoyPath string
 	// WorkingDir is the working directory of EnvoyPath and includes any generated configuration or debug files.
 	// Upon termination, this directory is archived as "../$(basename $WorkingDir).tar.gz"
-	// Defaults to "$HomeDir/debug/$epochtime"
+	// Defaults to "$HomeDir/runs/$epochtime"
 	WorkingDir string
 	// DontArchiveWorkingDir is used in testing and prevents archiving the WorkingDir
 	DontArchiveWorkingDir bool
@@ -40,7 +40,7 @@ type GlobalOpts struct {
 	RunOpts
 	// ManifestURL is the path to the getenvoy manifest json. Defaults to DefaultManifestURL
 	ManifestURL string
-	// HomeDir most importantly contains envoy binaries fetched from ManifestURL. Defaults to DefaultHomeDir
+	// HomeDir most importantly contains Envoy versions installed from ManifestURL. Defaults to DefaultHomeDir
 	HomeDir string
 	// Out is where status messages are written. Defaults to os.Stdout
 	Out io.Writer
diff --git a/internal/manifest/locate.go b/internal/manifest/locate.go
deleted file mode 100644
index 716351b..0000000
--- a/internal/manifest/locate.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2019 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 manifest
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-
-	"github.com/tetratelabs/getenvoy/internal/transport"
-)
-
-// FetchManifest returns a manifest from a remote URL. eg global.manifestURL.
-func FetchManifest(manifestURL string) (*Manifest, error) {
-	// #nosec => This is by design, users can call out to wherever they like!
-	resp, err := transport.Get(manifestURL)
-	if err != nil {
-		return nil, err
-	}
-	defer resp.Body.Close() //nolint
-
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("received %v status code from %v", resp.StatusCode, manifestURL)
-	}
-	body, err := io.ReadAll(resp.Body) // fully read the response
-	if err != nil {
-		return nil, fmt.Errorf("error reading %s: %w", manifestURL, err)
-	}
-
-	result := Manifest{}
-	if err := json.Unmarshal(body, &result); err != nil {
-		return nil, fmt.Errorf("error unmarshalling manifest: %w", err)
-	}
-	return &result, nil
-}
-
-// LocateBuild returns the downloadLocationURL of the associated envoy binary in the manifest using the input key
-func LocateBuild(ref *Reference, manifest *Manifest) (string, error) {
-	// This is pretty horrible... Not sure there is a nicer way though.
-	if manifest.Flavors[ref.Flavor] != nil && manifest.Flavors[ref.Flavor].Versions[ref.Version] != nil {
-		for _, build := range manifest.Flavors[ref.Flavor].Versions[ref.Version].Builds {
-			normalizedPlatform := platformFromEnum(build.Platform)
-			if normalizedPlatform == ref.Platform {
-				return build.DownloadLocationURL, nil
-			}
-		}
-	}
-	return "", fmt.Errorf("unable to find matching GetEnvoy build for reference %q", ref)
-}
diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go
index 9e4419a..f5d5707 100644
--- a/internal/manifest/manifest.go
+++ b/internal/manifest/manifest.go
@@ -14,6 +14,51 @@
 
 package manifest
 
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/tetratelabs/getenvoy/internal/transport"
+)
+
+// GetManifest returns a manifest from a remote URL. eg global.manifestURL.
+func GetManifest(manifestURL string) (*Manifest, error) {
+	// #nosec => This is by design, users can call out to wherever they like!
+	resp, err := transport.Get(manifestURL)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close() //nolint
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("received %v status code from %v", resp.StatusCode, manifestURL)
+	}
+	body, err := io.ReadAll(resp.Body) // fully read the response
+	if err != nil {
+		return nil, fmt.Errorf("error reading %s: %w", manifestURL, err)
+	}
+
+	result := Manifest{}
+	if err := json.Unmarshal(body, &result); err != nil {
+		return nil, fmt.Errorf("error unmarshalling manifest: %w", err)
+	}
+	return &result, nil
+}
+
+// BuildPlatform returns the Build.Platform for the goos (runtime.GOOS value) or "" if not found.
+func BuildPlatform(goos string) string {
+	switch goos {
+	case "darwin":
+		return "DARWIN"
+	case "linux":
+		return "LINUX_GLIBC"
+	default:
+		return "" // no other platform was ever released
+	}
+}
+
 // Manifest is derived from /manifest.proto
 type Manifest struct {
 	ManifestVersion string `json:"manifestVersion,omitempty"`
diff --git a/internal/manifest/platform_darwin.go b/internal/manifest/platform_darwin.go
deleted file mode 100644
index be39256..0000000
--- a/internal/manifest/platform_darwin.go
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright 2019 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 manifest
-
-// currentPlatform is the value of Reference.Platform for this GOOS.
-// We need this function because even if "darwin" matches exactly the platform in the manifest.json, "linux" does not.
-func currentPlatform() string {
-	return "darwin"
-}
diff --git a/internal/manifest/platform_linux.go b/internal/manifest/platform_linux.go
deleted file mode 100644
index d29e1a4..0000000
--- a/internal/manifest/platform_linux.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2019 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 manifest
-
-// currentPlatform is the value of Reference.Platform for this GOOS.
-// Specifically, when "getenvoy" runs and detects GOOS == "linux", we choose the "linux-glibc" Envoy platform.
-// While this name is qualified, it does not effect platform support because Envoy is statically linked.
-func currentPlatform() string {
-	return "linux-glibc"
-}
diff --git a/internal/manifest/print.go b/internal/manifest/print.go
deleted file mode 100644
index 82117ee..0000000
--- a/internal/manifest/print.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2019 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 manifest
-
-import (
-	"fmt"
-	"io"
-	"sort"
-	"text/tabwriter"
-)
-
-// Print retrieves the manifest from the passed location and writes it to the passed writer
-func Print(manifest *Manifest, writer io.Writer) error {
-	w := tabwriter.NewWriter(writer, 0, 8, 5, ' ', 0)
-	fmt.Fprintln(w, "REFERENCE\tVERSION")
-
-	for _, flavor := range deterministicFlavors(manifest.Flavors) {
-		for _, version := range deterministicVersions(flavor.Versions) {
-			for _, build := range deterministicBuilds(version.Builds) {
-				ref := Reference{flavor.Name, version.Name, platformFromEnum(build.Platform)}
-				fmt.Fprintf(w, "%s\t%s\n", ref.String(), version.Name)
-			}
-		}
-	}
-	return w.Flush()
-}
-
-func deterministicFlavors(flavors map[string]*Flavor) []*Flavor {
-	flavorList := make([]*Flavor, 0, len(flavors))
-	for _, flavor := range flavors {
-		flavorList = append(flavorList, flavor)
-	}
-	sort.Slice(flavorList, func(i, j int) bool {
-		return flavorList[i].Name < flavorList[j].Name
-	})
-	return flavorList
-}
-
-func deterministicVersions(versions map[string]*Version) []*Version {
-	versionList := make([]*Version, 0, len(versions))
-	for _, version := range versions {
-		versionList = append(versionList, version)
-	}
-	sort.Slice(versionList, func(i, j int) bool {
-		// Note: version is reverse alphabetical so that newer versions are first
-		return versionList[i].Name > versionList[j].Name
-	})
-	return versionList
-}
-
-func deterministicBuilds(builds map[string]*Build) []*Build {
-	buildList := make([]*Build, 0, len(builds))
-	for _, build := range builds {
-		buildList = append(buildList, build)
-	}
-	sort.Slice(buildList, func(i, j int) bool {
-		// Note: build is reverse alphabetical so that Linux versions are before Darwin
-		return fmt.Sprintf("%v", buildList[i]) > fmt.Sprintf("%v", buildList[j])
-	})
-	return buildList
-}
diff --git a/internal/manifest/reference.go b/internal/manifest/reference.go
deleted file mode 100644
index c22f801..0000000
--- a/internal/manifest/reference.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2020 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 manifest
-
-import (
-	"fmt"
-	"regexp"
-	"strings"
-)
-
-// Reference identifies an Envoy release provided by getenvoy.io.
-type Reference struct {
-	Flavor  string
-	Version string
-	// Platform is an lower-hyphen representation of the platform. Ex. "darwin" or "linux-glibc"
-	Platform string
-}
-
-// platformFromEnum takes a Reference.Platform like "LINUX_GLIBC" and returns a string variant like "linux-glibc"
-func platformFromEnum(s string) string {
-	s = strings.ToLower(s)
-	s = strings.ReplaceAll(s, "_", "-")
-	return s
-}
-
-var (
-	referenceFormat = regexp.MustCompile(`^([^:]+\:)?([\w\d-\._]+)/?([\w\d-\._]+)?$`)
-)
-
-// ParseReference parses a given text as a Reference.
-func ParseReference(text string) (*Reference, error) {
-	matches := referenceFormat.FindStringSubmatch(text)
-	if len(matches) != 4 {
-		return nil, fmt.Errorf("%q is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]", text)
-	}
-	flavor := strings.ToLower(matches[1])
-	if flavor == "" {
-		flavor = "standard"
-	} else {
-		flavor = flavor[:len(flavor)-1]
-	}
-	// Even though versions are usually semver, we currently allow anything in the version. Ex "latest" "1.19.2-alpha"
-	version := strings.ToLower(matches[2])
-	platform := platformFromEnum(matches[3])
-	// If platform is empty, fill it in.
-	if platform == "" {
-		platform = currentPlatform()
-	}
-	return &Reference{flavor, version, platform}, nil
-}
-
-// String returns a plain version optionally platform qualified. Ex "1.17.2" or "1.17.2/darwin"
-// Reference.Flavor is prefixed with a colon when non-standard. Ex "experimental:1.18.2"
-func (r *Reference) String() string {
-	if r.Flavor == "standard" { // don't clutter output in normal case
-		return fmt.Sprintf("%s/%s", r.Version, r.Platform)
-	}
-	return fmt.Sprintf("%s:%s/%s", r.Flavor, r.Version, r.Platform)
-}


Test patch:
diff --git a/e2e/getenvoy_run_test.go b/e2e/getenvoy_run_test.go
index 2a2b362..ab1afab 100644
--- a/e2e/getenvoy_run_test.go
+++ b/e2e/getenvoy_run_test.go
@@ -40,7 +40,7 @@ func TestGetEnvoyRun(t *testing.T) {
 	c := getEnvoy(`run`)
 	// Below is the minimal config needed to run envoy
 	// TODO allow implicit version #106
-	c.args(version.Envoy, `--`, `--config-yaml`, `admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}`)
+	c.args(version.Envoy, `--config-yaml`, `admin: {access_log_path: '/dev/stdout', address: {socket_address: {address: '127.0.0.1', port_value: 0}}}`)
 
 	stdout, stderr, terminate := c.start(t, terminateTimeout)
 
@@ -94,21 +94,21 @@ func requireEnvoyReady(t *testing.T, envoyWorkingDir string, stderr io.Reader, c
 }
 
 func verifyDebugDump(t *testing.T, workingDir string, c interface{}) {
-	// Run deletes the debug store directory after making a tar.gz with the same name.
+	// Run deletes the working directory after making a tar.gz with the same name.
 	// Restore it so assertions can read the contents later.
-	debugArchive := filepath.Join(workingDir + ".tar.gz")
-	defer os.Remove(debugArchive) //nolint
+	runArchive := filepath.Join(workingDir + ".tar.gz")
+	defer os.Remove(runArchive) //nolint
 
-	src, err := os.Open(debugArchive)
-	require.NoError(t, err, "error opening %s after stopping [%v]", debugArchive, c)
+	src, err := os.Open(runArchive)
+	require.NoError(t, err, "error opening %s after stopping [%v]", runArchive, c)
 	e := tar.Untar(workingDir, src)
-	require.NoError(t, e, "error restoring %s from %s after stopping [%v]", workingDir, debugArchive, c)
+	require.NoError(t, e, "error restoring %s from %s after stopping [%v]", workingDir, runArchive, c)
 
 	// ensure the minimum contents exist
 	for _, filename := range []string{"config_dump.json", "stats.json"} {
 		path := filepath.Join(workingDir, filename)
 		f, err := os.Stat(path)
-		require.NoError(t, err, `debug archive %s doesn't contain %s after stopping [%v]`, debugArchive, filename, c)
+		require.NoError(t, err, `run archive %s doesn't contain %s after stopping [%v]`, runArchive, filename, c)
 		require.NotEmpty(t, f.Size(), `%s was empty after stopping [%v]`, filename, c)
 	}
 }
diff --git a/e2e/getenvoy_test.go b/e2e/getenvoy_test.go
index 07a7949..5c8431c 100644
--- a/e2e/getenvoy_test.go
+++ b/e2e/getenvoy_test.go
@@ -15,9 +15,12 @@
 package e2e
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
 func TestGetEnvoyVersion(t *testing.T) {
@@ -30,12 +33,12 @@ func TestGetEnvoyVersion(t *testing.T) {
 	require.NoError(t, err)
 }
 
-func TestGetEnvoyList(t *testing.T) {
+func TestGetEnvoyVersions(t *testing.T) {
 	t.Parallel()
 
-	stdout, stderr, err := getEnvoy("list").exec()
+	stdout, stderr, err := getEnvoy("versions").exec()
 
-	require.Regexp(t, `REFERENCE +VERSION\n`, stdout)
+	require.Contains(t, stdout, fmt.Sprintln(version.Envoy))
 	require.Empty(t, stderr)
 	require.NoError(t, err)
 }
diff --git a/internal/binary/envoy/fetch_test.go b/internal/binary/envoy/install_test.go
similarity index 71%
rename from internal/binary/envoy/fetch_test.go
rename to internal/binary/envoy/install_test.go
index 5dd9726..15b65e2 100644
--- a/internal/binary/envoy/fetch_test.go
+++ b/internal/binary/envoy/install_test.go
@@ -23,6 +23,7 @@ import (
 	"net/http/httptest"
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -30,13 +31,7 @@ import (
 	"github.com/tetratelabs/getenvoy/internal/globals"
 	manifesttest "github.com/tetratelabs/getenvoy/internal/test/manifest"
 	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
-)
-
-const (
-	// This is only for unit testing: we don't need to use latest.
-	version = "1.17.2"
-	// Hard code even if the unit test env isn't darwin. This avoids drifts like linux vs linux-glibc
-	platform = "darwin"
+	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
 func TestUntarEnvoyError(t *testing.T) {
@@ -80,26 +75,57 @@ func TestUntarEnvoyError(t *testing.T) {
 
 // TestUntarEnvoy doesn't test compression formats because that logic is in tar.Tar
 func TestUntarEnvoy(t *testing.T) {
-	o, cleanup := setupTest(t)
+	o, cleanup := setupInstallTest(t)
 	defer cleanup()
 
 	out := new(bytes.Buffer)
-	e := untarEnvoy(o.tempDir, o.envoyURL, out)
+	e := untarEnvoy(o.tempDir, o.tarballURL, out)
 	require.NoError(t, e)
 	require.FileExists(t, filepath.Join(o.tempDir, binEnvoy))
 	require.Contains(t, out.String(), `100% |████████████████████████████████████████|`)
 }
 
-func TestFetchIfNeeded_ErrorOnIncorrectURL(t *testing.T) {
-	o, cleanup := setupTest(t)
+func TestInstallIfNeeded_ErrorOnIncorrectURL(t *testing.T) {
+	o, cleanup := setupInstallTest(t)
 	defer cleanup()
 
 	o.ManifestURL += "/mannyfest.json"
-	_, e := FetchIfNeeded(&o.GlobalOpts, o.reference)
+
+	_, e := InstallIfNeeded(&o.GlobalOpts, runtime.GOOS, version.Envoy)
 	require.EqualError(t, e, "received 404 status code from "+o.ManifestURL)
 	require.Empty(t, o.Out.(*bytes.Buffer))
 }
 
+func TestInstallIfNeeded_Validates(t *testing.T) {
+	o, cleanup := setupInstallTest(t)
+	defer cleanup()
+
+	tests := []struct{ name, goos, version, expectedErr string }{
+		{
+			name:        "invalid version",
+			goos:        "darwin",
+			version:     `1.1.1`,
+			expectedErr: `couldn't find version "1.1.1" for platform "darwin"`,
+		},
+		{
+			name:        "unsupported OS",
+			goos:        "windows",
+			version:     version.Envoy,
+			expectedErr: fmt.Sprintf(`couldn't find version %q for platform "windows"`, version.Envoy),
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			o.Out = new(bytes.Buffer)
+			_, e := InstallIfNeeded(&o.GlobalOpts, tc.goos, tc.version)
+			require.EqualError(t, e, tc.expectedErr)
+			require.Empty(t, o.Out.(*bytes.Buffer))
+		})
+	}
+}
+
 // progressReader emits a progress bar, when the length is known
 func TestProgressReader(t *testing.T) {
 	var out bytes.Buffer
@@ -122,22 +148,22 @@ func TestProgressReader_UnknownLength(t *testing.T) {
 	require.NotContains(t, out.String(), "100% |████████████████████████████████████████|")
 }
 
-func TestFetchIfNeeded(t *testing.T) {
-	o, cleanup := setupTest(t)
+func TestInstallIfNeeded(t *testing.T) {
+	o, cleanup := setupInstallTest(t)
 	defer cleanup()
 	out := o.Out.(*bytes.Buffer)
 
-	envoyPath, e := FetchIfNeeded(&o.GlobalOpts, o.reference)
+	envoyPath, e := InstallIfNeeded(&o.GlobalOpts, runtime.GOOS, version.Envoy)
 	require.NoError(t, e)
 	require.Equal(t, o.EnvoyPath, envoyPath)
 	require.FileExists(t, envoyPath)
 
-	require.Contains(t, out.String(), o.envoyURL)
+	require.Contains(t, out.String(), o.tarballURL)
 	require.Contains(t, out.String(), "100% |████████████████████████████████████████|")
 }
 
-func TestFetchIfNeeded_AlreadyExists(t *testing.T) {
-	o, cleanup := setupTest(t)
+func TestInstallIfNeeded_AlreadyExists(t *testing.T) {
+	o, cleanup := setupInstallTest(t)
 	defer cleanup()
 	out := o.Out.(*bytes.Buffer)
 
@@ -147,9 +173,9 @@ func TestFetchIfNeeded_AlreadyExists(t *testing.T) {
 	envoyStat, err := os.Stat(o.EnvoyPath)
 	require.NoError(t, err)
 
-	envoyPath, e := FetchIfNeeded(&o.GlobalOpts, o.reference)
+	envoyPath, e := InstallIfNeeded(&o.GlobalOpts, runtime.GOOS, version.Envoy)
 	require.NoError(t, e)
-	require.Equal(t, fmt.Sprintf("%s/%s is already downloaded\n", version, platform), out.String())
+	require.Equal(t, fmt.Sprintln(version.Envoy, "is already downloaded"), out.String())
 
 	newStat, e := os.Stat(envoyPath)
 	require.NoError(t, e)
@@ -162,57 +188,53 @@ func TestVerifyEnvoy(t *testing.T) {
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
 
-	platformPath := filepath.Join(tempDir, "platform")
-	require.NoError(t, os.MkdirAll(filepath.Join(platformPath, "bin"), 0755))
+	envoyPath := filepath.Join(tempDir, "versions", version.Envoy)
+	require.NoError(t, os.MkdirAll(filepath.Join(envoyPath, "bin"), 0755))
 	t.Run("envoy binary doesn't exist", func(t *testing.T) {
-		EnvoyPath, e := verifyEnvoy(platformPath)
+		EnvoyPath, e := verifyEnvoy(envoyPath)
 		require.Empty(t, EnvoyPath)
 		require.Contains(t, e.Error(), "no such file or directory")
 	})
 
-	expectedEnvoyPath := filepath.Join(platformPath, "bin", "envoy")
+	expectedEnvoyPath := filepath.Join(envoyPath, "bin", "envoy")
 	require.NoError(t, os.WriteFile(expectedEnvoyPath, []byte{}, 0600))
 	t.Run("envoy binary not executable", func(t *testing.T) {
-		EnvoyPath, e := verifyEnvoy(platformPath)
+		EnvoyPath, e := verifyEnvoy(envoyPath)
 		require.Empty(t, EnvoyPath)
 		require.EqualError(t, e, fmt.Sprintf(`envoy binary not executable at %q`, expectedEnvoyPath))
 	})
 
 	require.NoError(t, os.Chmod(expectedEnvoyPath, 0750))
 	t.Run("envoy binary ok", func(t *testing.T) {
-		EnvoyPath, e := verifyEnvoy(platformPath)
+		EnvoyPath, e := verifyEnvoy(envoyPath)
 		require.Equal(t, expectedEnvoyPath, EnvoyPath)
 		require.Nil(t, e)
 	})
 }
 
-type manifestTest struct {
+type installTest struct {
 	globals.GlobalOpts
-	tempDir, reference, envoyURL string
+	tempDir, tarballURL string
 }
 
-func setupTest(t *testing.T) (*manifestTest, func()) {
+func setupInstallTest(t *testing.T) (*installTest, func()) {
 	var tearDown []func()
 
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	tearDown = append(tearDown, removeTempDir)
 
-	ref := fmt.Sprintf("standard:%s/%s", version, platform)
-	m, err := manifesttest.NewSimpleManifest(ref)
-	require.NoError(t, err)
-	manifestServer := manifesttest.RequireManifestTestServer(t, m)
+	manifestServer := manifesttest.RequireManifestTestServer(t, version.Envoy)
 	tearDown = append(tearDown, manifestServer.Close)
 
-	return &manifestTest{
-			tempDir:   tempDir,
-			reference: ref,
-			envoyURL:  fmt.Sprintf("%s/builds/%s/%s.tar.gz", manifestServer.URL, version, platform),
+	return &installTest{
+			tempDir:    tempDir,
+			tarballURL: manifesttest.TarballURL(manifestServer.URL, runtime.GOOS, version.Envoy),
 			GlobalOpts: globals.GlobalOpts{
 				HomeDir:     tempDir,
 				ManifestURL: manifestServer.URL + "/manifest.json",
 				Out:         new(bytes.Buffer),
 				RunOpts: globals.RunOpts{
-					EnvoyPath: filepath.Join(tempDir, "builds", "standard", version, platform, "bin", "envoy"),
+					EnvoyPath: filepath.Join(tempDir, "versions", version.Envoy, "bin", "envoy"),
 				},
 			},
 		}, func() {
diff --git a/internal/binary/envoy/run_test.go b/internal/binary/envoy/run_test.go
index 32b367e..37b36b4 100644
--- a/internal/binary/envoy/run_test.go
+++ b/internal/binary/envoy/run_test.go
@@ -35,9 +35,9 @@ func TestRuntime_Run(t *testing.T) {
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
 
-	debugDir := filepath.Join(tempDir, "debug")
+	runsDir := filepath.Join(tempDir, "runs")
 	fakeTimestamp := "1619574747231823000"
-	workingDir := filepath.Join(debugDir, fakeTimestamp)
+	workingDir := filepath.Join(runsDir, fakeTimestamp)
 
 	// "quiet" as we aren't testing the environment envoy runs in
 	fakeEnvoy := filepath.Join(tempDir, "quiet")
@@ -100,12 +100,12 @@ working directory: %s
 			require.Equal(t, tc.expectedStdout, stdout.String())
 			require.Equal(t, tc.expectedStderr, stderr.String())
 
-			// Ensure the working directory was deleted, and the debug directory only contains the archive
-			files, err := os.ReadDir(debugDir)
+			// Ensure the working directory was deleted, and the "run" directory only contains the archive
+			files, err := os.ReadDir(runsDir)
 			require.NoError(t, err)
 			require.Equal(t, 1, len(files))
-			archive := filepath.Join(debugDir, files[0].Name())
-			require.Equal(t, filepath.Join(debugDir, fakeTimestamp+".tar.gz"), archive)
+			archive := filepath.Join(runsDir, files[0].Name())
+			require.Equal(t, filepath.Join(runsDir, fakeTimestamp+".tar.gz"), archive)
 
 			// Cleanup for the next run
 			require.NoError(t, os.Remove(archive))
diff --git a/internal/binary/envoy/versions_test.go b/internal/binary/envoy/versions_test.go
new file mode 100644
index 0000000..f8aedf0
--- /dev/null
+++ b/internal/binary/envoy/versions_test.go
@@ -0,0 +1,124 @@
+// 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 envoy
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/internal/manifest"
+)
+
+func TestPrintVersions(t *testing.T) {
+	tests := []struct {
+		name, goos string
+		manifest   *manifest.Manifest
+		expected   string
+	}{
+		{
+			name:     "darwin",
+			goos:     "darwin",
+			manifest: goodManifest,
+			expected: `1.18.3
+1.14.6
+`,
+		},
+		{
+			name:     "linux",
+			goos:     "linux",
+			manifest: goodManifest,
+			expected: `1.17.1
+1.14.6
+`,
+		},
+		{
+			name:     "unsupported OS",
+			goos:     "windows",
+			manifest: goodManifest,
+		},
+		{
+			name:     "empty manifest",
+			goos:     "darwin",
+			manifest: &manifest.Manifest{},
+			expected: ``,
+		},
+		{
+			name: "non-standard manifest",
+			goos: "darwin",
+			manifest: &manifest.Manifest{
+				ManifestVersion: "v0.1.0",
+				Flavors: map[string]*manifest.Flavor{
+					"non-standard": {
+						Name:     "non-standard",
+						Versions: goodManifest.Flavors["standard"].Versions,
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			out := new(bytes.Buffer)
+			PrintVersions(tc.manifest, tc.goos, out)
+			require.Equal(t, tc.expected, out.String())
+		})
+	}
+}
+
+var goodManifest = &manifest.Manifest{
+	ManifestVersion: "v0.1.0",
+	Flavors: map[string]*manifest.Flavor{
+		"standard": {
+			Name: "standard",
+			Versions: map[string]*manifest.Version{
+				"1.14.6": {
+					Name: "1.14.6",
+					Builds: map[string]*manifest.Build{
+						"LINUX_GLIBC": {
+							Platform:            "LINUX_GLIBC",
+							DownloadLocationURL: "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-linux-x86_64.tar.gz",
+						},
+						"DARWIN": {
+							Platform:            "DARWIN",
+							DownloadLocationURL: "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-darwin-x86_64.tar.gz",
+						},
+					},
+				},
+				"1.17.1": {
+					Name: "1.17.1",
+					Builds: map[string]*manifest.Build{
+						"LINUX_GLIBC": {
+							Platform:            "LINUX_GLIBC",
+							DownloadLocationURL: "https://getenvoy.io/versions/1.17.1/envoy-1.17.1-linux-x86_64.tar.gz",
+						},
+					},
+				},
+				"1.18.3": {
+					Name: "1.18.3",
+					Builds: map[string]*manifest.Build{
+						"DARWIN": {
+							Platform:            "DARWIN",
+							DownloadLocationURL: "https://getenvoy.io/versions/1.18.3/envoy-1.18.3-darwin-x86_64.tar.gz",
+						},
+					},
+				},
+			},
+		},
+	},
+}
diff --git a/internal/cmd/app_test.go b/internal/cmd/app_test.go
index f5da829..3b82405 100644
--- a/internal/cmd/app_test.go
+++ b/internal/cmd/app_test.go
@@ -16,6 +16,7 @@ package cmd_test
 
 import (
 	"bytes"
+	"io"
 	"os"
 	"os/user"
 	"path/filepath"
@@ -26,6 +27,9 @@ import (
 
 	rootcmd "github.com/tetratelabs/getenvoy/internal/cmd"
 	"github.com/tetratelabs/getenvoy/internal/globals"
+	manifesttest "github.com/tetratelabs/getenvoy/internal/test/manifest"
+	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
+	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
 func TestGetEnvoyValidateArgs(t *testing.T) {
@@ -201,3 +205,28 @@ func runTestCommand(t *testing.T, o *globals.GlobalOpts, args []string) error {
 	require.Empty(t, stderr)
 	return err
 }
+
+// setupTest returns globals.GlobalOpts and a tear-down function.
+// The tear-down functions reverts side-effects such as temp directories and a fake manifest server.
+func setupTest(t *testing.T) (*globals.GlobalOpts, func()) {
+	result := globals.GlobalOpts{}
+	result.Out = io.Discard // ignore logging by default
+	var tearDown []func()
+
+	tempDir, deleteTempDir := morerequire.RequireNewTempDir(t)
+	tearDown = append(tearDown, deleteTempDir)
+
+	result.HomeDir = filepath.Join(tempDir, "envoy_home")
+	err := os.Mkdir(result.HomeDir, 0700)
+	require.NoError(t, err, `error creating directory: %s`, result.HomeDir)
+
+	manifestServer := manifesttest.RequireManifestTestServer(t, version.Envoy)
+	result.ManifestURL = manifestServer.URL + "/manifest.json"
+	tearDown = append(tearDown, manifestServer.Close)
+
+	return &result, func() {
+		for i := len(tearDown) - 1; i >= 0; i-- {
+			tearDown[i]()
+		}
+	}
+}
diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go
index 47e2436..753b541 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{"", "fetch", "list", "run"} {
+	for _, command := range []string{"", "install", "versions", "run"} {
 		command := command
 		t.Run(command, func(t *testing.T) {
 			c, stdout, _ := newApp(&globals.GlobalOpts{})
@@ -45,7 +45,7 @@ func TestGetEnvoyHelp(t *testing.T) {
 
 			want, err := os.ReadFile(filepath.Join("testdata", expected))
 			require.NoError(t, err)
-			require.Equal(t, strings.ReplaceAll(string(want), "ENVOY_VERSION", version.Envoy), stdout.String())
+			require.Equal(t, strings.ReplaceAll(string(want), "{ENVOY_VERSION}", version.Envoy), stdout.String())
 		})
 	}
 }
diff --git a/internal/cmd/run_test.go b/internal/cmd/run_test.go
index 8df974d..a2871fb 100644
--- a/internal/cmd/run_test.go
+++ b/internal/cmd/run_test.go
@@ -16,21 +16,17 @@ package cmd_test
 
 import (
 	"fmt"
-	"io"
-	"os"
-	"path/filepath"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 
-	"github.com/tetratelabs/getenvoy/internal/globals"
-	"github.com/tetratelabs/getenvoy/internal/manifest"
-	manifesttest "github.com/tetratelabs/getenvoy/internal/test/manifest"
-	"github.com/tetratelabs/getenvoy/internal/test/morerequire"
 	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
 func TestGetEnvoyRunValidateFlag(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
 	tests := []struct {
 		name        string
 		args        []string
@@ -39,12 +35,12 @@ func TestGetEnvoyRunValidateFlag(t *testing.T) {
 		{
 			name:        "arg[0] missing",
 			args:        []string{"getenvoy", "run"},
-			expectedErr: `missing reference parameter`,
+			expectedErr: `missing <version> argument`,
 		},
 		{
-			name:        "arg[0] with invalid reference",
-			args:        []string{"getenvoy", "run", "???"},
-			expectedErr: `"???" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]`,
+			name:        "arg[0] with invalid version",
+			args:        []string{"getenvoy", "run", "unknown"},
+			expectedErr: fmt.Sprintf(`invalid <version> argument: "unknown" should look like "%s"`, version.Envoy),
 		},
 	}
 
@@ -53,7 +49,7 @@ func TestGetEnvoyRunValidateFlag(t *testing.T) {
 
 		t.Run(test.name, func(t *testing.T) {
 			// Run "getenvoy run"
-			c, stdout, stderr := newApp(&globals.GlobalOpts{})
+			c, stdout, stderr := newApp(o)
 			err := c.Run(test.args)
 
 			// Verify the command failed with the expected error
@@ -77,11 +73,11 @@ func TestGetEnvoyRun(t *testing.T) {
 		},
 		{
 			name: "empty envoy args",
-			args: []string{"getenvoy", "run", version.Envoy, "--"},
+			args: []string{"getenvoy", "run", version.Envoy},
 		},
 		{
 			name:              "envoy args",
-			args:              []string{"getenvoy", "run", version.Envoy, "--", "-c", "envoy.yaml"},
+			args:              []string{"getenvoy", "run", version.Envoy, "-c", "envoy.yaml"},
 			expectedEnvoyArgs: ` -c envoy.yaml`,
 		},
 	}
@@ -93,8 +89,8 @@ func TestGetEnvoyRun(t *testing.T) {
 			o, cleanup := setupTest(t)
 			defer cleanup()
 
-			// Run "getenvoy run 1.17.1 -- -c envoy.yaml"
-			c, stdout, stderr := newApp(&o.GlobalOpts)
+			// Run "getenvoy run 1.17.1 -c envoy.yaml"
+			c, stdout, stderr := newApp(o)
 			err := c.Run(test.args)
 
 			// Verify the command invoked, passing the correct default commandline
@@ -117,55 +113,15 @@ func TestGetEnvoyRunFailWithUnknownVersion(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
 
-	o.EnvoyPath = "" // force lookup of reference flag
-	c, stdout, stderr := newApp(&o.GlobalOpts)
+	o.EnvoyPath = "" // force lookup of version flag
+	c, stdout, stderr := newApp(o)
 
 	// Run "getenvoy run unknown"
 	err := c.Run([]string{"getenvoy", "run", "unknown"})
 
 	// Verify the command failed with the expected error.
-	reference := "unknown/" + o.platform
-	expectedErr := fmt.Sprintf(`unable to find matching GetEnvoy build for reference "%s"`, reference)
-	require.EqualError(t, err, expectedErr)
+	require.EqualError(t, err, fmt.Sprintf(`invalid <version> argument: "unknown" should look like "%s"`, version.Envoy))
 	// GetEnvoy handles logging of errors, so we expect nothing in stdout or stderr
 	require.Empty(t, stdout)
 	require.Empty(t, stderr)
 }
-
-type testEnvoyExtensionConfig struct {
-	globals.GlobalOpts
-	// platform is the types.Reference.Platform used in manifest commands
-	platform string
-}
-
-// setupTest returns testEnvoyExtensionConfig and a tear-down function.
-// The tear-down functions reverts side-effects such as temp directories and a fake manifest server.
-func setupTest(t *testing.T) (*testEnvoyExtensionConfig, func()) {
-	result := testEnvoyExtensionConfig{}
-	result.Out = io.Discard // ignore logging
-	var tearDown []func()
-
-	tempDir, deleteTempDir := morerequire.RequireNewTempDir(t)
-	tearDown = append(tearDown, deleteTempDir)
-
-	result.HomeDir = filepath.Join(tempDir, "envoy_home")
-	err := os.Mkdir(result.HomeDir, 0700)
-	require.NoError(t, err, `error creating directory: %s`, result.HomeDir)
-
-	ref, err := manifest.ParseReference(version.Envoy)
-	require.NoError(t, err, `error resolving manifest for reference: %s`, ref)
-	result.platform = ref.Platform
-
-	testManifest, err := manifesttest.NewSimpleManifest(ref.String())
-	require.NoError(t, err, `error creating test manifest`)
-
-	manifestServer := manifesttest.RequireManifestTestServer(t, testManifest)
-	result.ManifestURL = manifestServer.URL + "/manifest.json"
-	tearDown = append(tearDown, manifestServer.Close)
-
-	return &result, func() {
-		for i := len(tearDown) - 1; i >= 0; i-- {
-			tearDown[i]()
-		}
-	}
-}
diff --git a/internal/cmd/testdata/getenvoy.md b/internal/cmd/testdata/getenvoy.md
index ab18229..db1ffe6 100644
--- a/internal/cmd/testdata/getenvoy.md
+++ b/internal/cmd/testdata/getenvoy.md
@@ -31,13 +31,13 @@ getenvoy [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
 
 ## run
 
-Run Envoy and collect process state on exit
+Run Envoy as <version> with <args> as arguments, collecting process state on termination
 
-## list
+## versions
 
-List available Envoy builds
+List available Envoy versions
 
-## fetch
+## install
 
-Download a build of Envoy
+Download and install a <version> of Envoy
 
diff --git a/internal/cmd/testdata/getenvoy_fetch_help.txt b/internal/cmd/testdata/getenvoy_fetch_help.txt
deleted file mode 100644
index c8c409e..0000000
--- a/internal/cmd/testdata/getenvoy_fetch_help.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-NAME:
-   getenvoy fetch - Download a build of Envoy
-
-USAGE:
-   getenvoy fetch [command options] <reference>
-
-DESCRIPTION:
-   The '<reference>' minimally includes the Envoy version.
-   
-   Example: Prefetch Envoy prior to invoking `getenvoy run`
-   $ getenvoy fetch 1.17.1
-   
-   Example: Fetch Envoy to run on a specific platform
-   $ getenvoy fetch 1.17.1/linux-glibc
-   
-   To view all available builds, invoke the "list" command.
-
-OPTIONS:
-   --help, -h  show help (default: false)
-   
diff --git a/internal/cmd/testdata/getenvoy_help.txt b/internal/cmd/testdata/getenvoy_help.txt
index 46108d3..eedf788 100644
--- a/internal/cmd/testdata/getenvoy_help.txt
+++ b/internal/cmd/testdata/getenvoy_help.txt
@@ -8,9 +8,9 @@ VERSION:
    dev
 
 COMMANDS:
-   run    Run Envoy and collect process state on exit
-   list   List available Envoy builds
-   fetch  Download a build 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
 
 GLOBAL OPTIONS:
    --home-dir value  GetEnvoy home directory (location of downloaded artifacts, caches, etc) (default: ${HOME}/.getenvoy) [$GETENVOY_HOME]
diff --git a/internal/cmd/testdata/getenvoy_install_help.txt b/internal/cmd/testdata/getenvoy_install_help.txt
new file mode 100644
index 0000000..bc15801
--- /dev/null
+++ b/internal/cmd/testdata/getenvoy_install_help.txt
@@ -0,0 +1,16 @@
+NAME:
+   getenvoy install - Download and install a <version> of Envoy
+
+USAGE:
+   getenvoy install [command options] <version>
+
+DESCRIPTION:
+   The '<version>' is from the "versions" command.
+   The Envoy <version> will be installed into $GETENVOY_HOME/versions/<version>
+   
+   Example:
+   $ getenvoy install {ENVOY_VERSION}
+
+OPTIONS:
+   --help, -h  show help (default: false)
+   
diff --git a/internal/cmd/testdata/getenvoy_list_help.txt b/internal/cmd/testdata/getenvoy_list_help.txt
deleted file mode 100644
index 4961adc..0000000
--- a/internal/cmd/testdata/getenvoy_list_help.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-NAME:
-   getenvoy list - List available Envoy builds
-
-USAGE:
-   getenvoy list [command options] [arguments...]
-
-OPTIONS:
-   --help, -h  show help (default: false)
-   
diff --git a/internal/cmd/testdata/getenvoy_run_help.txt b/internal/cmd/testdata/getenvoy_run_help.txt
index f733052..1540b50 100644
--- a/internal/cmd/testdata/getenvoy_run_help.txt
+++ b/internal/cmd/testdata/getenvoy_run_help.txt
@@ -1,17 +1,16 @@
 NAME:
-   getenvoy run - Run Envoy and collect process state on exit
+   getenvoy run - Run Envoy as <version> with <args> as arguments, collecting process state on termination
 
 USAGE:
-   getenvoy run [command options] <reference> -- <envoy-args>
+   getenvoy run [command options] <version> <args>
 
 DESCRIPTION:
-   The '<reference>' minimally includes the Envoy version.
-   The '<envoy-args>' are interpreted by Envoy.
+   The '<version>' is from the "versions" command and installed if necessary.
+   The '<args>' are interpreted by Envoy.
+   The Envoy working directory is archived as $GETENVOY_HOME/runs/$epochtime.tar.gz upon termination.
    
    Example:
-   $ getenvoy run 1.17.1 -- --config-path ./bootstrap.yaml
-   
-   To view all available builds, invoke the "list" command.
+   $ getenvoy run {ENVOY_VERSION} --config-path ./bootstrap.yaml
 
 OPTIONS:
    --help, -h  show help (default: false)
diff --git a/internal/cmd/testdata/getenvoy_versions_help.txt b/internal/cmd/testdata/getenvoy_versions_help.txt
new file mode 100644
index 0000000..1e84cd1
--- /dev/null
+++ b/internal/cmd/testdata/getenvoy_versions_help.txt
@@ -0,0 +1,9 @@
+NAME:
+   getenvoy versions - List available Envoy versions
+
+USAGE:
+   getenvoy versions [command options] [arguments...]
+
+OPTIONS:
+   --help, -h  show help (default: false)
+   
diff --git a/internal/manifest/export_test.go b/internal/cmd/versions_test.go
similarity index 52%
rename from internal/manifest/export_test.go
rename to internal/cmd/versions_test.go
index a4febe2..da37624 100644
--- a/internal/manifest/export_test.go
+++ b/internal/cmd/versions_test.go
@@ -12,10 +12,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package manifest
+package cmd_test
 
-// CurrentPlatform exports currentPlatform for testing purpose.
-// Note that this file is not compiled into the resulting binary since the name is suffixed by `_test.go`
-func CurrentPlatform() string {
-	return currentPlatform() // unexported one defined in platform_${darwin,linux}.go
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/tetratelabs/getenvoy/internal/version"
+)
+
+func TestGetEnvoyVersions(t *testing.T) {
+	o, cleanup := setupTest(t)
+	defer cleanup()
+
+	// Run "getenvoy versions"
+	c, stdout, stderr := newApp(o)
+	o.Out = stdout
+	err := c.Run([]string{"getenvoy", "versions"})
+
+	// Verify the command invoked, passing the correct default commandline
+	require.NoError(t, err)
+	require.Equal(t, fmt.Sprintln(version.Envoy), stdout.String())
+	require.Empty(t, stderr)
 }
diff --git a/internal/manifest/locate_test.go b/internal/manifest/manifest_test.go
similarity index 55%
rename from internal/manifest/locate_test.go
rename to internal/manifest/manifest_test.go
index 8a9a328..81b4398 100644
--- a/internal/manifest/locate_test.go
+++ b/internal/manifest/manifest_test.go
@@ -16,7 +16,6 @@ package manifest
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -24,69 +23,60 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestLocateBuild(t *testing.T) {
-	tests := []struct{ name, reference, want, wantErr string }{
+func TestBuildPlatform(t *testing.T) {
+	tests := []struct{ name, goos, expected string }{
 		{
-			name:      "standard 1.17.1 linux-glibc matches",
-			reference: "standard:1.17.1/linux-glibc",
-			want:      "standard:1.17.1/linux-glibc",
+			name:     "darwin",
+			goos:     "darwin",
+			expected: "DARWIN",
 		},
 		{
-			name:      "standard 1.17.1 matches",
-			reference: "standard:1.17.1",
-			want:      fmt.Sprintf("standard:1.17.1/%v", currentPlatform()),
+			name:     "linux",
+			goos:     "linux",
+			expected: "LINUX_GLIBC",
 		},
 		{
-			name:      "standard-fips1402:1.10.0/linux-glibc matches",
-			reference: "standard-fips1402:1.10.0/linux-glibc",
-			want:      "standard-fips1402:1.10.0/linux-glibc",
+			name:     "unsupported",
+			goos:     "windows",
+			expected: "",
 		},
 		{
-			name:      "sTanDard:nIgHTLY/LiNuX-gLiBc matches",
-			reference: "sTanDard:nIgHTLY/LiNuX-gLiBc",
-			want:      "standard:nightly/linux-glibc",
-		},
-		{
-			name:      "Error if not found",
-			reference: "notaFlavor:1.17.1/notaPlatform",
-			wantErr:   `unable to find matching GetEnvoy build for reference "notaflavor:1.17.1/notaplatform"`,
+			name:     "empty",
+			goos:     "",
+			expected: "",
 		},
 	}
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
-			key, err := ParseReference(tc.reference)
-			require.NoError(t, err)
-
-			if have, err := LocateBuild(key, goodManifest); tc.wantErr != "" {
-				require.EqualError(t, err, tc.wantErr)
-				require.Equal(t, "", have)
-			} else {
-				require.NoError(t, err)
-				require.Equal(t, tc.want, have)
-			}
+			require.Equal(t, tc.expected, BuildPlatform(tc.goos))
 		})
 	}
 }
 
-func TestFetchManifest(t *testing.T) {
+func TestGetManifest(t *testing.T) {
 	goodManifestBytes, err := json.Marshal(goodManifest)
 	require.NoError(t, err)
 
 	// This tests we can parse a flavor besides "standard", and don't crash on new fields
-	nonStandardManifestBytes := []byte(`{
-  "manifestVersion": "v0.1.2",
+	unknownFieldsManifestBytes := []byte(`{
+  "manifestVersion": "v0.1.0",
   "flavors": {
-    "experiment": {
-      "name": "experiment",
+    "standard": {
+      "name": "standard",
+      "favoriteColor": "blue.. no, yellow!",
+
       "versions": {
-        "1.15": {
-          "name": "1.15",
+        "1.14.6": {
+          "name": "1.14.6",
           "builds": {
             "DARWIN": {
-              "downloadLocationUrl": "1.17.1/darwin",
               "platform": "DARWIN",
-              "ARCH": "amd64"
+              "downloadLocationUrl": "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-darwin-x86_64.tar.gz"
+            },
+            "LINUX_GLIBC": {
+              "platform": "LINUX_GLIBC",
+              "downloadLocationUrl": "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-linux-x86_64.tar.gz"
             }
           }
         }
@@ -95,26 +85,6 @@ func TestFetchManifest(t *testing.T) {
   }
 }`)
 
-	var nonStandardManifest = &Manifest{
-		ManifestVersion: "v0.1.2",
-		Flavors: map[string]*Flavor{
-			"experiment": {
-				Name: "experiment",
-				Versions: map[string]*Version{
-					"1.15": {
-						Name: "1.15",
-						Builds: map[string]*Build{
-							"DARWIN": {
-								Platform:            "DARWIN",
-								DownloadLocationURL: "1.17.1/darwin",
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-
 	tests := []struct {
 		name                  string
 		responseStatusCode    int
@@ -129,10 +99,10 @@ func TestFetchManifest(t *testing.T) {
 			want:                  goodManifest,
 		},
 		{
-			name:                  "allows non-standard flavor and unknown fields",
+			name:                  "allows unknown fields",
 			responseStatusCode:    http.StatusOK,
-			responseManifestBytes: nonStandardManifestBytes,
-			want:                  nonStandardManifest, // instead of crash
+			responseManifestBytes: unknownFieldsManifestBytes,
+			want:                  goodManifest, // instead of crash
 		},
 		{
 			name:               "errors on non-200 response",
@@ -152,7 +122,7 @@ func TestFetchManifest(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			mock := mockServer(tc.responseStatusCode, tc.responseManifestBytes)
 			defer mock.Close()
-			have, err := FetchManifest(mock.URL)
+			have, err := GetManifest(mock.URL)
 			require.Equal(t, tc.want, have)
 			if tc.wantErr {
 				require.Error(t, err)
@@ -171,3 +141,27 @@ func mockServer(responseStatusCode int, responseManifestBytes []byte) *httptest.
 		}
 	}))
 }
+
+var goodManifest = &Manifest{
+	ManifestVersion: "v0.1.0",
+	Flavors: map[string]*Flavor{
+		"standard": {
+			Name: "standard",
+			Versions: map[string]*Version{
+				"1.14.6": {
+					Name: "1.14.6",
+					Builds: map[string]*Build{
+						"LINUX_GLIBC": {
+							Platform:            "LINUX_GLIBC",
+							DownloadLocationURL: "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-linux-x86_64.tar.gz",
+						},
+						"DARWIN": {
+							Platform:            "DARWIN",
+							DownloadLocationURL: "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-darwin-x86_64.tar.gz",
+						},
+					},
+				},
+			},
+		},
+	},
+}
diff --git a/internal/manifest/print_test.go b/internal/manifest/print_test.go
deleted file mode 100644
index a0dd307..0000000
--- a/internal/manifest/print_test.go
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2019 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 manifest
-
-import (
-	"bytes"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestPrint(t *testing.T) {
-	got := bytes.NewBuffer(nil)
-	err := Print(goodManifest, got)
-
-	require.NoError(t, err)
-	require.Equal(t, `REFERENCE                                VERSION
-nightly/linux-glibc                      nightly
-1.17.1/linux-glibc                       1.17.1
-1.17.1/darwin                            1.17.1
-standard-fips1402:1.10.0/linux-glibc     1.10.0
-`, got.String())
-}
-
-var goodManifest = &Manifest{
-	ManifestVersion: "v0.1.0",
-	Flavors: map[string]*Flavor{
-		"standard": {
-			Name: "standard",
-			Versions: map[string]*Version{
-				"1.17.1": {
-					Name: "1.17.1",
-					Builds: map[string]*Build{
-						"LINUX_GLIBC": {
-							Platform:            "LINUX_GLIBC",
-							DownloadLocationURL: "standard:1.17.1/linux-glibc",
-						},
-						"DARWIN": {
-							Platform:            "DARWIN",
-							DownloadLocationURL: "standard:1.17.1/darwin",
-						},
-					},
-				},
-				"nightly": {
-					Name: "nightly",
-					Builds: map[string]*Build{
-						"LINUX_GLIBC": {
-							Platform:            "LINUX_GLIBC",
-							DownloadLocationURL: "standard:nightly/linux-glibc",
-						},
-					},
-				},
-			},
-		},
-		"standard-fips1402": {
-			Name: "standard-fips1402",
-			Versions: map[string]*Version{
-				"1.10.0": {
-					Name: "1.10.0",
-					Builds: map[string]*Build{
-						"LINUX_GLIBC": {
-							Platform:            "LINUX_GLIBC",
-							DownloadLocationURL: "standard-fips1402:1.10.0/linux-glibc",
-						},
-					},
-				},
-			},
-		},
-	},
-}
diff --git a/internal/manifest/reference_test.go b/internal/manifest/reference_test.go
deleted file mode 100644
index 954d720..0000000
--- a/internal/manifest/reference_test.go
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright 2020 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 manifest_test
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/tetratelabs/getenvoy/internal/manifest"
-)
-
-func TestParseReference(t *testing.T) {
-	tests := []struct {
-		name     string
-		input    string
-		expected manifest.Reference
-	}{
-		{
-			name:     "1.17.1",
-			input:    `1.17.1`,
-			expected: manifest.Reference{Flavor: "standard", Version: "1.17.1", Platform: manifest.CurrentPlatform()},
-		},
-		{
-			name:     "standard:1.17.1",
-			input:    `standard:1.17.1`,
-			expected: manifest.Reference{Flavor: "standard", Version: "1.17.1", Platform: manifest.CurrentPlatform()},
-		},
-		{
-			name:     "standard:1.17.1/darwin",
-			input:    `standard:1.17.1/darwin`,
-			expected: manifest.Reference{Flavor: "standard", Version: "1.17.1", Platform: "darwin"},
-		},
-		{
-			name:     "standard:1.17.1/linux-glibc",
-			input:    `standard:1.17.1/linux-glibc`,
-			expected: manifest.Reference{Flavor: "standard", Version: "1.17.1", Platform: "linux-glibc"},
-		},
-		{
-			name:     "coerces to lower-hyphen case format",
-			input:    `stAndArd:VeRsIoN/LINUX_GLIBC`,
-			expected: manifest.Reference{Flavor: "standard", Version: "version", Platform: "linux-glibc"},
-		},
-		{
-			name:     "trailing slash",
-			input:    `standard:1.17.1/`,
-			expected: manifest.Reference{Flavor: "standard", Version: "1.17.1", Platform: manifest.CurrentPlatform()},
-		},
-		{
-			name:     "non-standard",
-			input:    `experiment:latest`,
-			expected: manifest.Reference{Flavor: "experiment", Version: "latest", Platform: manifest.CurrentPlatform()},
-		},
-	}
-
-	for _, test := range tests {
-		test := test // pin! see https://github.com/kyoh86/scopelint for why
-
-		t.Run(test.name, func(t *testing.T) {
-			ref, err := manifest.ParseReference(test.input)
-			require.NoError(t, err)
-			require.Equal(t, &test.expected, ref)
-		})
-	}
-}
-
-func TestParseReferenceValidatesInput(t *testing.T) {
-	tests := []struct {
-		name        string
-		input       string
-		expectedErr string
-	}{
-		{
-			name:        "empty string",
-			input:       ``,
-			expectedErr: `"" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]`,
-		},
-		{
-			name:        "empty components",
-			input:       `:/`,
-			expectedErr: `":/" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]`,
-		},
-		{
-			name:        "no flavor",
-			input:       `:1.17.1/darwin`,
-			expectedErr: `":1.17.1/darwin" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]`,
-		},
-		{
-			name:        "no version",
-			input:       `standard:/darwin`,
-			expectedErr: `"standard:/darwin" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]`,
-		},
-		{
-			name:        "extra platform",
-			input:       `wasm:1.17.1/darwin/darwin`,
-			expectedErr: `"wasm:1.17.1/darwin/darwin" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]`,
-		},
-	}
-
-	for _, test := range tests {
-		test := test // pin! see https://github.com/kyoh86/scopelint for why
-
-		t.Run(test.name, func(t *testing.T) {
-			actual, err := manifest.ParseReference(test.input)
-			require.EqualError(t, err, test.expectedErr)
-			require.Nil(t, actual)
-		})
-	}
-}
diff --git a/internal/test/manifest/manifest.go b/internal/test/manifest/manifest.go
deleted file mode 100644
index 15a23ee..0000000
--- a/internal/test/manifest/manifest.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2020 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 manifest
-
-import (
-	"github.com/tetratelabs/getenvoy/internal/manifest"
-)
-
-// NewSimpleManifest returns a new manifest auto-generated for a given
-// list of references.
-func NewSimpleManifest(reference string) (*manifest.Manifest, error) {
-	m := new(manifest.Manifest)
-	m.ManifestVersion = "v0.1.0"
-	m.Flavors = make(map[string]*manifest.Flavor)
-	ref, err := manifest.ParseReference(reference)
-	if err != nil {
-		return nil, err
-	}
-	flavor, exists := m.Flavors[ref.Flavor]
-	if !exists {
-		flavor = &manifest.Flavor{Name: ref.Flavor}
-		m.Flavors[ref.Flavor] = flavor
-	}
-	if flavor.Versions == nil {
-		flavor.Versions = make(map[string]*manifest.Version)
-	}
-	version, exists := flavor.Versions[ref.Version]
-	if !exists {
-		version = &manifest.Version{Name: ref.Version}
-		flavor.Versions[ref.Version] = version
-	}
-	if version.Builds == nil {
-		version.Builds = make(map[string]*manifest.Build)
-	}
-	platforms := []string{"DARWIN", "LINUX_GLIBC"}
-	if ref.Platform != "" {
-		platforms = []string{ref.Platform}
-	}
-	for _, platform := range platforms {
-		version.Builds[platform] = &manifest.Build{
-			Platform:            platform,
-			DownloadLocationURL: ref.String(),
-		}
-	}
-	return m, nil
-}
diff --git a/internal/test/manifest/server.go b/internal/test/manifest/server.go
index 267b6ea..e5be06b 100644
--- a/internal/test/manifest/server.go
+++ b/internal/test/manifest/server.go
@@ -35,16 +35,43 @@ import (
 // Even though currently binaries are compressed with "xz" more than "gz", using "gz" in tests allows us to re-use
 // tar.TarGz instead of complicating internal utilities or adding dependencies only for tests.
 const archiveFormat = ".tar.gz"
-const buildsPath = "/builds/"
+const versionsPath = "/versions/"
 
 // RequireManifestTestServer serves "/manifest.json", which contains download links a compressed archive of artifactDir
-func RequireManifestTestServer(t *testing.T, m *manifest.Manifest) *httptest.Server {
+func RequireManifestTestServer(t *testing.T, version string) *httptest.Server {
 	s := &server{t: t}
 	h := httptest.NewServer(s)
-	s.rewriteDownloadLocations(h.URL, *m)
+	s.manifest = &manifest.Manifest{
+		ManifestVersion: "v0.1.0",
+		Flavors: map[string]*manifest.Flavor{
+			"standard": {
+				Name: "standard",
+				Versions: map[string]*manifest.Version{
+					version: {
+						Name: version,
+						Builds: map[string]*manifest.Build{
+							"LINUX_GLIBC": {
+								Platform:            "LINUX_GLIBC",
+								DownloadLocationURL: TarballURL(h.URL, "linux", version),
+							},
+							"DARWIN": {
+								Platform:            "DARWIN",
+								DownloadLocationURL: TarballURL(h.URL, "darwin", version),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
 	return h
 }
 
+// TarballURL gives the expected download URL for the given runtime.GOOS and Envoy version.
+func TarballURL(baseURL, goos, v string) string {
+	return fmt.Sprintf("%s%s%s/envoy-%s-%s-x86_64%s", baseURL, versionsPath, v, v, goos, archiveFormat)
+}
+
 // server represents an HTTP server serving GetEnvoy manifest.
 type server struct {
 	t        *testing.T
@@ -57,16 +84,15 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusOK)
 		_, err := w.Write(s.GetManifest())
 		require.NoError(s.t, err)
-	case strings.HasPrefix(r.RequestURI, buildsPath):
-		reference := r.RequestURI[len(buildsPath):]
-		require.True(s.t, strings.HasSuffix(reference, archiveFormat),
-			"unexpected uri %q: expected archive suffix %q", reference, archiveFormat)
-		ref, err := manifest.ParseReference(reference)
-		require.NoError(s.t, err, "could not parse reference from uri %q", reference)
-		require.Regexpf(s.t, `^1\.[1-9][0-9]\.[0-9]+$`, ref.Version, "unsupported version in uri %q", reference)
+	case strings.HasPrefix(r.RequestURI, versionsPath):
+		subpath := r.RequestURI[len(versionsPath):]
+		require.True(s.t, strings.HasSuffix(subpath, archiveFormat), "unexpected uri %q: expected archive suffix %q", subpath, archiveFormat)
+
+		v := strings.Split(subpath, "/")[0]
+		require.Regexpf(s.t, `^1\.[1-9][0-9]\.[0-9]+$`, v, "unsupported version in uri %q", subpath)
 
 		w.WriteHeader(http.StatusOK)
-		_, e := w.Write(requireFakeEnvoyTarGz(s.t, ref.Version))
+		_, e := w.Write(requireFakeEnvoyTarGz(s.t, v))
 		require.NoError(s.t, e)
 	default:
 		w.WriteHeader(http.StatusNotFound)
@@ -79,24 +105,13 @@ func (s *server) GetManifest() []byte {
 	return data
 }
 
-func (s *server) rewriteDownloadLocations(baseURL string, m manifest.Manifest) {
-	for _, flavor := range m.Flavors {
-		for _, version := range flavor.Versions {
-			for _, build := range version.Builds {
-				build.DownloadLocationURL = fmt.Sprintf("%s%s%s%s", baseURL, buildsPath, build.DownloadLocationURL, archiveFormat)
-			}
-		}
-	}
-	s.manifest = &m
-}
-
-func requireFakeEnvoyTarGz(t *testing.T, version string) []byte {
+func requireFakeEnvoyTarGz(t *testing.T, v string) []byte {
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
 
 	// construct the platform directory based on the input version
-	platformDir := filepath.Join(tempDir, version)
-	require.NoError(t, os.MkdirAll(filepath.Join(platformDir, "bin"), 0700)) //nolint:gosec
+	installDir := filepath.Join(tempDir, v)
+	require.NoError(t, os.MkdirAll(filepath.Join(installDir, "bin"), 0700)) //nolint:gosec
 	// go:embed doesn't allow us to retain execute bit, so it is simpler to inline this.
 	fakeEnvoy := []byte(`#!/bin/sh
 set -ue
@@ -106,11 +121,11 @@ echo envoy bin: $0
 echo envoy args: $@
 echo >&2 envoy stderr
 `)
-	require.NoError(t, os.WriteFile(filepath.Join(platformDir, "bin", "envoy"), fakeEnvoy, 0700)) //nolint:gosec
+	require.NoError(t, os.WriteFile(filepath.Join(installDir, "bin", "envoy"), fakeEnvoy, 0700)) //nolint:gosec
 
 	// tar.gz the platform dir
 	tempGz := filepath.Join(tempDir, "envoy.tar.gz")
-	e := tar.TarGz(tempGz, platformDir)
+	e := tar.TarGz(tempGz, installDir)
 	require.NoError(t, e)
 
 	// Read the tar.gz into a byte array. This allows the mock server to set content length correctly
diff --git a/main_test.go b/main_test.go
index 81d94c7..7187ea8 100644
--- a/main_test.go
+++ b/main_test.go
@@ -52,7 +52,7 @@ show usage with: getenvoy -h
 		},
 		{
 			name:           "incorrect global flag value",
-			args:           []string{"getenvoy", "--manifest", ".", "list"},
+			args:           []string{"getenvoy", "--manifest", ".", "versions"},
 			expectedStatus: 1,
 			expectedStderr: `"." is not a valid manifest URL
 show usage with: getenvoy -h
@@ -64,19 +64,11 @@ show usage with: getenvoy -h
 			expectedStatus: 1,
 			expectedStderr: `unknown command "fly"
 show usage with: getenvoy -h
-`,
-		},
-		{
-			name:           "invalid command arg",
-			args:           []string{"getenvoy", "fetch", "/"},
-			expectedStatus: 1,
-			expectedStderr: `"/" is not a valid GetEnvoy reference. Expected format: [<flavor>:]<version>[/<platform>]
-show usage with: getenvoy -h
 `,
 		},
 		{
 			name:           "execution error",
-			args:           []string{"getenvoy", "--manifest", server.URL, "list"},
+			args:           []string{"getenvoy", "--manifest", server.URL, "versions"},
 			expectedStatus: 1,
 			expectedStderr: `error: error unmarshalling manifest: unexpected end of JSON input
 `,