Merge pull request #643 from umohnani8/kpod_history

Add 'kpod history' command
This commit is contained in:
Mrunal Patel 2017-07-19 16:15:28 -07:00 committed by GitHub
commit a7c1745aa2
6 changed files with 518 additions and 1 deletions

View file

@ -41,6 +41,7 @@ It is currently in active development in the Kubernetes community through the [d
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| [crio(8)](/docs/crio.8.md) | Enable OCI Kubernetes Container Runtime daemon |
| [kpod(1)](/docs/kpod.1.md) | Simple management tool for pods and images |
| [kpod-history(1)](/docs/kpod-history.1.md)] | Shows the history of an image |
| [kpod-images(1)](/docs/kpod-images.1.md) | List images in local storage |
| [kpod-pull(1)](/docs/kpod-pull.1.md) | Pull an image from a registry |
| [kpod-rmi(1)](/docs/kpod-rmi.1.md) | Removes one or more images |

353
cmd/kpod/history.go Normal file
View file

@ -0,0 +1,353 @@
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 = "<missing>"
}
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
}

View file

@ -28,6 +28,7 @@ func main() {
tagCommand,
versionCommand,
pullCommand,
historyCommand,
}
app.Flags = []cli.Flag{
cli.StringFlag{

View file

@ -93,6 +93,28 @@ _kpod_pull() {
_complete_ "$options_with_args" "$boolean_options"
}
_kpod_history() {
local options_with_args="
--format
"
local boolean_options="
--human -H
--no-trunc
--quiet -q
--json
"
_complete_ "$options_with_args" "$boolean_options"
case "$cur" in
-*)
COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur"))
;;
*)
__kpod_list_images
;;
esac
}
_kpod_kpod() {
local options_with_args="
"
@ -106,6 +128,7 @@ _kpod_kpod() {
tag
version
pull
history
"
case "$prev" in

69
docs/kpod-history.1.md Normal file
View file

@ -0,0 +1,69 @@
% kpod(8) # kpod-history - Simple tool to view the history of an image
% Urvashi Mohnani
% JULY 2017
# NAME
kpod-history - Shows the history of an image.
# SYNOPSIS
**kpod history [OPTIONS] IMAGE[:TAG|DIGEST]**
# DESCRIPTION
**kpod history** displays the history of an image by printing out information
about each layer used in the image. The information printed out for each layer
include Created (time and date), Created By, Size, and Comment. The output can
be truncated or not using the **--no-trunc** flag. If the **--human** flag is
set, the time of creation and size are printed out in a human readable format.
The **--quiet** flag displays the ID of the image only when set and the **--format**
flag is used to print the information using the Go template provided by the user.
Valid placeholders for the Go template are listed below:
| **Placeholder** | **Description** |
|-----------------|------------------------------------------------------------------------------|
| .ID | Image ID |
| .Created | if **--human**, time elapsed since creation, otherwise time stamp of creation|
| .CreatedBy | Command used to create the layer |
| .Size | Size of layer on disk |
| .Comment | Comment for the layer |
**kpod [GLOBAL OPTIONS]**
**kpod [GLOBAL OPTIONS] history [OPTIONS]**
# GLOBAL OPTIONS
**--help, -h**
Print usage statement
# OPTIONS
**--human, -H**
Display sizes and dates in human readable format
**--no-trunc**
Do not truncate the output
**--quiet, -q**
Print the numeric IDs only
**--format**
Pretty-print history of the image using a Go template
**--json**
Print the history in JSON form
# COMMANDS
**kpod history debian**
**kpod history --no-trunc=true --human=false debian**
**kpod history --format "{{.ID}} {{.Created}}" debian**
## history
Show the history of an image
# SEE ALSO
kpod(1), crio(8), crio.conf(5)
# HISTORY
July 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>

View file

@ -2,6 +2,7 @@
load helpers
IMAGE="alpine"
ROOT="$TESTDIR/crio"
RUNROOT="$TESTDIR/crio-run"
KPOD_OPTIONS="--root $ROOT --runroot $RUNROOT --storage-driver vfs"
@ -16,30 +17,40 @@ KPOD_OPTIONS="--root $ROOT --runroot $RUNROOT --storage-driver vfs"
run ${KPOD_BINARY} $KPOD_OPTIONS pull debian:6.0.10
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi debian:6.0.10
[ "$status" -eq 0 ]
}
@test "kpod pull from docker without tag" {
run ${KPOD_BINARY} $KPOD_OPTIONS pull debian
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi debian
[ "$status" -eq 0 ]
}
@test "kpod pull from a non-docker registry with tag" {
run ${KPOD_BINARY} $KPOD_OPTIONS pull registry.fedoraproject.org/fedora:rawhide
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi registry.fedoraproject.org/fedora:rawhide
[ "$status" -eq 0 ]
}
@test "kpod pull from a non-docker registry without tag" {
run ${KPOD_BINARY} $KPOD_OPTIONS pull registry.fedoraproject.org/fedora
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi registry.fedoraproject.org/fedora
[ "$status" -eq 0 ]
}
@test "kpod pull using digest" {
run ${KPOD_BINARY} $KPOD_OPTIONS pull debian@sha256:7d067f77d2ae5a23fe6920f8fbc2936c4b0d417e9d01b26372561860750815f0
run ${KPOD_BINARY} $KPOD_OPTIONS pull alpine@sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi alpine:latest
[ "$status" -eq 0 ]
}
@test "kpod pull from a non existent image" {
@ -47,3 +58,62 @@ KPOD_OPTIONS="--root $ROOT --runroot $RUNROOT --storage-driver vfs"
echo "$output"
[ "$status" -ne 0 ]
}
@test "kpod history default" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pull $IMAGE
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} history $IMAGE
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE
[ "$status" -eq 0 ]
}
@test "kpod history with format" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pull $IMAGE
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} history --format "{{.ID}} {{.Created}}" $IMAGE
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE
[ "$status" -eq 0 ]
}
@test "kpod history human flag" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pull $IMAGE
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} history --human=false $IMAGE
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE
[ "$status" -eq 0 ]
}
@test "kpod history quiet flag" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pull $IMAGE
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} history -q $IMAGE
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE
[ "$status" -eq 0 ]
}
@test "kpod history no-trunc flag" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pull $IMAGE
[ "$status" -eq 0 ]
run ${KPOD_BINARY} ${KPOD_OPTIONS} history --no-trunc $IMAGE
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE
[ "$status" -eq 0 ]
}
@test "kpod history json flag" {
run ${KPOD_BINARY} ${KPOD_OPTIONS} pull $IMAGE
run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} history --json $IMAGE | python -m json.tool"
echo "$output"
[ "$status" -eq 0 ]
run ${KPOD_BINARY} $KPOD_OPTIONS rmi $IMAGE
[ "$status" -eq 0 ]
}