github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podman/history.go (about)

     1  package main
     2  
     3  import (
     4  	"reflect"
     5  	"strconv"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/containers/buildah/pkg/formats"
    10  	"github.com/containers/libpod/cmd/podman/cliconfig"
    11  	"github.com/containers/libpod/libpod/image"
    12  	"github.com/containers/libpod/pkg/adapter"
    13  	"github.com/docker/go-units"
    14  	"github.com/pkg/errors"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  const createdByTruncLength = 45
    19  
    20  // historyTemplateParams stores info about each layer
    21  type historyTemplateParams struct {
    22  	ID        string
    23  	Created   string
    24  	CreatedBy string
    25  	Size      string
    26  	Comment   string
    27  }
    28  
    29  // historyOptions stores cli flag values
    30  type historyOptions struct {
    31  	human   bool
    32  	noTrunc bool
    33  	quiet   bool
    34  	format  string
    35  }
    36  
    37  var (
    38  	historyCommand cliconfig.HistoryValues
    39  
    40  	historyDescription = `Displays the history of an image.
    41  
    42    The information can be printed out in an easy to read, or user specified format, and can be truncated.`
    43  	_historyCommand = &cobra.Command{
    44  		Use:   "history [flags] IMAGE",
    45  		Short: "Show history of a specified image",
    46  		Long:  historyDescription,
    47  		RunE: func(cmd *cobra.Command, args []string) error {
    48  			historyCommand.InputArgs = args
    49  			historyCommand.GlobalFlags = MainGlobalOpts
    50  			historyCommand.Remote = remoteclient
    51  			return historyCmd(&historyCommand)
    52  		},
    53  	}
    54  )
    55  
    56  func init() {
    57  	historyCommand.Command = _historyCommand
    58  	historyCommand.SetHelpTemplate(HelpTemplate())
    59  	historyCommand.SetUsageTemplate(UsageTemplate())
    60  	flags := historyCommand.Flags()
    61  	flags.StringVar(&historyCommand.Format, "format", "", "Change the output to JSON or a Go template")
    62  	flags.BoolVarP(&historyCommand.Human, "human", "H", true, "Display sizes and dates in human readable format")
    63  	// notrucate needs to be added
    64  	flags.BoolVar(&historyCommand.NoTrunc, "no-trunc", false, "Do not truncate the output")
    65  	flags.BoolVarP(&historyCommand.Quiet, "quiet", "q", false, "Display the numeric IDs only")
    66  
    67  }
    68  
    69  func historyCmd(c *cliconfig.HistoryValues) error {
    70  	runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand)
    71  	if err != nil {
    72  		return errors.Wrapf(err, "could not get runtime")
    73  	}
    74  	defer runtime.DeferredShutdown(false)
    75  
    76  	format := genHistoryFormat(c.Format, c.Quiet)
    77  
    78  	args := c.InputArgs
    79  	if len(args) == 0 {
    80  		return errors.Errorf("an image name must be specified")
    81  	}
    82  	if len(args) > 1 {
    83  		return errors.Errorf("podman history takes at most 1 argument")
    84  	}
    85  
    86  	image, err := runtime.NewImageFromLocal(args[0])
    87  	if err != nil {
    88  		return err
    89  	}
    90  	opts := historyOptions{
    91  		human:   c.Human,
    92  		noTrunc: c.NoTrunc,
    93  		quiet:   c.Quiet,
    94  		format:  format,
    95  	}
    96  
    97  	history, err := image.History(getContext())
    98  	if err != nil {
    99  		return errors.Wrapf(err, "error getting history of image %q", image.InputName)
   100  	}
   101  
   102  	return generateHistoryOutput(history, opts)
   103  }
   104  
   105  func genHistoryFormat(format string, quiet bool) string {
   106  	if format != "" {
   107  		// "\t" from the command line is not being recognized as a tab
   108  		// replacing the string "\t" to a tab character if the user passes in "\t"
   109  		return strings.Replace(format, `\t`, "\t", -1)
   110  	}
   111  	if quiet {
   112  		return formats.IDString
   113  	}
   114  	return "table {{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\t"
   115  }
   116  
   117  // historyToGeneric makes an empty array of interfaces for output
   118  func historyToGeneric(templParams []historyTemplateParams, jsonParams []*image.History) (genericParams []interface{}) {
   119  	if len(templParams) > 0 {
   120  		for _, v := range templParams {
   121  			genericParams = append(genericParams, interface{}(v))
   122  		}
   123  		return
   124  	}
   125  	for _, v := range jsonParams {
   126  		genericParams = append(genericParams, interface{}(v))
   127  	}
   128  	return
   129  }
   130  
   131  // generate the header based on the template provided
   132  func (h *historyTemplateParams) headerMap() map[string]string {
   133  	v := reflect.Indirect(reflect.ValueOf(h))
   134  	values := make(map[string]string)
   135  	for h := 0; h < v.NumField(); h++ {
   136  		key := v.Type().Field(h).Name
   137  		value := key
   138  		values[key] = strings.ToUpper(splitCamelCase(value))
   139  	}
   140  	return values
   141  }
   142  
   143  // getHistorytemplateOutput gets the modified history information to be printed in human readable format
   144  func getHistoryTemplateOutput(history []*image.History, opts historyOptions) []historyTemplateParams {
   145  	var (
   146  		outputSize    string
   147  		createdTime   string
   148  		createdBy     string
   149  		historyOutput []historyTemplateParams
   150  	)
   151  	for _, hist := range history {
   152  		imageID := hist.ID
   153  		if !opts.noTrunc && imageID != "<missing>" {
   154  			imageID = shortID(imageID)
   155  		}
   156  
   157  		if opts.human {
   158  			createdTime = units.HumanDuration(time.Since(*hist.Created)) + " ago"
   159  			outputSize = units.HumanSize(float64(hist.Size))
   160  		} else {
   161  			createdTime = (hist.Created).Format(time.RFC3339)
   162  			outputSize = strconv.FormatInt(hist.Size, 10)
   163  		}
   164  
   165  		createdBy = strings.Join(strings.Fields(hist.CreatedBy), " ")
   166  		if !opts.noTrunc && len(createdBy) > createdByTruncLength {
   167  			createdBy = createdBy[:createdByTruncLength-3] + "..."
   168  		}
   169  
   170  		params := historyTemplateParams{
   171  			ID:        imageID,
   172  			Created:   createdTime,
   173  			CreatedBy: createdBy,
   174  			Size:      outputSize,
   175  			Comment:   hist.Comment,
   176  		}
   177  		historyOutput = append(historyOutput, params)
   178  	}
   179  	return historyOutput
   180  }
   181  
   182  // generateHistoryOutput generates the history based on the format given
   183  func generateHistoryOutput(history []*image.History, opts historyOptions) error {
   184  	if len(history) == 0 {
   185  		return nil
   186  	}
   187  
   188  	var out formats.Writer
   189  
   190  	switch opts.format {
   191  	case formats.JSONString:
   192  		out = formats.JSONStructArray{Output: historyToGeneric([]historyTemplateParams{}, history)}
   193  	default:
   194  		historyOutput := getHistoryTemplateOutput(history, opts)
   195  		out = formats.StdoutTemplateArray{Output: historyToGeneric(historyOutput, []*image.History{}), Template: opts.format, Fields: historyOutput[0].headerMap()}
   196  	}
   197  
   198  	return out.Out()
   199  }