Modify the JSON output of kpod history
The JSON output was being modified to type string, fixed that to be of the same type as the source This is better for further processing of the JSON output if needed Restructured kpod history a bit as well Signed-off-by: umohnani8 <umohnani@redhat.com>
This commit is contained in:
parent
f9387aca28
commit
216e35db18
1 changed files with 134 additions and 170 deletions
|
@ -1,20 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
is "github.com/containers/image/storage"
|
is "github.com/containers/image/storage"
|
||||||
|
"github.com/containers/image/types"
|
||||||
"github.com/containers/storage"
|
"github.com/containers/storage"
|
||||||
units "github.com/docker/go-units"
|
units "github.com/docker/go-units"
|
||||||
"github.com/kubernetes-incubator/cri-o/cmd/kpod/formats"
|
"github.com/kubernetes-incubator/cri-o/cmd/kpod/formats"
|
||||||
"github.com/kubernetes-incubator/cri-o/libpod/common"
|
"github.com/kubernetes-incubator/cri-o/libpod/common"
|
||||||
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,12 +22,24 @@ const (
|
||||||
idTruncLength = 13
|
idTruncLength = 13
|
||||||
)
|
)
|
||||||
|
|
||||||
// historyOutputParams stores info about each layer
|
// historyTemplateParams stores info about each layer
|
||||||
type historyOutputParams struct {
|
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"`
|
ID string `json:"id"`
|
||||||
Created string `json:"created"`
|
Created *time.Time `json:"created"`
|
||||||
CreatedBy string `json:"createdby"`
|
CreatedBy string `json:"createdBy"`
|
||||||
Size string `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,14 +47,14 @@ type historyOutputParams struct {
|
||||||
type historyOptions struct {
|
type historyOptions struct {
|
||||||
image string
|
image string
|
||||||
human bool
|
human bool
|
||||||
truncate bool
|
noTrunc bool
|
||||||
quiet bool
|
quiet bool
|
||||||
format string
|
format string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
historyFlags = []cli.Flag{
|
historyFlags = []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolTFlag{
|
||||||
Name: "human, H",
|
Name: "human, H",
|
||||||
Usage: "Display sizes and dates in human readable format",
|
Usage: "Display sizes and dates in human readable format",
|
||||||
},
|
},
|
||||||
|
@ -83,226 +94,179 @@ func historyCmd(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
human := true
|
format := genHistoryFormat(c.Bool("quiet"))
|
||||||
if c.IsSet("human") {
|
|
||||||
human = c.Bool("human")
|
|
||||||
}
|
|
||||||
|
|
||||||
truncate := true
|
|
||||||
if c.IsSet("no-trunc") {
|
|
||||||
truncate = !c.Bool("no-trunc")
|
|
||||||
}
|
|
||||||
quiet := false
|
|
||||||
if c.IsSet("quiet") {
|
|
||||||
quiet = c.Bool("quiet")
|
|
||||||
}
|
|
||||||
format := ""
|
|
||||||
if c.IsSet("format") {
|
if c.IsSet("format") {
|
||||||
format = c.String("format")
|
format = c.String("format")
|
||||||
}
|
}
|
||||||
|
|
||||||
args := c.Args()
|
args := c.Args()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
logrus.Errorf("an image name must be specified")
|
return errors.Errorf("an image name must be specified")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
logrus.Errorf("Kpod history takes at most 1 argument")
|
return errors.Errorf("Kpod history takes at most 1 argument")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
imgName := args[0]
|
imgName := args[0]
|
||||||
|
|
||||||
opts := historyOptions{
|
opts := historyOptions{
|
||||||
image: imgName,
|
image: imgName,
|
||||||
human: human,
|
human: c.BoolT("human"),
|
||||||
truncate: truncate,
|
noTrunc: c.Bool("no-trunc"),
|
||||||
quiet: quiet,
|
quiet: c.Bool("quiet"),
|
||||||
format: format,
|
format: format,
|
||||||
}
|
}
|
||||||
err = outputHistory(store, opts)
|
return generateHistoryOutput(store, opts)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func genHistoryFormat(quiet, truncate, human bool) (format string) {
|
func genHistoryFormat(quiet bool) (format string) {
|
||||||
if quiet {
|
if quiet {
|
||||||
return formats.IDString
|
return formats.IDString
|
||||||
}
|
}
|
||||||
|
return "table {{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\t"
|
||||||
|
}
|
||||||
|
|
||||||
if truncate {
|
// historyToGeneric makes an empty array of interfaces for output
|
||||||
format = "table {{ .ID | printf \"%-12.12s\" }} {{ .Created | printf \"%-16s\" }} {{ .CreatedBy | " +
|
func historyToGeneric(templParams []historyTemplateParams, JSONParams []historyJSONParams) (genericParams []interface{}) {
|
||||||
"printf \"%-45.45s\" }} {{ .Size | printf \"%-16s\" }} {{ .Comment | printf \"%s\" }}"
|
if len(templParams) > 0 {
|
||||||
} else {
|
for _, v := range templParams {
|
||||||
format = "table {{ .ID | printf \"%-64s\" }} {{ .Created | printf \"%-18s\" }} {{ .CreatedBy | " +
|
genericParams = append(genericParams, interface{}(v))
|
||||||
"printf \"%-60s\" }} {{ .Size | printf \"%-16s\" }} {{ .Comment | printf \"%s\"}}"
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, v := range JSONParams {
|
||||||
|
genericParams = append(genericParams, interface{}(v))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputTime displays the time stamp in "2017-06-20T20:24:10Z" format
|
// generate the header based on the template provided
|
||||||
func outputTime(tm *time.Time) string {
|
func (h *historyTemplateParams) headerMap() map[string]string {
|
||||||
return tm.Format(time.RFC3339)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputHumanTime displays the time elapsed since creation
|
// getHistory gets the history of an image and information about its layers
|
||||||
func outputHumanTime(tm *time.Time) string {
|
func getHistory(store storage.Store, image string) ([]v1.History, []types.BlobInfo, string, error) {
|
||||||
return units.HumanDuration(time.Since(*tm))
|
ref, err := is.Transport.ParseStoreReference(store, image)
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
createdTime string
|
|
||||||
outputSize string
|
|
||||||
)
|
|
||||||
|
|
||||||
ref, err := is.Transport.ParseStoreReference(store, opts.image)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Errorf("error parsing reference to image %q: %v", opts.image, err)
|
return nil, nil, "", errors.Wrapf(err, "error parsing reference to image %q", image)
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err = is.Transport.GetStoreImage(store, ref)
|
img, err := is.Transport.GetStoreImage(store, ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Errorf("no such image %q: %v", opts.image, err)
|
return nil, nil, "", errors.Wrapf(err, "no such image %q", image)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemContext := common.GetSystemContext("")
|
systemContext := common.GetSystemContext("")
|
||||||
|
|
||||||
src, err := ref.NewImage(systemContext)
|
src, err := ref.NewImage(systemContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Errorf("error instantiating image %q: %v", opts.image, err)
|
return nil, nil, "", errors.Wrapf(err, "error instantiating image %q", image)
|
||||||
}
|
}
|
||||||
|
|
||||||
oci, err := src.OCIConfig()
|
oci, err := src.OCIConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
history := oci.History
|
return oci.History, src.LayerInfos(), img.ID, nil
|
||||||
layers := src.LayerInfos()
|
}
|
||||||
count := 1
|
|
||||||
// iterating backwards to get newwest to oldest
|
// 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
|
||||||
|
)
|
||||||
for i := len(history) - 1; i >= 0; i-- {
|
for i := len(history) - 1; i >= 0; i-- {
|
||||||
if i == len(history)-1 {
|
if i != len(history)-1 {
|
||||||
imageID = img.ID
|
|
||||||
} else {
|
|
||||||
imageID = "<missing>"
|
imageID = "<missing>"
|
||||||
}
|
}
|
||||||
|
if !opts.noTrunc && i == len(history)-1 {
|
||||||
|
imageID = imageID[:idTruncLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int64
|
||||||
if !history[i].EmptyLayer {
|
if !history[i].EmptyLayer {
|
||||||
size = layers[len(layers)-count].Size
|
size = layers[len(layers)-count].Size
|
||||||
count++
|
count++
|
||||||
} else {
|
|
||||||
size = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.human {
|
if opts.human {
|
||||||
createdTime = outputHumanTime(history[i].Created) + " ago"
|
createdTime = units.HumanDuration(time.Since((*history[i].Created))) + " ago"
|
||||||
outputSize = units.HumanSize(float64(size))
|
outputSize = units.HumanSize(float64(size))
|
||||||
} else {
|
} else {
|
||||||
createdTime = outputTime(history[i].Created)
|
createdTime = (history[i].Created).Format(time.RFC3339)
|
||||||
outputSize = strconv.FormatInt(size, 10)
|
outputSize = strconv.FormatInt(size, 10)
|
||||||
}
|
}
|
||||||
params := historyOutputParams{
|
|
||||||
|
createdBy = strings.Join(strings.Fields(history[i].CreatedBy), " ")
|
||||||
|
if !opts.noTrunc && len(createdBy) > createdByTruncLength {
|
||||||
|
createdBy = createdBy[:createdByTruncLength-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
params := historyTemplateParams{
|
||||||
ID: imageID,
|
ID: imageID,
|
||||||
Created: createdTime,
|
Created: createdTime,
|
||||||
CreatedBy: history[i].CreatedBy,
|
CreatedBy: createdBy,
|
||||||
Size: outputSize,
|
Size: outputSize,
|
||||||
Comment: history[i].Comment,
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// historyToGeneric makes an empty array of interfaces for output
|
|
||||||
func historyToGeneric(params []historyOutputParams) []interface{} {
|
|
||||||
genericParams := make([]interface{}, len(params))
|
|
||||||
for i, v := range params {
|
|
||||||
genericParams[i] = interface{}(v)
|
|
||||||
}
|
|
||||||
return genericParams
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
historyOutput := []historyOutputParams{}
|
|
||||||
|
|
||||||
historyFormat := opts.format
|
|
||||||
if historyFormat == "" {
|
|
||||||
historyFormat = genHistoryFormat(opts.quiet, opts.truncate, opts.human)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(history); i++ {
|
|
||||||
imageID = history[i].ID
|
|
||||||
|
|
||||||
outputCreatedBy = strings.Join(strings.Fields(history[i].CreatedBy), " ")
|
|
||||||
if opts.truncate && len(outputCreatedBy) > createdByTruncLength {
|
|
||||||
outputCreatedBy = outputCreatedBy[:createdByTruncLength-3] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.truncate && i == 0 {
|
|
||||||
imageID = history[i].ID[:idTruncLength]
|
|
||||||
}
|
|
||||||
|
|
||||||
params := historyOutputParams{
|
|
||||||
ID: imageID,
|
|
||||||
Created: history[i].Created,
|
|
||||||
CreatedBy: outputCreatedBy,
|
|
||||||
Size: history[i].Size,
|
|
||||||
Comment: history[i].Comment,
|
|
||||||
}
|
|
||||||
historyOutput = append(historyOutput, params)
|
historyOutput = append(historyOutput, params)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
var out formats.Writer
|
|
||||||
switch opts.format {
|
|
||||||
case formats.JSONString:
|
|
||||||
out = formats.JSONStructArray{Output: historyToGeneric(historyOutput)}
|
|
||||||
default:
|
|
||||||
out = formats.StdoutTemplateArray{Output: historyToGeneric(historyOutput), Template: historyFormat, Fields: historyOutput[0].headerMap()}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
formats.Writer(out).Out()
|
|
||||||
|
// 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++
|
||||||
|
}
|
||||||
|
|
||||||
|
params := historyJSONParams{
|
||||||
|
ID: imageID,
|
||||||
|
Created: history[i].Created,
|
||||||
|
CreatedBy: history[i].CreatedBy,
|
||||||
|
Size: size,
|
||||||
|
Comment: history[i].Comment,
|
||||||
|
}
|
||||||
|
historyOutput = append(historyOutput, params)
|
||||||
|
}
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *historyOutputParams) headerMap() map[string]string {
|
var out formats.Writer
|
||||||
v := reflect.Indirect(reflect.ValueOf(h))
|
|
||||||
values := make(map[string]string)
|
switch opts.format {
|
||||||
for h := 0; h < v.NumField(); h++ {
|
case formats.JSONString:
|
||||||
key := v.Type().Field(h).Name
|
historyOutput := getHistoryJSONOutput(history, layers, imageID)
|
||||||
value := key
|
out = formats.JSONStructArray{Output: historyToGeneric([]historyTemplateParams{}, historyOutput)}
|
||||||
values[key] = fmt.Sprintf("%s ", strings.ToUpper(splitCamelCase(value)))
|
default:
|
||||||
|
historyOutput := getHistoryTemplateOutput(history, layers, imageID, opts)
|
||||||
|
out = formats.StdoutTemplateArray{Output: historyToGeneric(historyOutput, []historyJSONParams{}), Template: opts.format, Fields: historyOutput[0].headerMap()}
|
||||||
}
|
}
|
||||||
return values
|
|
||||||
|
return formats.Writer(out).Out()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue