Skip to content

Commit

Permalink
add format option for pouch ps and integrate/unit test
Browse files Browse the repository at this point in the history
Signed-off-by: ao hang <aohang111@gmail.com>
  • Loading branch information
aohang111 committed May 20, 2019
1 parent 5c253f9 commit e29fed0
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 17 deletions.
15 changes: 15 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"strconv"
"text/tabwriter"
"text/template"
"time"

"github.com/alibaba/pouch/client"
Expand Down Expand Up @@ -159,6 +160,20 @@ func (c *Cli) Print(obj interface{}) {
display.Flush()
}

// FormatDisplay use to format output with options
func (c *Cli) FormatDisplay(format string, tmpl *template.Template, container interface{}) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, c.padding, ' ', 0)
tmpl, err := tmpl.Parse(format)
if err != nil {
return err
}
if err := tmpl.Execute(w, container); err != nil {
return err
}
w.Flush()
return nil
}

// ExitError defines exit error produce by cli commands.
type ExitError struct {
Code int
Expand Down
48 changes: 48 additions & 0 deletions cli/formatter/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package formatter

import (
"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/pkg/utils"
)

// ContainerHeader is the map to show container head
var ContainerHeader = map[string]string{
"Name": "Name",
"ID": "ID",
"Status": "Status",
"Created": "Created",
"Image": "Image",
"Runtime": "Runtime",
"Command": "Command",
"ImageID": "ImageID",
"Labels": "Labels",
// "Mounts": "Mounts",
"State": "State",
}

// ContainerContext is the map to show container context detail
type ContainerContext map[string]string

// NewContainerContext is to generate a ContainerContext to be show
func NewContainerContext(c *types.Container, flagNoTrunc bool) ContainerContext {
id := c.ID[:6]
if flagNoTrunc {
id = c.ID
}
created, _ := utils.FormatTimeInterval(c.Created)
labels := LabelsToString(c.Labels)
containerContext := ContainerContext{
"Name": c.Names[0],
"ID": id,
"Status": c.Status,
"Created": created + " ago",
"Image": c.Image,
"Runtime": c.HostConfig.Runtime,
"Command": c.Command,
"ImageID": c.ImageID,
"Labels": labels,
// "Mounts": "Mounts",
"State": c.State,
}
return containerContext
}
78 changes: 78 additions & 0 deletions cli/formatter/container_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package formatter

import (
"testing"

"github.com/alibaba/pouch/apis/types"

"github.com/stretchr/testify/assert"
)

func TestNewContainerContext(t *testing.T) {
type TestCase struct {
container *types.Container
flagNoTrunc bool
expected ContainerContext
}

testCases := []TestCase{
{
container: &types.Container{
Command: "bash",
Created: 4,
HostConfig: &types.HostConfig{Runtime: "runc"},
ID: "abcdelj8937",
Image: "Image123",
ImageID: "234567890",
Labels: map[string]string{"a": "b", "c": "d"},
Names: []string{"nameA", "nameB"},
State: "StateA",
Status: "StatusB",
},
flagNoTrunc: false,
expected: ContainerContext{
"Name": "nameA",
"ID": "abcdel",
"Status": "StatusB",
"Created": "49 years" + " ago",
"Image": "Image123",
"Runtime": "runc",
"Command": "bash",
"ImageID": "234567890",
"Labels": "a = b c = d ",
"State": "StateA",
},
},
{
container: &types.Container{
Command: "shell",
Created: 5,
HostConfig: &types.HostConfig{Runtime: "runv"},
ID: "1234567890",
Image: "Image456",
ImageID: "abcdefg",
Labels: map[string]string{},
Names: []string{"nameB", "nameA"},
State: "StateB",
Status: "StatusA",
},
flagNoTrunc: true,
expected: ContainerContext{
"Name": "nameB",
"ID": "1234567890",
"Status": "StatusA",
"Created": "49 years" + " ago",
"Image": "Image456",
"Runtime": "runv",
"Command": "shell",
"ImageID": "abcdefg",
"Labels": "",
"State": "StateB",
},
},
}
for _, testCase := range testCases {
result := NewContainerContext(testCase.container, testCase.flagNoTrunc)
assert.Equal(t, testCase.expected, result)
}
}
51 changes: 51 additions & 0 deletions cli/formatter/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package formatter

import (
"fmt"
"sort"
"strings"
)

// Format key to output
const (
TableFormat = "table"
RawFormat = "raw"
)

// IsTable to verify if table or raw
func IsTable(format string) bool {
return strings.HasPrefix(format, TableFormat)
}

// PreFormat is to format the format option
func PreFormat(format string) string {
if IsTable(format) {
format = format[len(TableFormat):]
//cut the space
format = strings.Trim(format, " ")
//input by cmd of "\t" is "\\t"
replace := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
format = replace.Replace(format)

if format[len(format)-1:] != "\n" {
format += "\n"
}
} else {
format = "Name:{{.Name}}\nID:{{.ID}}\nStatus:{{.Status}}\nCreated:{{.Created}}\nImage:{{.Image}}\nRuntime:{{.Runtime}}\n\n"
}
return format
}

// LabelsToString is to transfor the labels from map to string
func LabelsToString(labels map[string]string) string {
var labelstring string
sortedkeys := make([]string, 0)
for key := range labels {
sortedkeys = append(sortedkeys, key)
}
sort.Strings(sortedkeys)
for _, key := range sortedkeys {
labelstring += fmt.Sprintf("%s = %s ", key, labels[key])
}
return labelstring
}
67 changes: 67 additions & 0 deletions cli/formatter/formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package formatter

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestPreFormat(t *testing.T) {
type TestCase struct {
input string
output string
}

testCases := []TestCase{
{
input: "table {{.ID}}\t{{.Status}}\t{{.Created}}\t{{.Name}}\t{{.Image}}\t{{.State}}",
output: "{{.ID}}\t{{.Status}}\t{{.Created}}\t{{.Name}}\t{{.Image}}\t{{.State}}\n",
},
{
input: "table {{.ID}}\\t{{.Status}}\\t{{.Created}}\\t{{.Name}}\\t{{.Image}}\\t{{.State}}",
output: "{{.ID}}\t{{.Status}}\t{{.Created}}\t{{.Name}}\t{{.Image}}\t{{.State}}\n",
},
{
input: "table {{.ID}}\t{{.Status}}\t{{.Created}}\\t{{.Name}}\t{{.Image}}\t{{.State}}\n",
output: "{{.ID}}\t{{.Status}}\t{{.Created}}\t{{.Name}}\t{{.Image}}\t{{.State}}\n",
},
{
input: "raw",
output: "Name:{{.Name}}\nID:{{.ID}}\nStatus:{{.Status}}\nCreated:{{.Created}}\nImage:{{.Image}}\nRuntime:{{.Runtime}}\n\n",
},
}
for _, testCase := range testCases {
result := PreFormat(testCase.input)
assert.Equal(t, testCase.output, result)
}
}

func TestLabelToString(t *testing.T) {
type TestCase struct {
input map[string]string
output string
}
testCases := []TestCase{
{
input: map[string]string{
"a": "b",
"c": "d",
},
output: "a = b c = d ",
},
{
input: map[string]string{
"a": "b",
},
output: "a = b ",
},
{
input: map[string]string{},
output: "",
},
}
for _, testCase := range testCases {
result := LabelsToString(testCase.input)
assert.Equal(t, testCase.output, result)
}
}
38 changes: 21 additions & 17 deletions cli/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import (
"context"
"fmt"
"sort"
"text/template"
"time"

"github.com/alibaba/pouch/apis/types"
"github.com/alibaba/pouch/pkg/utils"
"github.com/alibaba/pouch/cli/formatter"
"github.com/alibaba/pouch/pkg/utils/filters"

"github.com/spf13/cobra"
)

// psDescription is used to describe ps command in detail and auto generate command doc.
var psDescription = "\nList Containers with container name, ID, status, creation time, image reference and runtime."
var psDefaultFormat = "table {{.Name}}\t{{.ID}}\t{{.Status}}\t{{.Created}}\t{{.Image}}\t{{.Runtime}}\n"

// containerList is used to save the container list.
type containerList []*types.Container
Expand All @@ -28,6 +30,7 @@ type PsCommand struct {
flagQuiet bool
flagNoTrunc bool
flagFilter []string
flagFormat string
}

// Init initializes PsCommand command.
Expand All @@ -52,6 +55,7 @@ func (p *PsCommand) addFlags() {
flagSet.BoolVarP(&p.flagAll, "all", "a", false, "Show all containers (default shows just running)")
flagSet.BoolVarP(&p.flagQuiet, "quiet", "q", false, "Only show numeric IDs")
flagSet.BoolVar(&p.flagNoTrunc, "no-trunc", false, "Do not truncate output")
flagSet.StringVarP(&p.flagFormat, "format", "", "", "intelligent-print containers based on Go template")
flagSet.StringSliceVarP(&p.flagFilter, "filter", "f", nil, "Filter output based on given conditions, support filter key [ id label name status ]")
}

Expand Down Expand Up @@ -88,24 +92,24 @@ func (p *PsCommand) runPs(args []string) error {
}
return nil
}

display := p.cli.NewTableDisplay()
display.AddRow([]string{"Name", "ID", "Status", "Created", "Image", "Runtime"})

// add to format the output with go template
format := p.flagFormat
tmplH := template.New("ps_head")
if len(format) == 0 {
format = psDefaultFormat
}
// true is table,false is raw
tableOrRaw := formatter.IsTable(format)
format = formatter.PreFormat(format)
if tableOrRaw {
containerHeader := formatter.ContainerHeader
p.cli.FormatDisplay(format, tmplH, containerHeader)
}
for _, c := range containers {
created, err := utils.FormatTimeInterval(c.Created)
if err != nil {
return err
}

id := c.ID[:6]
if p.flagNoTrunc {
id = c.ID
}

display.AddRow([]string{c.Names[0], id, c.Status, created + " ago", c.Image, c.HostConfig.Runtime})
containerContext := formatter.NewContainerContext(c, p.flagNoTrunc)
tmplD := template.New("ps_detail")
p.cli.FormatDisplay(format, tmplD, containerContext)
}
display.Flush()
return nil
}

Expand Down
Loading

0 comments on commit e29fed0

Please sign in to comment.