package main import ( "encoding/json" "fmt" "strings" "text/template" "time" "os" "strconv" "github.com/Sirupsen/logrus" is "github.com/containers/image/storage" "github.com/containers/storage" units "github.com/docker/go-units" "github.com/pkg/errors" "github.com/urfave/cli" ) const ( createdByTruncLength = 45 idTruncLength = 13 ) // historyOutputParams stores info about each layer type historyOutputParams struct { ID string `json:"id"` Created *time.Time `json:"created"` CreatedBy string `json:"createdby"` Size int64 `json:"size"` Comment string `json:"comment"` } // historyOptions stores cli flag values type historyOptions struct { image string human bool noTrunc bool quiet bool format string } var ( historyFlags = []cli.Flag{ cli.BoolFlag{ Name: "human, H", Usage: "Display sizes and dates in human readable format", }, cli.BoolFlag{ Name: "no-trunc", Usage: "Do not truncate the output", }, cli.BoolFlag{ Name: "quiet, q", Usage: "Display the numeric IDs only", }, cli.StringFlag{ Name: "format", Usage: "Pretty-print history of the image using a Go template", }, cli.BoolFlag{ Name: "json", Usage: "Print the history in JSON format", }, } 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 { store, err := getStore(c) if err != nil { return err } human := true if c.IsSet("human") { human = c.Bool("human") } noTruncate := false if c.IsSet("no-trunc") { noTruncate = c.Bool("no-trunc") } quiet := false if c.IsSet("quiet") { quiet = c.Bool("quiet") } json := false if c.IsSet("json") { json = c.Bool("json") } format := "" if c.IsSet("format") { format = c.String("format") } args := c.Args() if len(args) == 0 { logrus.Errorf("an image name must be specified") return nil } if len(args) > 1 { logrus.Errorf("Kpod history takes at most 1 argument") return nil } imgName := args[0] opts := historyOptions{ image: imgName, human: human, noTrunc: noTruncate, quiet: quiet, format: format, } var history []byte if json { history, err = createJSON(store, opts) fmt.Println(string(history)) } else { if format == "" && !quiet { outputHeading(noTruncate) } err = outputHistory(store, opts) } return err } // outputHeader outputs the heading func outputHeading(noTrunc bool) { if !noTrunc { fmt.Printf("%-12s\t\t%-16s\t\t%-45s\t\t", "IMAGE", "CREATED", "CREATED BY") fmt.Printf("%-16s\t\t%s\n", "SIZE", "COMMENT") } else { fmt.Printf("%-64s\t%-18s\t%-60s\t", "IMAGE", "CREATED", "CREATED BY") fmt.Printf("%-16s\t%s\n", "SIZE", "COMMENT") } } // outputString outputs the information in historyOutputParams func outputString(noTrunc, human bool, params historyOutputParams) { var ( createdTime string outputSize string ) if human { createdTime = outputHumanTime(params.Created) + " ago" outputSize = units.HumanSize(float64(params.Size)) } else { createdTime = outputTime(params.Created) outputSize = strconv.FormatInt(params.Size, 10) } if !noTrunc { fmt.Printf("%-12.12s\t\t%-16s\t\t%-45.45s\t\t", params.ID, createdTime, params.CreatedBy) fmt.Printf("%-16s\t\t%s\n", outputSize, params.Comment) } else { fmt.Printf("%-64s\t%-18s\t%-60s\t", params.ID, createdTime, params.CreatedBy) fmt.Printf("%-16s\t%s\n\n", outputSize, params.Comment) } } // outputWithTemplate is called when --format is given a template func outputWithTemplate(format string, params historyOutputParams, human bool) error { templ, err := template.New("history").Parse(format) if err != nil { return errors.Wrapf(err, "error parsing template") } createdTime := outputTime(params.Created) outputSize := strconv.FormatInt(params.Size, 10) if human { createdTime = outputHumanTime(params.Created) + " ago" outputSize = units.HumanSize(float64(params.Size)) } // templParams is used to store the info from params and the time and // size that have been converted to type string for when the human flag // is set templParams := struct { ID string Created string CreatedBy string Size string Comment string }{ params.ID, createdTime, params.CreatedBy, outputSize, params.Comment, } if err = templ.Execute(os.Stdout, templParams); err != nil { return err } fmt.Println() return nil } // outputTime displays the time stamp in "2017-06-20T20:24:10Z" format func outputTime(tm *time.Time) string { return tm.Format(time.RFC3339) } // outputHumanTime displays the time elapsed since creation func outputHumanTime(tm *time.Time) string { return units.HumanDuration(time.Since(*tm)) } // createJSON retrieves the history of the image and returns a JSON object func createJSON(store storage.Store, opts historyOptions) ([]byte, error) { var ( size int64 img *storage.Image imageID string layerAll []historyOutputParams ) ref, err := is.Transport.ParseStoreReference(store, opts.image) if err != nil { return nil, errors.Errorf("error parsing reference to image %q: %v", opts.image, err) } img, err = is.Transport.GetStoreImage(store, ref) if err != nil { return nil, errors.Errorf("no such image %q: %v", opts.image, err) } systemContext := getSystemContext("") src, err := ref.NewImage(systemContext) if err != nil { return nil, errors.Errorf("error instantiating image %q: %v", opts.image, err) } oci, err := src.OCIConfig() if err != nil { return nil, err } history := oci.History layers := src.LayerInfos() count := 1 // iterating backwards to get newwest to oldest for i := len(history) - 1; i >= 0; i-- { if i == len(history)-1 { imageID = img.ID } else { imageID = "" } if !history[i].EmptyLayer { size = layers[len(layers)-count].Size count++ } else { size = 0 } params := historyOutputParams{ ID: imageID, Created: history[i].Created, CreatedBy: history[i].CreatedBy, Size: size, Comment: history[i].Comment, } layerAll = append(layerAll, params) } output, err := json.MarshalIndent(layerAll, "", "\t\t") if err != nil { return nil, errors.Errorf("error marshalling to JSON: %v", err) } if err = src.Close(); err != nil { return nil, err } return output, nil } // outputHistory gets the history of the image from the JSON object // and pretty prints it to the screen func outputHistory(store storage.Store, opts historyOptions) error { var ( outputCreatedBy string imageID string history []historyOutputParams ) raw, err := createJSON(store, opts) if err != nil { return errors.Errorf("error creating JSON: %v", err) } if err = json.Unmarshal(raw, &history); err != nil { return errors.Errorf("error Unmarshalling JSON: %v", err) } for i := 0; i < len(history); i++ { imageID = history[i].ID outputCreatedBy = strings.Join(strings.Fields(history[i].CreatedBy), " ") if !opts.noTrunc && len(outputCreatedBy) > createdByTruncLength { outputCreatedBy = outputCreatedBy[:createdByTruncLength-3] + "..." } if !opts.noTrunc && i == 0 { imageID = history[i].ID[:idTruncLength] } if opts.quiet { if !opts.noTrunc { fmt.Printf("%-12.12s\n", imageID) } else { fmt.Printf("%-s\n", imageID) } continue } params := historyOutputParams{ ID: imageID, Created: history[i].Created, CreatedBy: outputCreatedBy, Size: history[i].Size, Comment: history[i].Comment, } if len(opts.format) > 0 { if err = outputWithTemplate(opts.format, params, opts.human); err != nil { return errors.Errorf("error outputing with template: %v", err) } continue } outputString(opts.noTrunc, opts.human, params) } return nil }