Repository: tetratelabs/getenvoy
Pull request #21 (2019-07-19 21:15:08)

Hook up list command
Resolves https://github.com/tetratelabs/getenvoy/issues/9.

Signed-off-by: Liam White <liam@tetrate.io>


Patch:
diff --git a/go.sum b/go.sum
index a77091f..edf6076 100644
--- a/go.sum
+++ b/go.sum
@@ -38,8 +38,6 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/tetratelabs/getenvoy-package v0.0.0-20190718093948-35658f673f39 h1:ocOviUkYIJiidvsVmYjzzgm2Xy5WZ3EqrxXlnq3tm00=
-github.com/tetratelabs/getenvoy-package v0.0.0-20190718093948-35658f673f39/go.mod h1:IrGJ2YZz3+qia1dEU9GFg3QieuMUUwiJf5tjG8uhogg=
 github.com/tetratelabs/getenvoy-package v0.0.0-20190718134531-9487f25b3273 h1:Iy7CXZY65z7KY0z6LXh2V2urACTo8EDBbKqXf8O+mRU=
 github.com/tetratelabs/getenvoy-package v0.0.0-20190718134531-9487f25b3273/go.mod h1:IrGJ2YZz3+qia1dEU9GFg3QieuMUUwiJf5tjG8uhogg=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go
new file mode 100644
index 0000000..ab71852
--- /dev/null
+++ b/pkg/cmd/list.go
@@ -0,0 +1,40 @@
+// 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 cmd
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+	"github.com/tetratelabs/getenvoy/pkg/manifest"
+)
+
+// ListCmd lists available Envoy binaries
+var (
+	manifestURL string
+	ListCmd     = &cobra.Command{
+		Use:   "list",
+		Short: "Lists available Envoys from GetEnvoy.",
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			return manifest.Print(os.Stdout, manifestURL)
+		},
+	}
+)
+
+func init() {
+	ListCmd.Flags().StringVarP(&manifestURL, "url", "u",
+		"https://bintray.com/tetrate/getenvoy/download_file?file_path=manifest.json", "overrides the manifest URL")
+	rootCmd.AddCommand(ListCmd)
+}
diff --git a/pkg/manifest/fetch.go b/pkg/manifest/fetch.go
deleted file mode 100644
index 681aac0..0000000
--- a/pkg/manifest/fetch.go
+++ /dev/null
@@ -1,41 +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"
-	"net/http"
-
-	"github.com/golang/protobuf/jsonpb"
-	"github.com/tetratelabs/getenvoy-package/api"
-)
-
-// Fetch retrieves and parses a manifest from the URL passed
-func Fetch(manifestURL string) (*api.Manifest, error) {
-	// #nosec => This is by design, users can call out to wherever they like!
-	resp, err := http.Get(manifestURL)
-	if err != nil {
-		return nil, err
-	}
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("received %q from %v", resp.StatusCode, manifestURL)
-	}
-	defer func() { _ = resp.Body.Close() }()
-	result := api.Manifest{}
-	if err := jsonpb.Unmarshal(resp.Body, &result); err != nil {
-		return nil, fmt.Errorf("error unmarshalling manifest: %v", err)
-	}
-	return &result, nil
-}
diff --git a/pkg/manifest/print.go b/pkg/manifest/print.go
index 555e761..e42cf72 100644
--- a/pkg/manifest/print.go
+++ b/pkg/manifest/print.go
@@ -15,17 +15,29 @@
 package manifest
 
 import (
+	"errors"
 	"fmt"
 	"io"
+	"net/http"
 	"sort"
 	"strings"
 	"text/tabwriter"
 
+	"net/url"
+
+	"github.com/golang/protobuf/jsonpb"
 	"github.com/tetratelabs/getenvoy-package/api"
 )
 
-// PrettyPrint tabwrites the passed manifest to the passed writer
-func PrettyPrint(writer io.Writer, manifest *api.Manifest) error {
+// Print retrieves the manifest from the passed location and writes it to the passed writer
+func Print(writer io.Writer, manifestLocation string) error {
+	if _, err := url.Parse(manifestLocation); err != nil {
+		return errors.New("only URL manifest locations are supported")
+	}
+	manifest, err := fetch(manifestLocation)
+	if err != nil {
+		return err
+	}
 	w := new(tabwriter.Writer).Init(writer, 0, 8, 5, ' ', 0)
 	fmt.Fprintln(w, "FLAVOR\tVERSION\tAVAILABLE ON")
 
@@ -45,6 +57,23 @@ func PrettyPrint(writer io.Writer, manifest *api.Manifest) error {
 	return w.Flush()
 }
 
+func fetch(manifestURL string) (*api.Manifest, error) {
+	// #nosec => This is by design, users can call out to wherever they like!
+	resp, err := http.Get(manifestURL)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("received %v response code from %v", resp.StatusCode, manifestURL)
+	}
+	defer func() { _ = resp.Body.Close() }()
+	result := api.Manifest{}
+	if err := jsonpb.Unmarshal(resp.Body, &result); err != nil {
+		return nil, fmt.Errorf("error unmarshalling manifest: %v", err)
+	}
+	return &result, nil
+}
+
 func deterministicFlavors(flavors map[string]*api.Flavor) []*api.Flavor {
 	flavorList := []*api.Flavor{}
 	for _, flavor := range flavors {


Test patch:
diff --git a/pkg/manifest/fetch_test.go b/pkg/manifest/fetch_test.go
deleted file mode 100644
index 6ea2d23..0000000
--- a/pkg/manifest/fetch_test.go
+++ /dev/null
@@ -1,169 +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 (
-	"io/ioutil"
-	"net/http"
-	"net/http/httptest"
-	"path/filepath"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/tetratelabs/getenvoy-package/api"
-)
-
-func TestFetch(t *testing.T) {
-	tests := []struct {
-		name                 string
-		responseStatusCode   int
-		responseManifestFile string
-		want                 *api.Manifest
-		wantErr              bool
-	}{
-		{
-			name:                 "responds with parsed manifest",
-			responseStatusCode:   http.StatusOK,
-			responseManifestFile: "manifest.golden",
-			want:                 goodManifest(),
-		},
-		{
-			name:               "errors on non-200 response",
-			responseStatusCode: http.StatusInternalServerError,
-			want:               nil,
-			wantErr:            true,
-		},
-		{
-			name:                 "errors on unparsable manifest",
-			responseStatusCode:   http.StatusOK,
-			responseManifestFile: "malformed.golden",
-			wantErr:              true,
-		},
-	}
-	for _, tt := range tests {
-		tc := tt
-		t.Run(tc.name, func(t *testing.T) {
-			mock := mockServer(tc.responseStatusCode, tc.responseManifestFile)
-			defer mock.Close()
-			got, err := Fetch(mock.URL)
-			assert.Equal(t, tc.want, got)
-			if tc.wantErr {
-				assert.Error(t, err)
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
-func mockServer(responseStatusCode int, responseManifestFile string) *httptest.Server {
-	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(responseStatusCode)
-		if responseStatusCode == http.StatusOK {
-			bytes, _ := ioutil.ReadFile(filepath.Join("testdata", responseManifestFile))
-			w.Write(bytes)
-		}
-	}))
-}
-
-func goodManifest() *api.Manifest {
-	return &api.Manifest{
-		ManifestVersion: "v0.1.0",
-		Flavors: map[string]*api.Flavor{
-			"standard": {
-				Name:          "standard",
-				FilterProfile: "standard",
-				Versions: map[string]*api.Version{
-					"1.11.0": {
-						Name: "1.11.0",
-						OperatingSystems: map[string]*api.OperatingSystem{
-							"Ubuntu": {
-								Name: api.OperatingSystemName_UBUNTU,
-								Builds: []*api.Build{
-									{
-										OperatingSystemVersions: []string{"16.04", "18.04"},
-										DownloadLocationUrl:     "http://example.com",
-									},
-								},
-							},
-							"macOS": {
-								Name: api.OperatingSystemName_MACOS,
-								Builds: []*api.Build{
-									{
-										OperatingSystemVersions: []string{"10.14"},
-										DownloadLocationUrl:     "http://example.com",
-									},
-								},
-							},
-							"CentOS": {
-								Name: api.OperatingSystemName_CENTOS,
-								Builds: []*api.Build{
-									{
-										OperatingSystemVersions: []string{"7"},
-										DownloadLocationUrl:     "http://example.com",
-									},
-								},
-							},
-						},
-					},
-					"nightly": {
-						Name: "nightly",
-						OperatingSystems: map[string]*api.OperatingSystem{
-							"CentOS": {
-								Name: api.OperatingSystemName_CENTOS,
-								Builds: []*api.Build{
-									{
-										OperatingSystemVersions: []string{"7"},
-										DownloadLocationUrl:     "http://example.com",
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			"standard-fips1402": {
-				Name:          "standard-fips1402",
-				FilterProfile: "standard",
-				Compliances:   []api.Compliance{api.Compliance_FIPS_1402},
-				Versions: map[string]*api.Version{
-					"1.10.0": {
-						Name: "1.10.0",
-						OperatingSystems: map[string]*api.OperatingSystem{
-							"Ubuntu": {
-								Name: api.OperatingSystemName_UBUNTU,
-								Builds: []*api.Build{
-									{
-										OperatingSystemVersions: []string{"16.04"},
-										DownloadLocationUrl:     "http://example.com",
-									},
-								},
-							},
-							"CentOS": {
-								Name: api.OperatingSystemName_CENTOS,
-								Builds: []*api.Build{
-									{
-										OperatingSystemVersions: []string{"7"},
-										DownloadLocationUrl:     "http://example.com",
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-}
diff --git a/pkg/manifest/print_test.go b/pkg/manifest/print_test.go
index a580e05..792bb3a 100644
--- a/pkg/manifest/print_test.go
+++ b/pkg/manifest/print_test.go
@@ -17,29 +17,193 @@ package manifest
 import (
 	"bytes"
 	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
 	"path/filepath"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/tetratelabs/getenvoy-package/api"
 )
 
-func TestPrettyPrint(t *testing.T) {
+func TestPrint(t *testing.T) {
 	tests := []struct {
-		name           string
-		wantOutputFile string
+		name             string
+		wantOutputFile   string
+		wantErr          bool
+		locationOverride string
 	}{
 		{
 			name:           "Prints golden output",
 			wantOutputFile: "list.golden",
 		},
+
+		{
+			name:             "Errors on non-URL inputs",
+			locationOverride: "dir/file.json",
+			wantErr:          true,
+		},
 	}
 	for _, tt := range tests {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
+			mock := mockServer(http.StatusOK, "manifest.golden")
+			defer mock.Close()
 			got := bytes.NewBuffer(nil)
-			PrettyPrint(got, goodManifest())
+			location := mock.URL
+			if tc.locationOverride != "" {
+				location = tc.locationOverride
+			}
+			if err := Print(got, location); tc.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
 			want, _ := ioutil.ReadFile(filepath.Join("testdata", tc.wantOutputFile))
 			assert.Equal(t, string(want), got.String())
 		})
 	}
 }
+
+func TestFetch(t *testing.T) {
+	tests := []struct {
+		name                 string
+		responseStatusCode   int
+		responseManifestFile string
+		want                 *api.Manifest
+		wantErr              bool
+	}{
+		{
+			name:                 "responds with parsed manifest",
+			responseStatusCode:   http.StatusOK,
+			responseManifestFile: "manifest.golden",
+			want:                 goodManifest(),
+		},
+		{
+			name:               "errors on non-200 response",
+			responseStatusCode: http.StatusInternalServerError,
+			want:               nil,
+			wantErr:            true,
+		},
+		{
+			name:                 "errors on unparsable manifest",
+			responseStatusCode:   http.StatusOK,
+			responseManifestFile: "malformed.golden",
+			wantErr:              true,
+		},
+	}
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			mock := mockServer(tc.responseStatusCode, tc.responseManifestFile)
+			defer mock.Close()
+			got, err := fetch(mock.URL)
+			assert.Equal(t, tc.want, got)
+			if tc.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func mockServer(responseStatusCode int, responseManifestFile string) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(responseStatusCode)
+		if responseStatusCode == http.StatusOK {
+			bytes, _ := ioutil.ReadFile(filepath.Join("testdata", responseManifestFile))
+			w.Write(bytes)
+		}
+	}))
+}
+
+func goodManifest() *api.Manifest {
+	return &api.Manifest{
+		ManifestVersion: "v0.1.0",
+		Flavors: map[string]*api.Flavor{
+			"standard": {
+				Name:          "standard",
+				FilterProfile: "standard",
+				Versions: map[string]*api.Version{
+					"1.11.0": {
+						Name: "1.11.0",
+						OperatingSystems: map[string]*api.OperatingSystem{
+							"Ubuntu": {
+								Name: api.OperatingSystemName_UBUNTU,
+								Builds: []*api.Build{
+									{
+										OperatingSystemVersions: []string{"16.04", "18.04"},
+										DownloadLocationUrl:     "http://example.com",
+									},
+								},
+							},
+							"macOS": {
+								Name: api.OperatingSystemName_MACOS,
+								Builds: []*api.Build{
+									{
+										OperatingSystemVersions: []string{"10.14"},
+										DownloadLocationUrl:     "http://example.com",
+									},
+								},
+							},
+							"CentOS": {
+								Name: api.OperatingSystemName_CENTOS,
+								Builds: []*api.Build{
+									{
+										OperatingSystemVersions: []string{"7"},
+										DownloadLocationUrl:     "http://example.com",
+									},
+								},
+							},
+						},
+					},
+					"nightly": {
+						Name: "nightly",
+						OperatingSystems: map[string]*api.OperatingSystem{
+							"CentOS": {
+								Name: api.OperatingSystemName_CENTOS,
+								Builds: []*api.Build{
+									{
+										OperatingSystemVersions: []string{"7"},
+										DownloadLocationUrl:     "http://example.com",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			"standard-fips1402": {
+				Name:          "standard-fips1402",
+				FilterProfile: "standard",
+				Compliances:   []api.Compliance{api.Compliance_FIPS_1402},
+				Versions: map[string]*api.Version{
+					"1.10.0": {
+						Name: "1.10.0",
+						OperatingSystems: map[string]*api.OperatingSystem{
+							"Ubuntu": {
+								Name: api.OperatingSystemName_UBUNTU,
+								Builds: []*api.Build{
+									{
+										OperatingSystemVersions: []string{"16.04"},
+										DownloadLocationUrl:     "http://example.com",
+									},
+								},
+							},
+							"CentOS": {
+								Name: api.OperatingSystemName_CENTOS,
+								Builds: []*api.Build{
+									{
+										OperatingSystemVersions: []string{"7"},
+										DownloadLocationUrl:     "http://example.com",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}