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 }