Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add porter bundle list command #329

Merged
merged 3 commits into from
May 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions cmd/porter/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"github.com/deislabs/porter/pkg/porter"
"github.com/deislabs/porter/pkg/printer"
"github.com/spf13/cobra"
)

Expand All @@ -20,6 +21,7 @@ func buildBundlesCommand(p *porter.Porter) *cobra.Command {

cmd.AddCommand(buildBundleCreateCommand(p))
cmd.AddCommand(buildBundleBuildCommand(p))
cmd.AddCommand(buildBundleListCommand(p))
cmd.AddCommand(buildBundleInstallCommand(p))
cmd.AddCommand(buildBundleUpgradeCommand(p))
cmd.AddCommand(buildBundleUninstallCommand(p))
Expand Down Expand Up @@ -77,6 +79,36 @@ func buildBuildCommand(p *porter.Porter) *cobra.Command {
return cmd
}

func buildBundleListCommand(p *porter.Porter) *cobra.Command {
opts := porter.ListOptions{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay! Thanks for fixing my past mistakes for me. 🙇‍♀


cmd := &cobra.Command{
Use: "list",
Short: "list installed bundles",
Long: `List all bundles installed by Porter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯


A listing of bundles currently installed by Porter will be provided, along with metadata such as creation time, last action, last status, etc.

Optional output formats include json and yaml.`,
Example: ` porter bundle list
porter bundle list -o json`,
PreRunE: func(cmd *cobra.Command, args []string) error {
var err error
opts.Format, err = printer.ParseFormat(opts.RawFormat)
return err
},
RunE: func(cmd *cobra.Command, args []string) error {
return p.ListBundles(printer.PrintOptions{Format: opts.Format})
},
}

f := cmd.Flags()
f.StringVarP(&opts.RawFormat, "output", "o", "table",
"Specify an output format. Allowed values: table, json, yaml")

return cmd
}

func buildBundleInstallCommand(p *porter.Porter) *cobra.Command {
opts := porter.InstallOptions{}
cmd := &cobra.Command{
Expand Down
31 changes: 31 additions & 0 deletions cmd/porter/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,34 @@ func TestValidateUninstallCommand(t *testing.T) {
})
}
}

func TestValidateBundleListCommand(t *testing.T) {
testcases := []struct {
name string
args string
wantError string
}{
{"no args", "bundle list", ""},
{"output json", "bundle list -o json", ""},
{"invalid format", "bundle list -o wingdings", "invalid format: wingdings"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha wingdings

}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
p := buildRootCommand()
osargs := strings.Split(tc.args, " ")
cmd, args, err := p.Find(osargs)
require.NoError(t, err)

err = cmd.ParseFlags(args)
require.NoError(t, err)

err = cmd.PreRunE(cmd, cmd.Flags().Args())
if tc.wantError == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.wantError)
}
})
}
}
72 changes: 72 additions & 0 deletions pkg/porter/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package porter

import (
"fmt"

"github.com/pkg/errors"

cnab "github.com/deislabs/porter/pkg/cnab/provider"
printer "github.com/deislabs/porter/pkg/printer"
)

// ListOptions represent options for a bundle list command
type ListOptions struct {
RawFormat string
Format printer.Format
}

const (
// TimeFormat is used to generate a human-readable representation of a raw time.Time value
TimeFormat = "Mon Jan _2 15:04:05"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Seriously, I'm so going to make a prettytime pkg now.

)

// CondensedClaim holds a subset of pertinent values to be listed from a claim.Claim
type CondensedClaim struct {
Name string
Created string
Modified string
Action string
Status string
}

// ListBundles lists installed bundles using the printer.Format provided
func (p *Porter) ListBundles(opts printer.PrintOptions) error {
cp := cnab.NewDuffle(p.Config)
claimStore := cp.NewClaimStore()
claims, err := claimStore.ReadAll()
if err != nil {
return errors.Wrap(err, "could not list claims")
}

var condensedClaims []CondensedClaim
for _, claim := range claims {
condensedClaim := CondensedClaim{
Name: claim.Name,
Created: fmt.Sprint(claim.Created.Format(TimeFormat)),
Modified: fmt.Sprint(claim.Modified.Format(TimeFormat)),
Action: claim.Result.Action,
Status: claim.Result.Status,
}
condensedClaims = append(condensedClaims, condensedClaim)
}

switch opts.Format {
case printer.FormatJson:
return printer.PrintJson(p.Out, condensedClaims)
case printer.FormatYaml:
return printer.PrintYaml(p.Out, condensedClaims)
case printer.FormatTable:
printClaimRow :=
func(v interface{}) []interface{} {
cl, ok := v.(CondensedClaim)
if !ok {
return nil
}
return []interface{}{cl.Name, cl.Created, cl.Modified, cl.Action, cl.Status}
}
return printer.PrintTable(p.Out, condensedClaims, printClaimRow,
"NAME", "CREATED", "MODIFIED", "LAST ACTION", "LAST STATUS")
default:
return fmt.Errorf("invalid format: %s", opts.Format)
}
}
18 changes: 16 additions & 2 deletions pkg/printer/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,24 @@ func PrintTable(out io.Writer, v interface{}, getRow func(row interface{}) []int

table := NewTableWriter(out)
if len(headers) > 0 {
fmt.Fprintln(table, headers...)
fmt.Fprintln(table, tabify(headers)...)
}
for i := 0; i < rows.Len(); i++ {
fmt.Fprintln(table, getRow(rows.Index(i).Interface())...)
fmt.Fprintln(table, tabify(getRow(rows.Index(i).Interface()))...)
}
return table.Flush()
}

// tabify is a helper function which takes a slice and injects tab characters
// between each element such that tabwriter can work its magic
func tabify(untabified []interface{}) []interface{} {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh! I always use a better package than the stdlib text/tabwriter and completely forgot to do this.

I think I'll do a follow-up later to switch to a table writer package that I like better but thank you, this fixes the stdlib for now. 👍

var tabified []interface{}
for i := 0; i < len(untabified); i++ {
tabified = append(tabified, untabified[i])
// only append tab character if prior to last element
if i+1 < len(untabified) {
tabified = append(tabified, "\t")
}
}
return tabified
}
7 changes: 4 additions & 3 deletions pkg/printer/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import (
)

type testType struct {
A, B string
A, B interface{}
}

func TestPrintTable(t *testing.T) {
v := []testType{
{A: "foo", B: "bar"},
{A: "baz", B: "qux"},
{A: 123, B: true},
}

b := &bytes.Buffer{}
Expand All @@ -27,7 +28,7 @@ func TestPrintTable(t *testing.T) {
"A", "B")

require.Nil(t, err)
require.Equal(t, "A B\nfoo bar\nbaz qux\n", b.String())
require.Equal(t, "A B\nfoo bar\nbaz qux\n123 true\n", b.String())
}

func TestPrintTable_WithoutHeaders(t *testing.T) {
Expand All @@ -44,5 +45,5 @@ func TestPrintTable_WithoutHeaders(t *testing.T) {
})

require.Nil(t, err)
require.Equal(t, "foo bar\n", b.String())
require.Equal(t, "foo bar\n", b.String())
}