Repository: getporter/porter (go)

Issue:
Add --all-namespaces flag
On v1, you can specify --namespace="*" and that indicates that porter should list across all namespaces. Since bash treats * funny (you have to wrap it in quotes), this is awkward for people who are not used to this. Plus it's not as discoverable as a dedicated flag. I like keeping the * support around because it makes scripting more straightforward though.

This should be changed on v1, but can be shipped after v1.

Pull request description:
All namespaces
# What does this change
Add an `--all-namespaces` flag to porter installation|credential|parameter list commands. This is the equivalent of `list -n "*"`

# What issue does it fix
Closes #1709 

# Notes for the reviewer
Relies on #1701 and will need a rebase after that is merged.

# Checklist
- [x] Unit Tests
- [x] Documentation
- [ ] Schema (porter.yaml)


Patch:
diff --git a/cmd/porter/credentials.go b/cmd/porter/credentials.go
index 8e93a330..4f0b03d2 100644
--- a/cmd/porter/credentials.go
+++ b/cmd/porter/credentials.go
@@ -109,20 +109,22 @@ Optionally filters the results name, which returns all results whose name contai
 The results may also be filtered by associated labels and the namespace in which the credential set is defined.`,
 		Example: `  porter credentials list
   porter credentials list --namespace prod
-  porter credentials list --namespace "*"
+  porter credentials list --all-namespaces,
   porter credentials list --name myapp
   porter credentials list --label env=dev`,
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			return opts.Validate()
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return p.ListCredentials(opts)
+			return p.PrintCredentials(opts)
 		},
 	}
 
 	f := cmd.Flags()
 	f.StringVarP(&opts.Namespace, "namespace", "n", "",
 		"Namespace in which the credential set is defined. Defaults to the global namespace. Use * to list across all namespaces.")
+	f.BoolVar(&opts.AllNamespaces, "all-namespaces", false,
+		"Include all namespaces in the results.")
 	f.StringVar(&opts.Name, "name", "",
 		"Filter the credential sets where the name contains the specified substring.")
 	f.StringSliceVarP(&opts.Labels, "label", "l", nil,
diff --git a/cmd/porter/installations.go b/cmd/porter/installations.go
index a8c2e77b..bfd31dee 100644
--- a/cmd/porter/installations.go
+++ b/cmd/porter/installations.go
@@ -40,6 +40,7 @@ The results may also be filtered by associated labels and the namespace in which
 Optional output formats include json and yaml.`,
 		Example: `  porter installations list
   porter installations list -o json
+  porter installations list --all-namespaces,
   porter installations list --label owner=myname --namespace dev
   porter installations list --name myapp`,
 		PreRunE: func(cmd *cobra.Command, args []string) error {
@@ -53,6 +54,8 @@ Optional output formats include json and yaml.`,
 	f := cmd.Flags()
 	f.StringVarP(&opts.Namespace, "namespace", "n", "",
 		"Filter the installations by namespace. Defaults to the global namespace.")
+	f.BoolVar(&opts.AllNamespaces, "all-namespaces", false,
+		"Include all namespaces in the results.")
 	f.StringVar(&opts.Name, "name", "",
 		"Filter the installations where the name contains the specified substring.")
 	f.StringSliceVarP(&opts.Labels, "label", "l", nil,
diff --git a/cmd/porter/parameters.go b/cmd/porter/parameters.go
index 02cd4add..00e7af21 100644
--- a/cmd/porter/parameters.go
+++ b/cmd/porter/parameters.go
@@ -109,20 +109,22 @@ Optionally filters the results name, which returns all results whose name contai
 The results may also be filtered by associated labels and the namespace in which the parameter set is defined.`,
 		Example: `  porter parameters list
   porter parameters list --namespace prod -o json
-  porter parameters list --namespace "*"
+  porter parameters list --all-namespaces,
   porter parameters list --name myapp
   porter parameters list --label env=dev`,
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			return opts.Validate()
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return p.ListParameters(opts)
+			return p.PrintParameters(opts)
 		},
 	}
 
 	f := cmd.Flags()
 	f.StringVarP(&opts.Namespace, "namespace", "n", "",
 		"Namespace in which the parameter set is defined. Defaults to the global namespace. Use * to list across all namespaces.")
+	f.BoolVar(&opts.AllNamespaces, "all-namespaces", false,
+		"Include all namespaces in the results.")
 	f.StringVar(&opts.Name, "name", "",
 		"Filter the parameter sets where the name contains the specified substring.")
 	f.StringSliceVarP(&opts.Labels, "label", "l", nil,
diff --git a/docs/content/cli/credentials_list.md b/docs/content/cli/credentials_list.md
index fa1d64ff..35330d3f 100644
--- a/docs/content/cli/credentials_list.md
+++ b/docs/content/cli/credentials_list.md
@@ -23,7 +23,7 @@ porter credentials list [flags]
 ```
   porter credentials list
   porter credentials list --namespace prod
-  porter credentials list --namespace "*"
+  porter credentials list --all-namespaces,
   porter credentials list --name myapp
   porter credentials list --label env=dev
 ```
@@ -31,6 +31,7 @@ porter credentials list [flags]
 ### Options
 
 ```
+      --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the credential sets by a label formatted as: KEY=VALUE. May be specified multiple times.
       --name string        Filter the credential sets where the name contains the specified substring.
diff --git a/docs/content/cli/installations_list.md b/docs/content/cli/installations_list.md
index 59e6cee7..3f40125d 100644
--- a/docs/content/cli/installations_list.md
+++ b/docs/content/cli/installations_list.md
@@ -26,6 +26,7 @@ porter installations list [flags]
 ```
   porter installations list
   porter installations list -o json
+  porter installations list --all-namespaces,
   porter installations list --label owner=myname --namespace dev
   porter installations list --name myapp
 ```
@@ -33,6 +34,7 @@ porter installations list [flags]
 ### Options
 
 ```
+      --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the installations by a label formatted as: KEY=VALUE. May be specified multiple times.
       --name string        Filter the installations where the name contains the specified substring.
diff --git a/docs/content/cli/list.md b/docs/content/cli/list.md
index 678d251a..a312b623 100644
--- a/docs/content/cli/list.md
+++ b/docs/content/cli/list.md
@@ -26,6 +26,7 @@ porter list [flags]
 ```
   porter list
   porter list -o json
+  porter list --all-namespaces,
   porter list --label owner=myname --namespace dev
   porter list --name myapp
 ```
@@ -33,6 +34,7 @@ porter list [flags]
 ### Options
 
 ```
+      --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the installations by a label formatted as: KEY=VALUE. May be specified multiple times.
       --name string        Filter the installations where the name contains the specified substring.
diff --git a/docs/content/cli/parameters_list.md b/docs/content/cli/parameters_list.md
index 6c53be6b..41aef007 100644
--- a/docs/content/cli/parameters_list.md
+++ b/docs/content/cli/parameters_list.md
@@ -23,7 +23,7 @@ porter parameters list [flags]
 ```
   porter parameters list
   porter parameters list --namespace prod -o json
-  porter parameters list --namespace "*"
+  porter parameters list --all-namespaces,
   porter parameters list --name myapp
   porter parameters list --label env=dev
 ```
@@ -31,6 +31,7 @@ porter parameters list [flags]
 ### Options
 
 ```
+      --all-namespaces     Include all namespaces in the results.
   -h, --help               help for list
   -l, --label strings      Filter the parameter sets by a label formatted as: KEY=VALUE. May be specified multiple times.
       --name string        Filter the parameter sets where the name contains the specified substring.
diff --git a/pkg/porter/credentials.go b/pkg/porter/credentials.go
index 43900c08..3e815081 100644
--- a/pkg/porter/credentials.go
+++ b/pkg/porter/credentials.go
@@ -29,8 +29,13 @@ type CredentialEditOptions struct {
 }
 
 // ListCredentials lists saved credential sets.
-func (p *Porter) ListCredentials(opts ListOptions) error {
-	creds, err := p.Credentials.ListCredentialSets(opts.Namespace, opts.Name, opts.ParseLabels())
+func (p *Porter) ListCredentials(opts ListOptions) ([]credentials.CredentialSet, error) {
+	return p.Credentials.ListCredentialSets(opts.GetNamespace(), opts.Name, opts.ParseLabels())
+}
+
+// PrintCredentials prints saved credential sets.
+func (p *Porter) PrintCredentials(opts ListOptions) error {
+	creds, err := p.ListCredentials(opts)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/porter/list.go b/pkg/porter/list.go
index 0010f37c..32bc29a5 100644
--- a/pkg/porter/list.go
+++ b/pkg/porter/list.go
@@ -16,7 +16,8 @@ import (
 // ListOptions represent generic options for use by Porter's list commands
 type ListOptions struct {
 	printer.PrintOptions
-	Namespace string
+	AllNamespaces bool
+	Namespace     string
 	Name      string
 	Labels    []string
 }
@@ -25,6 +26,13 @@ func (o *ListOptions) Validate() error {
 	return o.ParseFormat()
 }
 
+func (o ListOptions) GetNamespace() string {
+	if o.AllNamespaces {
+		return "*"
+	}
+	return o.Namespace
+}
+
 func (o ListOptions) ParseLabels() map[string]string {
 	return parseLabels(o.Labels)
 }
@@ -133,7 +141,7 @@ func NewDisplayRun(run claims.Run) DisplayRun {
 
 // ListInstallations lists installed bundles.
 func (p *Porter) ListInstallations(opts ListOptions) ([]claims.Installation, error) {
-	installations, err := p.Claims.ListInstallations(opts.Namespace, opts.Name, opts.ParseLabels())
+	installations, err := p.Claims.ListInstallations(opts.GetNamespace(), opts.Name, opts.ParseLabels())
 	return installations, errors.Wrap(err, "could not list installations")
 }
 
diff --git a/pkg/porter/parameters.go b/pkg/porter/parameters.go
index 7645f6fd..73389d45 100644
--- a/pkg/porter/parameters.go
+++ b/pkg/porter/parameters.go
@@ -38,8 +38,13 @@ type ParameterEditOptions struct {
 }
 
 // ListParameters lists saved parameter sets.
-func (p *Porter) ListParameters(opts ListOptions) error {
-	params, err := p.Parameters.ListParameterSets(opts.Namespace, opts.Name, opts.ParseLabels())
+func (p *Porter) ListParameters(opts ListOptions) ([]parameters.ParameterSet, error) {
+	return p.Parameters.ListParameterSets(opts.GetNamespace(), opts.Name, opts.ParseLabels())
+}
+
+// PrintParameters prints saved parameter sets.
+func (p *Porter) PrintParameters(opts ListOptions) error {
+	params, err := p.ListParameters(opts)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/pkg/porter/credentials_test.go b/pkg/porter/credentials_test.go
index d5a7e77f..1ec21b80 100644
--- a/pkg/porter/credentials_test.go
+++ b/pkg/porter/credentials_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 	"time"
 
+	"get.porter.sh/porter/pkg/credentials"
 	"get.porter.sh/porter/pkg/printer"
 	"get.porter.sh/porter/pkg/storage"
 	"get.porter.sh/porter/pkg/test"
@@ -121,7 +122,7 @@ func TestCredentialsList_None(t *testing.T) {
 
 			listOpts := ListOptions{}
 			listOpts.Format = tc.format
-			err := p.ListCredentials(listOpts)
+			err := p.PrintCredentials(listOpts)
 			if tc.errorMsg != "" {
 				require.Equal(t, err.Error(), tc.errorMsg)
 			} else {
@@ -136,7 +137,7 @@ func TestCredentialsList_None(t *testing.T) {
 	}
 }
 
-func TestCredentialsList(t *testing.T) {
+func TestPorter_PrintCredentials(t *testing.T) {
 	testcases := []CredentialsListTest{
 		{
 			name:         "json",
@@ -174,7 +175,7 @@ func TestCredentialsList(t *testing.T) {
 			listOpts := ListOptions{}
 			listOpts.Namespace = "dev"
 			listOpts.Format = tc.format
-			err := p.ListCredentials(listOpts)
+			err := p.PrintCredentials(listOpts)
 			require.NoError(t, err)
 
 			gotOutput := p.TestConfig.TestContext.GetOutput()
@@ -185,6 +186,45 @@ func TestCredentialsList(t *testing.T) {
 	}
 }
 
+// Test filtering
+func TestPorter_ListCredentials(t *testing.T) {
+	p := NewTestPorter(t)
+	defer p.Teardown()
+
+	p.TestCredentials.InsertCredentialSet(credentials.NewCredentialSet("", "shared-mysql"))
+	p.TestCredentials.InsertCredentialSet(credentials.NewCredentialSet("dev", "carolyn-wordpress"))
+	p.TestCredentials.InsertCredentialSet(credentials.NewCredentialSet("dev", "vaughn-wordpress"))
+	p.TestCredentials.InsertCredentialSet(credentials.NewCredentialSet("test", "staging-wordpress"))
+	p.TestCredentials.InsertCredentialSet(credentials.NewCredentialSet("test", "iat-wordpress"))
+	p.TestCredentials.InsertCredentialSet(credentials.NewCredentialSet("test", "shared-mysql"))
+
+	t.Run("all-namespaces", func(t *testing.T) {
+		opts := ListOptions{AllNamespaces: true}
+		results, err := p.ListCredentials(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 6)
+	})
+
+	t.Run("local namespace", func(t *testing.T) {
+		opts := ListOptions{Namespace: "dev"}
+		results, err := p.ListCredentials(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 2)
+
+		opts = ListOptions{Namespace: "test"}
+		results, err = p.ListCredentials(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 3)
+	})
+
+	t.Run("global namespace", func(t *testing.T) {
+		opts := ListOptions{Namespace: ""}
+		results, err := p.ListCredentials(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 1)
+	})
+}
+
 type CredentialShowTest struct {
 	name       string
 	format     printer.Format
diff --git a/pkg/porter/list_test.go b/pkg/porter/list_test.go
index 150b7834..1129164e 100644
--- a/pkg/porter/list_test.go
+++ b/pkg/porter/list_test.go
@@ -5,6 +5,7 @@ import (
 
 	"get.porter.sh/porter/pkg/claims"
 	"get.porter.sh/porter/pkg/cnab"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -48,3 +49,41 @@ func TestNewDisplayInstallation(t *testing.T) {
 		require.Empty(t, di.Status, "invalid last status")
 	})
 }
+
+func TestPorter_ListInstallations(t *testing.T) {
+	p := NewTestPorter(t)
+	defer p.Teardown()
+
+	p.TestClaims.CreateInstallation(claims.NewInstallation("", "shared-mysql"))
+	p.TestClaims.CreateInstallation(claims.NewInstallation("dev", "carolyn-wordpress"))
+	p.TestClaims.CreateInstallation(claims.NewInstallation("dev", "vaughn-wordpress"))
+	p.TestClaims.CreateInstallation(claims.NewInstallation("test", "staging-wordpress"))
+	p.TestClaims.CreateInstallation(claims.NewInstallation("test", "iat-wordpress"))
+	p.TestClaims.CreateInstallation(claims.NewInstallation("test", "shared-mysql"))
+
+	t.Run("all-namespaces", func(t *testing.T) {
+		opts := ListOptions{AllNamespaces: true}
+		results, err := p.ListInstallations(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 6)
+	})
+
+	t.Run("local namespace", func(t *testing.T) {
+		opts := ListOptions{Namespace: "dev"}
+		results, err := p.ListInstallations(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 2)
+
+		opts = ListOptions{Namespace: "test"}
+		results, err = p.ListInstallations(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 3)
+	})
+
+	t.Run("global namespace", func(t *testing.T) {
+		opts := ListOptions{Namespace: ""}
+		results, err := p.ListInstallations(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 1)
+	})
+}
diff --git a/pkg/porter/parameters_test.go b/pkg/porter/parameters_test.go
index 0ce34d57..43904d7d 100644
--- a/pkg/porter/parameters_test.go
+++ b/pkg/porter/parameters_test.go
@@ -4,6 +4,7 @@ import (
 	"sort"
 	"testing"
 
+	"get.porter.sh/porter/pkg/parameters"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -44,3 +45,41 @@ func TestGenerateParameterSet(t *testing.T) {
 	require.NoError(t, err, "expected parameter to have been generated")
 	assert.Equal(t, map[string]string{"env": "dev"}, creds.Labels)
 }
+
+func TestPorter_ListParameters(t *testing.T) {
+	p := NewTestPorter(t)
+	defer p.Teardown()
+
+	p.TestParameters.InsertParameterSet(parameters.NewParameterSet("", "shared-mysql"))
+	p.TestParameters.InsertParameterSet(parameters.NewParameterSet("dev", "carolyn-wordpress"))
+	p.TestParameters.InsertParameterSet(parameters.NewParameterSet("dev", "vaughn-wordpress"))
+	p.TestParameters.InsertParameterSet(parameters.NewParameterSet("test", "staging-wordpress"))
+	p.TestParameters.InsertParameterSet(parameters.NewParameterSet("test", "iat-wordpress"))
+	p.TestParameters.InsertParameterSet(parameters.NewParameterSet("test", "shared-mysql"))
+
+	t.Run("all-namespaces", func(t *testing.T) {
+		opts := ListOptions{AllNamespaces: true}
+		results, err := p.ListParameters(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 6)
+	})
+
+	t.Run("local namespace", func(t *testing.T) {
+		opts := ListOptions{Namespace: "dev"}
+		results, err := p.ListParameters(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 2)
+
+		opts = ListOptions{Namespace: "test"}
+		results, err = p.ListParameters(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 3)
+	})
+
+	t.Run("global namespace", func(t *testing.T) {
+		opts := ListOptions{Namespace: ""}
+		results, err := p.ListParameters(opts)
+		require.NoError(t, err)
+		assert.Len(t, results, 1)
+	})
+}
diff --git a/tests/smoke/hello_test.go b/tests/smoke/hello_test.go
index 2fc3aec0..7305ff55 100644
--- a/tests/smoke/hello_test.go
+++ b/tests/smoke/hello_test.go
@@ -68,14 +68,14 @@ func TestHelloBundle(t *testing.T) {
 	require.Equal(t, "test", installations[0]["namespace"], "expected the installation to be in the test namespace")
 
 	// Search by name
-	output, err = test.Porter("list", "--name=mybuns", "--namespace=*", "--output=json").Output()
+	output, err = test.Porter("list", "--name=mybuns", "--all-namespaces", "--output=json").Output()
 	require.NoError(t, err)
 	installations = []map[string]interface{}{}
 	require.NoError(t, json.Unmarshal([]byte(output), &installations))
 	require.Len(t, installations, 2, "expected two installations named mybuns")
 
 	// Search by label
-	output, err = test.Porter("list", "--label", "test=true", "--namespace=*", "--output=json").Output()
+	output, err = test.Porter("list", "--label", "test=true", "--all-namespaces", "--output=json").Output()
 	require.NoError(t, err)
 	installations = []map[string]interface{}{}
 	require.NoError(t, json.Unmarshal([]byte(output), &installations))