Repository: FairwindsOps/pluto (go)

Issue:
Add human-readable list output
Currently we only have JSON. Allow for yaml and human-readable like 
our other outptus

Pull request description:
add options for list output to cover all our standard output options
Fixes #66

Removed all omitempty on the json and yaml output for versions. The more I thought about it the more I think Luke is correct.

Patch:
diff --git a/cmd/root.go b/cmd/root.go
index 9d08988..ebf01b7 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -15,7 +15,6 @@
 package cmd
 
 import (
-	"encoding/json"
 	"flag"
 	"fmt"
 	"io/ioutil"
@@ -239,12 +238,10 @@ var listVersionsCmd = &cobra.Command{
 	Short: "Outputs a JSON object of the versions that Pluto knows about.",
 	Long:  `Outputs a JSON object of the versions that Pluto knows about.`,
 	Run: func(cmd *cobra.Command, args []string) {
-		data, err := json.Marshal(api.VersionList)
+		err := api.PrintVersionList(outputFormat)
 		if err != nil {
-			klog.Error(err)
 			os.Exit(1)
 		}
-		fmt.Println(string(data))
 	},
 }
 
diff --git a/pkg/api/list.go b/pkg/api/list.go
index 8d64cb0..25d4d65 100644
--- a/pkg/api/list.go
+++ b/pkg/api/list.go
@@ -14,6 +14,15 @@
 
 package api
 
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"text/tabwriter"
+
+	"gopkg.in/yaml.v2"
+)
+
 // VersionList is a set of apiVersions and if they are deprecated or not.
 var VersionList = []Version{
 	// Not Removed or Deprecated
@@ -61,3 +70,67 @@ var VersionList = []Version{
 	// Unknown Removal, but deprecated
 	{"storage.k8s.io/v1beta1", "CSINode", "v1.17.0", "", ""},
 }
+
+// PrintVersionList prints out the list of versions
+// in a specific format
+func PrintVersionList(outputFormat string) error {
+	switch outputFormat {
+	case "normal":
+		err := printVersionsTabular()
+		if err != nil {
+			return err
+		}
+	case "wide":
+		err := printVersionsTabular()
+		if err != nil {
+			return err
+		}
+	case "json":
+		data, err := json.Marshal(VersionList)
+		if err != nil {
+			return err
+		}
+		fmt.Println(string(data))
+	case "yaml":
+		data, err := yaml.Marshal(VersionList)
+		if err != nil {
+			return err
+		}
+		fmt.Println(string(data))
+	default:
+		errText := "The output format must one of (normal|json|yaml)"
+		fmt.Println(errText)
+		return fmt.Errorf(errText)
+	}
+	return nil
+}
+
+func printVersionsTabular() error {
+	w := new(tabwriter.Writer)
+	w.Init(os.Stdout, 0, 15, 2, padChar, 0)
+
+	_, _ = fmt.Fprintln(w, "KIND\t NAME\t DEPRECATED IN\t REMOVED IN\t REPLACEMENT\t")
+
+	for _, version := range VersionList {
+		deprecatedIn := version.DeprecatedIn
+		if deprecatedIn == "" {
+			deprecatedIn = "n/a"
+		}
+		removedIn := version.RemovedIn
+		if removedIn == "" {
+			removedIn = "n/a"
+		}
+
+		replacementAPI := version.ReplacementAPI
+		if replacementAPI == "" {
+			replacementAPI = "n/a"
+		}
+
+		_, _ = fmt.Fprintf(w, "%s\t %s\t %s\t %s\t %s\t\n", version.Kind, version.Name, deprecatedIn, removedIn, replacementAPI)
+	}
+	err := w.Flush()
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/pkg/api/versions.go b/pkg/api/versions.go
index 4a530ff..22e0495 100644
--- a/pkg/api/versions.go
+++ b/pkg/api/versions.go
@@ -40,17 +40,17 @@ type StubMeta struct {
 // Version is an apiVersion and a flag for deprecation
 type Version struct {
 	// Name is the name of the api version
-	Name string `json:"version,omitempty" yaml:"version,omitempty"`
+	Name string `json:"version" yaml:"version"`
 	// Kind is the kind of object associated with this version
-	Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
+	Kind string `json:"kind" yaml:"kind"`
 	// DeprecatedIn is a string that indicates what version the api is deprecated in
 	// an empty string indicates that the version is not deprecated
-	DeprecatedIn string `json:"deprecated-in,omitempty" yaml:"deprecated-in,omitempty"`
+	DeprecatedIn string `json:"deprecated-in" yaml:"deprecated-in"`
 	// RemovedIn denotes the version that the api was actually removed in
 	// An empty string indicates that the version has not been removed yet
-	RemovedIn string `json:"removed-in,omitempty" yaml:"removed-in,omitempty"`
+	RemovedIn string `json:"removed-in" yaml:"removed-in"`
 	// ReplacementAPI is the apiVersion that replaces the deprecated one
-	ReplacementAPI string `json:"replacement-api,omitempty" yaml:"replacement-api,omitempty"`
+	ReplacementAPI string `json:"replacement-api" yaml:"replacement-api"`
 }
 
 func checkVersion(stub *Stub) *Version {


Test patch:
diff --git a/e2e/tests/00_static_files.yaml b/e2e/tests/00_static_files.yaml
index ea41cf7..81d329f 100644
--- a/e2e/tests/00_static_files.yaml
+++ b/e2e/tests/00_static_files.yaml
@@ -63,7 +63,7 @@ testcases:
   - script: pluto detect-files -d assets/ -A -ojson
     assertions:
     - result.code ShouldEqual 3
-    - result.systemout ShouldEqual '{"items":[{"name":"utilities","api":{"version":"extensions/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"},"deprecated":true,"removed":true},{"name":"utilities","api":{"version":"apps/v1","kind":"Deployment"},"deprecated":false,"removed":false}],"show-all":true,"target-version":"v1.16.0"}'
+    - result.systemout ShouldEqual '{"items":[{"name":"utilities","api":{"version":"extensions/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"},"deprecated":true,"removed":true},{"name":"utilities","api":{"version":"apps/v1","kind":"Deployment","deprecated-in":"","removed-in":"","replacement-api":""},"deprecated":false,"removed":false}],"show-all":true,"target-version":"v1.16.0"}'
 
 - name: static files json
   steps:
diff --git a/pkg/api/list_test.go b/pkg/api/list_test.go
index c5dc06a..c27b408 100644
--- a/pkg/api/list_test.go
+++ b/pkg/api/list_test.go
@@ -33,3 +33,89 @@ func Test_VersionListIsValid(t *testing.T) {
 		}
 	}
 }
+
+var TestVersionList = []Version{
+	{"apps/v1beta1", "Deployment", "v1.9.0", "v1.16.0", "apps/v1"},
+}
+
+func Example_printVersionsTabular() {
+	padChar = '-'
+	_ = printVersionsTabular()
+
+	// Output:
+	// KIND-------------------------- NAME---------------------------------- DEPRECATED IN-- REMOVED IN-- REPLACEMENT----------------------
+	// Deployment-------------------- apps/v1------------------------------- n/a------------ n/a--------- n/a------------------------------
+	// NetworkPolicy----------------- networking.k8s.io/v1------------------ n/a------------ n/a--------- n/a------------------------------
+	// PodSecurityPolicy------------- policy/v1beta1------------------------ n/a------------ n/a--------- n/a------------------------------
+	// Deployment-------------------- extensions/v1beta1-------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// Deployment-------------------- apps/v1beta2-------------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// Deployment-------------------- apps/v1beta1-------------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// StatefulSet------------------- apps/v1beta1-------------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// StatefulSet------------------- apps/v1beta2-------------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// NetworkPolicy----------------- extensions/v1beta1-------------------- v1.9.0--------- v1.16.0----- networking.k8s.io/v1-------------
+	// DaemonSet--------------------- apps/v1beta2-------------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// DaemonSet--------------------- extensions/v1beta1-------------------- v1.9.0--------- v1.16.0----- apps/v1--------------------------
+	// PodSecurityPolicy------------- extensions/v1beta1-------------------- v1.10.0-------- v1.16.0----- policy/v1beta1-------------------
+	// ReplicaSet-------------------- extensions/v1beta1-------------------- n/a------------ v1.16.0----- apps/v1--------------------------
+	// ReplicaSet-------------------- apps/v1beta1-------------------------- n/a------------ v1.16.0----- apps/v1--------------------------
+	// ReplicaSet-------------------- apps/v1beta2-------------------------- n/a------------ v1.16.0----- apps/v1--------------------------
+	// PriorityClass----------------- scheduling.k8s.io/v1beta1------------- v1.14.0-------- v1.17.0----- scheduling.k8s.io/v1-------------
+	// PriorityClass----------------- scheduling.k8s.io/v1alpha1------------ v1.14.0-------- v1.17.0----- scheduling.k8s.io/v1-------------
+	// CustomResourceDefinition------ apiextensions.k8s.io/v1beta1---------- v1.16.0-------- v1.19.0----- apiextensions.k8s.io/v1----------
+	// MutatingWebhookConfiguration-- admissionregistration.k8s.io/v1beta1-- v1.16.0-------- v1.19.0----- admissionregistration.k8s.io/v1--
+	// ClusterRoleBinding------------ rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// ClusterRole------------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// ClusterRoleBindingList-------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// ClusterRoleList--------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// Role-------------------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// RoleBinding------------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// RoleList---------------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// RoleBindingList--------------- rbac.authorization.k8s.io/v1alpha1---- v1.17.0-------- v1.20.0----- rbac.authorization.k8s.io/v1-----
+	// CSINode----------------------- storage.k8s.io/v1beta1---------------- v1.17.0-------- n/a--------- n/a------------------------------
+}
+
+func ExamplePrintVersionList_json() {
+	VersionList = TestVersionList
+	_ = PrintVersionList("json")
+
+	// Output:
+	// [{"version":"apps/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"}]
+}
+
+func ExamplePrintVersionList_yaml() {
+	VersionList = TestVersionList
+	_ = PrintVersionList("yaml")
+
+	// Output:
+	// - version: apps/v1beta1
+	//   kind: Deployment
+	//   deprecated-in: v1.9.0
+	//   removed-in: v1.16.0
+	//   replacement-api: apps/v1
+}
+
+func ExamplePrintVersionList_normal() {
+	VersionList = TestVersionList
+	_ = PrintVersionList("normal")
+
+	// Output:
+	// KIND-------- NAME---------- DEPRECATED IN-- REMOVED IN-- REPLACEMENT--
+	// Deployment-- apps/v1beta1-- v1.9.0--------- v1.16.0----- apps/v1------
+}
+
+func ExamplePrintVersionList_wide() {
+	VersionList = TestVersionList
+	_ = PrintVersionList("wide")
+
+	// Output:
+	// KIND-------- NAME---------- DEPRECATED IN-- REMOVED IN-- REPLACEMENT--
+	// Deployment-- apps/v1beta1-- v1.9.0--------- v1.16.0----- apps/v1------
+}
+
+func ExamplePrintVersionList_badformat() {
+	VersionList = TestVersionList
+	_ = PrintVersionList("foo")
+
+	// Output:
+	// The output format must one of (normal|json|yaml)
+}
diff --git a/pkg/api/output_test.go b/pkg/api/output_test.go
index 7d08cd8..ae649d8 100644
--- a/pkg/api/output_test.go
+++ b/pkg/api/output_test.go
@@ -128,7 +128,7 @@ func ExampleInstance_DisplayOutput_showAll_json() {
 	_ = instance.DisplayOutput()
 
 	// Output:
-	// {"items":[{"name":"some name one","api":{"version":"apps/v1","kind":"Deployment"},"deprecated":false,"removed":false},{"name":"some name two","api":{"version":"extensions/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"},"deprecated":true,"removed":true}],"show-all":true,"target-version":"v1.16.0"}
+	// {"items":[{"name":"some name one","api":{"version":"apps/v1","kind":"Deployment","deprecated-in":"","removed-in":"","replacement-api":""},"deprecated":false,"removed":false},{"name":"some name two","api":{"version":"extensions/v1beta1","kind":"Deployment","deprecated-in":"v1.9.0","removed-in":"v1.16.0","replacement-api":"apps/v1"},"deprecated":true,"removed":true}],"show-all":true,"target-version":"v1.16.0"}
 }
 
 func ExampleInstance_DisplayOutput_showAll_yaml() {
@@ -149,6 +149,9 @@ func ExampleInstance_DisplayOutput_showAll_yaml() {
 	//   api:
 	//     version: apps/v1
 	//     kind: Deployment
+	//     deprecated-in: ""
+	//     removed-in: ""
+	//     replacement-api: ""
 	//   deprecated: false
 	//   removed: false
 	// - name: some name two