github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/image_history.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"text/tabwriter"
    27  	"text/template"
    28  	"time"
    29  
    30  	"github.com/containerd/containerd"
    31  	"github.com/containerd/containerd/pkg/progress"
    32  	"github.com/containerd/log"
    33  	"github.com/containerd/nerdctl/pkg/clientutil"
    34  	"github.com/containerd/nerdctl/pkg/formatter"
    35  	"github.com/containerd/nerdctl/pkg/idutil/imagewalker"
    36  	"github.com/containerd/nerdctl/pkg/imgutil"
    37  	"github.com/opencontainers/image-spec/identity"
    38  	"github.com/spf13/cobra"
    39  )
    40  
    41  func newHistoryCommand() *cobra.Command {
    42  	var historyCommand = &cobra.Command{
    43  		Use:               "history [flags] IMAGE",
    44  		Short:             "Show the history of an image",
    45  		Args:              IsExactArgs(1),
    46  		RunE:              historyAction,
    47  		ValidArgsFunction: historyShellComplete,
    48  		SilenceUsage:      true,
    49  		SilenceErrors:     true,
    50  	}
    51  	addHistoryFlags(historyCommand)
    52  	return historyCommand
    53  }
    54  
    55  func addHistoryFlags(cmd *cobra.Command) {
    56  	cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'")
    57  	cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    58  		return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
    59  	})
    60  	cmd.Flags().BoolP("quiet", "q", false, "Only show numeric IDs")
    61  	cmd.Flags().Bool("no-trunc", false, "Don't truncate output")
    62  }
    63  
    64  type historyPrintable struct {
    65  	Snapshot     string
    66  	CreatedSince string
    67  	CreatedBy    string
    68  	Size         string
    69  	Comment      string
    70  }
    71  
    72  func historyAction(cmd *cobra.Command, args []string) error {
    73  	globalOptions, err := processRootCmdFlags(cmd)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	defer cancel()
    82  
    83  	walker := &imagewalker.ImageWalker{
    84  		Client: client,
    85  		OnFound: func(ctx context.Context, found imagewalker.Found) error {
    86  			if found.MatchCount > 1 {
    87  				return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
    88  			}
    89  			ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    90  			defer cancel()
    91  			img := containerd.NewImage(client, found.Image)
    92  			imageConfig, _, err := imgutil.ReadImageConfig(ctx, img)
    93  			if err != nil {
    94  				return fmt.Errorf("failed to ReadImageConfig: %w", err)
    95  			}
    96  			configHistories := imageConfig.History
    97  			layerCounter := 0
    98  			diffIDs, err := img.RootFS(ctx)
    99  			if err != nil {
   100  				return fmt.Errorf("failed to get diffIDS: %w", err)
   101  			}
   102  			var historys []historyPrintable
   103  			for _, h := range configHistories {
   104  				var size string
   105  				var snapshotName string
   106  				if !h.EmptyLayer {
   107  					if len(diffIDs) <= layerCounter {
   108  						return fmt.Errorf("too many non-empty layers in History section")
   109  					}
   110  					diffIDs := diffIDs[0 : layerCounter+1]
   111  					chainID := identity.ChainID(diffIDs).String()
   112  
   113  					s := client.SnapshotService(globalOptions.Snapshotter)
   114  					stat, err := s.Stat(ctx, chainID)
   115  					if err != nil {
   116  						return fmt.Errorf("failed to get stat: %w", err)
   117  					}
   118  					use, err := s.Usage(ctx, chainID)
   119  					if err != nil {
   120  						return fmt.Errorf("failed to get usage: %w", err)
   121  					}
   122  					size = progress.Bytes(use.Size).String()
   123  					snapshotName = stat.Name
   124  					layerCounter++
   125  				} else {
   126  					size = progress.Bytes(0).String()
   127  					snapshotName = "<missing>"
   128  				}
   129  				history := historyPrintable{
   130  					Snapshot:     snapshotName,
   131  					CreatedSince: formatter.TimeSinceInHuman(*h.Created),
   132  					CreatedBy:    h.CreatedBy,
   133  					Size:         size,
   134  					Comment:      h.Comment,
   135  				}
   136  				historys = append(historys, history)
   137  			}
   138  			err = printHistory(cmd, historys)
   139  			if err != nil {
   140  				return fmt.Errorf("failed printHistory: %w", err)
   141  			}
   142  			return nil
   143  		},
   144  	}
   145  
   146  	return walker.WalkAll(ctx, args, true)
   147  }
   148  
   149  type historyPrinter struct {
   150  	w              io.Writer
   151  	quiet, noTrunc bool
   152  	tmpl           *template.Template
   153  }
   154  
   155  func printHistory(cmd *cobra.Command, historys []historyPrintable) error {
   156  	quiet, err := cmd.Flags().GetBool("quiet")
   157  	if err != nil {
   158  		return err
   159  	}
   160  	noTrunc, err := cmd.Flags().GetBool("no-trunc")
   161  	if err != nil {
   162  		return err
   163  	}
   164  	var w io.Writer
   165  	w = os.Stdout
   166  
   167  	format, err := cmd.Flags().GetString("format")
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	var tmpl *template.Template
   173  	switch format {
   174  	case "", "table":
   175  		w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
   176  		if !quiet {
   177  			fmt.Fprintln(w, "SNAPSHOT\tCREATED\tCREATED BY\tSIZE\tCOMMENT")
   178  		}
   179  	case "raw":
   180  		return errors.New("unsupported format: \"raw\"")
   181  	default:
   182  		if quiet {
   183  			return errors.New("format and quiet must not be specified together")
   184  		}
   185  		var err error
   186  		tmpl, err = formatter.ParseTemplate(format)
   187  		if err != nil {
   188  			return err
   189  		}
   190  	}
   191  
   192  	printer := &historyPrinter{
   193  		w:       w,
   194  		quiet:   quiet,
   195  		noTrunc: noTrunc,
   196  		tmpl:    tmpl,
   197  	}
   198  
   199  	for index := len(historys) - 1; index >= 0; index-- {
   200  		if err := printer.printHistory(historys[index]); err != nil {
   201  			log.L.Warn(err)
   202  		}
   203  	}
   204  
   205  	if f, ok := w.(formatter.Flusher); ok {
   206  		return f.Flush()
   207  	}
   208  	return nil
   209  }
   210  
   211  func (x *historyPrinter) printHistory(p historyPrintable) error {
   212  	if !x.noTrunc {
   213  		if len(p.CreatedBy) > 45 {
   214  			p.CreatedBy = p.CreatedBy[0:44] + "…"
   215  		}
   216  	}
   217  	if x.tmpl != nil {
   218  		var b bytes.Buffer
   219  		if err := x.tmpl.Execute(&b, p); err != nil {
   220  			return err
   221  		}
   222  		if _, err := fmt.Fprintln(x.w, b.String()); err != nil {
   223  			return err
   224  		}
   225  	} else if x.quiet {
   226  		if _, err := fmt.Fprintln(x.w, p.Snapshot); err != nil {
   227  			return err
   228  		}
   229  	} else {
   230  		if _, err := fmt.Fprintf(x.w, "%s\t%s\t%s\t%s\t%s\n",
   231  			p.Snapshot,
   232  			p.CreatedSince,
   233  			p.CreatedBy,
   234  			p.Size,
   235  			p.Comment,
   236  		); err != nil {
   237  			return err
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  func historyShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   244  	// show image names
   245  	return shellCompleteImageNames(cmd)
   246  }