2017-07-10 00:33:50 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2017-08-14 19:32:00 +00:00
|
|
|
"reflect"
|
|
|
|
"strconv"
|
2017-07-10 00:33:50 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
is "github.com/containers/image/storage"
|
2017-08-30 20:05:02 +00:00
|
|
|
"github.com/containers/image/types"
|
2017-07-10 00:33:50 +00:00
|
|
|
"github.com/containers/storage"
|
|
|
|
units "github.com/docker/go-units"
|
2017-08-14 19:32:00 +00:00
|
|
|
"github.com/kubernetes-incubator/cri-o/cmd/kpod/formats"
|
2017-08-31 15:17:21 +00:00
|
|
|
"github.com/kubernetes-incubator/cri-o/libpod/common"
|
2017-08-30 20:05:02 +00:00
|
|
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
2017-07-10 00:33:50 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/urfave/cli"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
createdByTruncLength = 45
|
|
|
|
idTruncLength = 13
|
|
|
|
)
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
// historyTemplateParams stores info about each layer
|
|
|
|
type historyTemplateParams struct {
|
|
|
|
ID string
|
|
|
|
Created string
|
|
|
|
CreatedBy string
|
|
|
|
Size string
|
|
|
|
Comment string
|
|
|
|
}
|
|
|
|
|
|
|
|
// historyJSONParams is only used when the JSON format is specified,
|
|
|
|
// and is better for data processing from JSON.
|
|
|
|
// historyJSONParams will be populated by data from v1.History and types.BlobInfo,
|
|
|
|
// the members of the struct are the sama data types as their sources.
|
|
|
|
type historyJSONParams struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
Created *time.Time `json:"created"`
|
|
|
|
CreatedBy string `json:"createdBy"`
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
Comment string `json:"comment"`
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// historyOptions stores cli flag values
|
|
|
|
type historyOptions struct {
|
2017-08-30 20:05:02 +00:00
|
|
|
image string
|
|
|
|
human bool
|
|
|
|
noTrunc bool
|
|
|
|
quiet bool
|
|
|
|
format string
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
historyFlags = []cli.Flag{
|
2017-08-30 20:05:02 +00:00
|
|
|
cli.BoolTFlag{
|
2017-07-10 00:33:50 +00:00
|
|
|
Name: "human, H",
|
|
|
|
Usage: "Display sizes and dates in human readable format",
|
|
|
|
},
|
|
|
|
cli.BoolFlag{
|
2017-08-14 19:32:00 +00:00
|
|
|
Name: "no-trunc, notruncate",
|
2017-07-10 00:33:50 +00:00
|
|
|
Usage: "Do not truncate the output",
|
|
|
|
},
|
|
|
|
cli.BoolFlag{
|
|
|
|
Name: "quiet, q",
|
|
|
|
Usage: "Display the numeric IDs only",
|
|
|
|
},
|
|
|
|
cli.StringFlag{
|
|
|
|
Name: "format",
|
2017-08-14 19:32:00 +00:00
|
|
|
Usage: "Change the output to JSON or a Go template",
|
2017-07-10 00:33:50 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
historyDescription = "Displays the history of an image. The information can be printed out in an easy to read, " +
|
|
|
|
"or user specified format, and can be truncated."
|
|
|
|
historyCommand = cli.Command{
|
|
|
|
Name: "history",
|
|
|
|
Usage: "Show history of a specified image",
|
|
|
|
Description: historyDescription,
|
|
|
|
Flags: historyFlags,
|
|
|
|
Action: historyCmd,
|
|
|
|
ArgsUsage: "",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
func historyCmd(c *cli.Context) error {
|
2017-09-28 18:44:48 +00:00
|
|
|
if err := validateFlags(c, historyFlags); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-07-27 17:18:07 +00:00
|
|
|
config, err := getConfig(c)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "Could not get config")
|
|
|
|
}
|
|
|
|
store, err := getStore(config)
|
2017-07-10 00:33:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
format := genHistoryFormat(c.Bool("quiet"))
|
2017-07-10 00:33:50 +00:00
|
|
|
if c.IsSet("format") {
|
|
|
|
format = c.String("format")
|
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
|
2017-07-10 00:33:50 +00:00
|
|
|
args := c.Args()
|
|
|
|
if len(args) == 0 {
|
2017-08-30 20:05:02 +00:00
|
|
|
return errors.Errorf("an image name must be specified")
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
if len(args) > 1 {
|
2017-08-30 20:05:02 +00:00
|
|
|
return errors.Errorf("Kpod history takes at most 1 argument")
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
imgName := args[0]
|
|
|
|
|
|
|
|
opts := historyOptions{
|
2017-08-30 20:05:02 +00:00
|
|
|
image: imgName,
|
|
|
|
human: c.BoolT("human"),
|
|
|
|
noTrunc: c.Bool("no-trunc"),
|
|
|
|
quiet: c.Bool("quiet"),
|
|
|
|
format: format,
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
return generateHistoryOutput(store, opts)
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
func genHistoryFormat(quiet bool) (format string) {
|
2017-08-14 19:32:00 +00:00
|
|
|
if quiet {
|
2017-08-08 13:24:58 +00:00
|
|
|
return formats.IDString
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
return "table {{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\t"
|
|
|
|
}
|
2017-07-10 00:33:50 +00:00
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
// historyToGeneric makes an empty array of interfaces for output
|
|
|
|
func historyToGeneric(templParams []historyTemplateParams, JSONParams []historyJSONParams) (genericParams []interface{}) {
|
|
|
|
if len(templParams) > 0 {
|
|
|
|
for _, v := range templParams {
|
|
|
|
genericParams = append(genericParams, interface{}(v))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, v := range JSONParams {
|
|
|
|
genericParams = append(genericParams, interface{}(v))
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
2017-08-14 19:32:00 +00:00
|
|
|
return
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
// generate the header based on the template provided
|
|
|
|
func (h *historyTemplateParams) headerMap() map[string]string {
|
|
|
|
v := reflect.Indirect(reflect.ValueOf(h))
|
|
|
|
values := make(map[string]string)
|
|
|
|
for h := 0; h < v.NumField(); h++ {
|
|
|
|
key := v.Type().Field(h).Name
|
|
|
|
value := key
|
|
|
|
values[key] = strings.ToUpper(splitCamelCase(value))
|
|
|
|
}
|
|
|
|
return values
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
// getHistory gets the history of an image and information about its layers
|
|
|
|
func getHistory(store storage.Store, image string) ([]v1.History, []types.BlobInfo, string, error) {
|
|
|
|
ref, err := is.Transport.ParseStoreReference(store, image)
|
2017-07-10 00:33:50 +00:00
|
|
|
if err != nil {
|
2017-08-30 20:05:02 +00:00
|
|
|
return nil, nil, "", errors.Wrapf(err, "error parsing reference to image %q", image)
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
img, err := is.Transport.GetStoreImage(store, ref)
|
2017-07-10 00:33:50 +00:00
|
|
|
if err != nil {
|
2017-08-30 20:05:02 +00:00
|
|
|
return nil, nil, "", errors.Wrapf(err, "no such image %q", image)
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-07-23 23:01:37 +00:00
|
|
|
systemContext := common.GetSystemContext("")
|
2017-07-10 00:33:50 +00:00
|
|
|
|
|
|
|
src, err := ref.NewImage(systemContext)
|
|
|
|
if err != nil {
|
2017-08-30 20:05:02 +00:00
|
|
|
return nil, nil, "", errors.Wrapf(err, "error instantiating image %q", image)
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
oci, err := src.OCIConfig()
|
|
|
|
if err != nil {
|
2017-08-30 20:05:02 +00:00
|
|
|
return nil, nil, "", err
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
return oci.History, src.LayerInfos(), img.ID, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getHistorytemplateOutput gets the modified history information to be printed in human readable format
|
|
|
|
func getHistoryTemplateOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) (historyOutput []historyTemplateParams) {
|
|
|
|
var (
|
|
|
|
outputSize string
|
|
|
|
createdTime string
|
|
|
|
createdBy string
|
|
|
|
count = 1
|
|
|
|
)
|
2017-07-10 00:33:50 +00:00
|
|
|
for i := len(history) - 1; i >= 0; i-- {
|
2017-08-30 20:05:02 +00:00
|
|
|
if i != len(history)-1 {
|
2017-07-10 00:33:50 +00:00
|
|
|
imageID = "<missing>"
|
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
if !opts.noTrunc && i == len(history)-1 {
|
|
|
|
imageID = imageID[:idTruncLength]
|
|
|
|
}
|
2017-07-10 00:33:50 +00:00
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
var size int64
|
2017-07-10 00:33:50 +00:00
|
|
|
if !history[i].EmptyLayer {
|
|
|
|
size = layers[len(layers)-count].Size
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
|
2017-08-14 19:32:00 +00:00
|
|
|
if opts.human {
|
2017-08-30 20:05:02 +00:00
|
|
|
createdTime = units.HumanDuration(time.Since((*history[i].Created))) + " ago"
|
2017-08-14 19:32:00 +00:00
|
|
|
outputSize = units.HumanSize(float64(size))
|
|
|
|
} else {
|
2017-08-30 20:05:02 +00:00
|
|
|
createdTime = (history[i].Created).Format(time.RFC3339)
|
2017-08-14 19:32:00 +00:00
|
|
|
outputSize = strconv.FormatInt(size, 10)
|
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
|
|
|
|
createdBy = strings.Join(strings.Fields(history[i].CreatedBy), " ")
|
|
|
|
if !opts.noTrunc && len(createdBy) > createdByTruncLength {
|
|
|
|
createdBy = createdBy[:createdByTruncLength-3] + "..."
|
|
|
|
}
|
|
|
|
|
|
|
|
params := historyTemplateParams{
|
2017-07-10 00:33:50 +00:00
|
|
|
ID: imageID,
|
2017-08-14 19:32:00 +00:00
|
|
|
Created: createdTime,
|
2017-08-30 20:05:02 +00:00
|
|
|
CreatedBy: createdBy,
|
2017-08-14 19:32:00 +00:00
|
|
|
Size: outputSize,
|
2017-07-10 00:33:50 +00:00
|
|
|
Comment: history[i].Comment,
|
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
historyOutput = append(historyOutput, params)
|
2017-08-14 19:32:00 +00:00
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
return
|
2017-08-14 19:32:00 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
// getHistoryJSONOutput returns the history information in its raw form
|
|
|
|
func getHistoryJSONOutput(history []v1.History, layers []types.BlobInfo, imageID string) (historyOutput []historyJSONParams) {
|
|
|
|
count := 1
|
|
|
|
for i := len(history) - 1; i >= 0; i-- {
|
|
|
|
var size int64
|
|
|
|
if !history[i].EmptyLayer {
|
|
|
|
size = layers[len(layers)-count].Size
|
|
|
|
count++
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
params := historyJSONParams{
|
2017-07-10 00:33:50 +00:00
|
|
|
ID: imageID,
|
|
|
|
Created: history[i].Created,
|
2017-08-30 20:05:02 +00:00
|
|
|
CreatedBy: history[i].CreatedBy,
|
|
|
|
Size: size,
|
2017-07-10 00:33:50 +00:00
|
|
|
Comment: history[i].Comment,
|
|
|
|
}
|
2017-08-14 19:32:00 +00:00
|
|
|
historyOutput = append(historyOutput, params)
|
|
|
|
}
|
2017-08-30 20:05:02 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateHistoryOutput generates the history based on the format given
|
|
|
|
func generateHistoryOutput(store storage.Store, opts historyOptions) error {
|
|
|
|
history, layers, imageID, err := getHistory(store, opts.image)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "error getting history of image %q", opts.image)
|
|
|
|
}
|
|
|
|
if len(history) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
2017-07-10 00:33:50 +00:00
|
|
|
|
2017-08-14 19:32:00 +00:00
|
|
|
var out formats.Writer
|
2017-08-30 20:05:02 +00:00
|
|
|
|
2017-08-14 19:32:00 +00:00
|
|
|
switch opts.format {
|
|
|
|
case formats.JSONString:
|
2017-08-30 20:05:02 +00:00
|
|
|
historyOutput := getHistoryJSONOutput(history, layers, imageID)
|
|
|
|
out = formats.JSONStructArray{Output: historyToGeneric([]historyTemplateParams{}, historyOutput)}
|
2017-08-14 19:32:00 +00:00
|
|
|
default:
|
2017-08-30 20:05:02 +00:00
|
|
|
historyOutput := getHistoryTemplateOutput(history, layers, imageID, opts)
|
|
|
|
out = formats.StdoutTemplateArray{Output: historyToGeneric(historyOutput, []historyJSONParams{}), Template: opts.format, Fields: historyOutput[0].headerMap()}
|
2017-07-10 00:33:50 +00:00
|
|
|
}
|
2017-08-14 19:32:00 +00:00
|
|
|
|
2017-08-30 20:05:02 +00:00
|
|
|
return formats.Writer(out).Out()
|
2017-08-14 19:32:00 +00:00
|
|
|
}
|