Merge pull request #30962 from TheHipbot/30431-implement-format-for-history-with-docs
30431 implement format for history with docs
This commit is contained in:
commit
a3ab46361e
4 changed files with 381 additions and 50 deletions
113
cli/command/formatter/history.go
Normal file
113
cli/command/formatter/history.go
Normal file
|
@ -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"
|
||||
}
|
||||
|
||||
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
cli/command/formatter/history_test.go
Normal file
213
cli/command/formatter/history_test.go
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,19 +1,11 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cli/command"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/docker/docker/cli/command/formatter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -23,6 +15,7 @@ type historyOptions struct {
|
|||
human bool
|
||||
quiet bool
|
||||
noTrunc bool
|
||||
format string
|
||||
}
|
||||
|
||||
// NewHistoryCommand creates a new `docker history` command
|
||||
|
@ -44,6 +37,7 @@ func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command {
|
|||
flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -56,44 +50,15 @@ func runHistory(dockerCli *command.DockerCli, opts historyOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
|
||||
|
||||
if opts.quiet {
|
||||
for _, entry := range history {
|
||||
if opts.noTrunc {
|
||||
fmt.Fprintf(w, "%s\n", entry.ID)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\n", stringid.TruncateID(entry.ID))
|
||||
}
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
format := opts.format
|
||||
if len(format) == 0 {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
|
||||
var imageID string
|
||||
var createdBy string
|
||||
var created string
|
||||
var size string
|
||||
|
||||
fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT")
|
||||
for _, entry := range history {
|
||||
imageID = entry.ID
|
||||
createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1)
|
||||
if !opts.noTrunc {
|
||||
createdBy = stringutils.Ellipsis(createdBy, 45)
|
||||
imageID = stringid.TruncateID(entry.ID)
|
||||
}
|
||||
|
||||
if opts.human {
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago"
|
||||
size = units.HumanSizeWithPrecision(float64(entry.Size), 3)
|
||||
} else {
|
||||
created = time.Unix(entry.Created, 0).Format(time.RFC3339)
|
||||
size = strconv.FormatInt(entry.Size, 10)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", imageID, created, createdBy, size, entry.Comment)
|
||||
historyCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human),
|
||||
Trunc: !opts.noTrunc,
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
return formatter.HistoryWrite(historyCtx, opts.human, history)
|
||||
}
|
||||
|
|
|
@ -21,10 +21,11 @@ Usage: docker history [OPTIONS] IMAGE
|
|||
Show the history of an image
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
-H, --human Print sizes and dates in human readable format (default true)
|
||||
--no-trunc Don't truncate output
|
||||
-q, --quiet Only show numeric IDs
|
||||
--format string Pretty-print images using a Go template
|
||||
--help Print usage
|
||||
-H, --human Print sizes and dates in human readable format (default true)
|
||||
--no-trunc Don't truncate output
|
||||
-q, --quiet Only show numeric IDs
|
||||
```
|
||||
|
||||
|
||||
|
@ -54,3 +55,42 @@ IMAGE CREATED CREATED BY
|
|||
c69cab00d6ef 5 months ago /bin/sh -c #(nop) MAINTAINER Lokesh Mandvekar 0 B
|
||||
511136ea3c5a 19 months ago 0 B Imported from -
|
||||
```
|
||||
|
||||
### Format the output
|
||||
|
||||
The formatting option (`--format`) will pretty print history output
|
||||
using a Go template.
|
||||
|
||||
Valid placeholders for the Go template are listed below:
|
||||
|
||||
| Placeholder | Description|
|
||||
| ---- | ---- |
|
||||
| `.ID` | Image ID |
|
||||
| `.CreatedSince` | Elapsed time since the image was created if --human=true, otherwise timestamp of when image was created |
|
||||
| `.CreatedAt` | Timestamp of when image was created |
|
||||
| `.CreatedBy` | Command that was used to create the image |
|
||||
| `.Size` | Image disk size |
|
||||
| `.Comment` | Comment for image |
|
||||
|
||||
When using the `--format` option, the `history` command will either
|
||||
output the data exactly as the template declares or, when using the
|
||||
`table` directive, will include column headers as well.
|
||||
|
||||
The following example uses a template without headers and outputs the
|
||||
`ID` and `CreatedSince` entries separated by a colon for all images:
|
||||
|
||||
```bash
|
||||
{% raw %}
|
||||
$ docker images --format "{{.ID}}: {{.Created}} ago"
|
||||
|
||||
cc1b61406712: 2 weeks ago
|
||||
<missing>: 2 weeks ago
|
||||
<missing>: 2 weeks ago
|
||||
<missing>: 2 weeks ago
|
||||
<missing>: 2 weeks ago
|
||||
<missing>: 3 weeks ago
|
||||
<missing>: 3 weeks ago
|
||||
<missing>: 3 weeks ago
|
||||
|
||||
{% endraw %}
|
||||
```
|
Loading…
Reference in a new issue