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

30431 implement format for history with docs #30962

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
113 changes: 113 additions & 0 deletions cli/command/formatter/history.go
@@ -0,0 +1,113 @@
package formatter

import (
"strconv"
"strings"
"time"

"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
units "github.com/docker/go-units"
)

const (
defaultHistoryTableFormat = "table {{.ID}}\t{{.CreatedSince}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}"
nonHumanHistoryTableFormat = "table {{.ID}}\t{{.CreatedAt}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}"

historyIDHeader = "IMAGE"
createdByHeader = "CREATED BY"
commentHeader = "COMMENT"
)

// NewHistoryFormat returns a format for rendering an HistoryContext
func NewHistoryFormat(source string, quiet bool, human bool) Format {
switch source {
case TableFormatKey:
switch {
case quiet:
return defaultQuietFormat
case !human:
return nonHumanHistoryTableFormat
default:
return defaultHistoryTableFormat
}
}

return Format(source)
}

// HistoryWrite writes the context
func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error {
render := func(format func(subContext subContext) error) error {
for _, history := range histories {
historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human}
if err := format(historyCtx); err != nil {
return err
}
}
return nil
}
historyCtx := &historyContext{}
historyCtx.header = map[string]string{
"ID": historyIDHeader,
"CreatedSince": createdSinceHeader,
"CreatedAt": createdAtHeader,
"CreatedBy": createdByHeader,
"Size": sizeHeader,
"Comment": commentHeader,
}
return ctx.Write(historyCtx, render)
}

type historyContext struct {
HeaderContext
trunc bool
human bool
h image.HistoryResponseItem
}

func (c *historyContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}

func (c *historyContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.h.ID)
}
return c.h.ID
}

func (c *historyContext) CreatedAt() string {
var created string
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
return created
}

func (c *historyContext) CreatedSince() string {
var created string
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
return created + " ago"
}
Copy link
Member

Choose a reason for hiding this comment

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

Also, it seems that CreatedAt and CreatedSince is pretty much the same except for the ago. And --human flag does not have an impact to the rendering

I am wondering if CreatedAt should use time.Unix(int64(c.h.Created), 0).String() (just like docker images --format) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ended up having to separate these again due to some changes in how the headers are output with the formatters. I added to the docs to have both in the template.


func (c *historyContext) CreatedBy() string {
createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1)
if c.trunc {
createdBy = stringutils.Ellipsis(createdBy, 45)
}
return createdBy
}

func (c *historyContext) Size() string {
size := ""
if c.human {
size = units.HumanSizeWithPrecision(float64(c.h.Size), 3)
} else {
size = strconv.FormatInt(c.h.Size, 10)
}
return size
}

func (c *historyContext) Comment() string {
return c.h.Comment
}
213 changes: 213 additions & 0 deletions cli/command/formatter/history_test.go
@@ -0,0 +1,213 @@
package formatter

import (
"strconv"
"strings"
"testing"
"time"

"bytes"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/pkg/testutil/assert"
)

type historyCase struct {
historyCtx historyContext
expValue string
call func() string
}

func TestHistoryContext_ID(t *testing.T) {
id := stringid.GenerateRandomID()

var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{ID: id},
trunc: false,
}, id, ctx.ID,
},
{
historyContext{
h: image.HistoryResponseItem{ID: id},
trunc: true,
}, stringid.TruncateID(id), ctx.ID,
},
}

for _, c := range cases {
ctx = c.historyCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}

func TestHistoryContext_CreatedSince(t *testing.T) {
unixTime := time.Now().AddDate(0, 0, -7).Unix()
expected := "7 days ago"

var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Created: unixTime},
trunc: false,
human: true,
}, expected, ctx.CreatedSince,
},
}

for _, c := range cases {
ctx = c.historyCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}

func TestHistoryContext_CreatedBy(t *testing.T) {
withTabs := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*`
expected := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*`

var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{CreatedBy: withTabs},
trunc: false,
}, expected, ctx.CreatedBy,
},
{
historyContext{
h: image.HistoryResponseItem{CreatedBy: withTabs},
trunc: true,
}, stringutils.Ellipsis(expected, 45), ctx.CreatedBy,
},
}

for _, c := range cases {
ctx = c.historyCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}

func TestHistoryContext_Size(t *testing.T) {
size := int64(182964289)
expected := "183MB"

var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Size: size},
trunc: false,
human: true,
}, expected, ctx.Size,
}, {
historyContext{
h: image.HistoryResponseItem{Size: size},
trunc: false,
human: false,
}, strconv.Itoa(182964289), ctx.Size,
},
}

for _, c := range cases {
ctx = c.historyCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}

func TestHistoryContext_Comment(t *testing.T) {
comment := "Some comment"

var ctx historyContext
cases := []historyCase{
{
historyContext{
h: image.HistoryResponseItem{Comment: comment},
trunc: false,
}, comment, ctx.Comment,
},
}

for _, c := range cases {
ctx = c.historyCtx
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
}
}
}

func TestHistoryContext_Table(t *testing.T) {
out := bytes.NewBufferString("")
unixTime := time.Now().AddDate(0, 0, -1).Unix()
histories := []image.HistoryResponseItem{
{ID: "imageID1", Created: unixTime, CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
{ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
{ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
{ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}},
}
expectedNoTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT
imageID1 24 hours ago /bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on 183MB Hi
imageID2 24 hours ago /bin/bash echo 183MB Hi
imageID3 24 hours ago /bin/bash ls 183MB Hi
imageID4 24 hours ago /bin/bash grep 183MB Hi
`
expectedTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT
imageID1 24 hours ago /bin/bash ls && npm i && npm run test && k... 183MB Hi
imageID2 24 hours ago /bin/bash echo 183MB Hi
imageID3 24 hours ago /bin/bash ls 183MB Hi
imageID4 24 hours ago /bin/bash grep 183MB Hi
`

contexts := []struct {
context Context
expected string
}{
{Context{
Format: NewHistoryFormat("table", false, true),
Trunc: true,
Output: out,
},
expectedTrunc,
},
{Context{
Format: NewHistoryFormat("table", false, true),
Trunc: false,
Output: out,
},
expectedNoTrunc,
},
}

for _, context := range contexts {
HistoryWrite(context.context, true, histories)
assert.Equal(t, out.String(), context.expected)
// Clean buffer
out.Reset()
}
}