github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podmanV2/images/list.go (about) 1 package images 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "sort" 8 "strings" 9 "text/tabwriter" 10 "text/template" 11 "time" 12 "unicode" 13 14 "github.com/containers/libpod/cmd/podmanV2/registry" 15 "github.com/containers/libpod/pkg/domain/entities" 16 "github.com/docker/go-units" 17 jsoniter "github.com/json-iterator/go" 18 "github.com/spf13/cobra" 19 "github.com/spf13/pflag" 20 ) 21 22 type listFlagType struct { 23 format string 24 history bool 25 noHeading bool 26 noTrunc bool 27 quiet bool 28 sort string 29 readOnly bool 30 digests bool 31 } 32 33 var ( 34 // Command: podman image _list_ 35 listCmd = &cobra.Command{ 36 Use: "list [flag] [IMAGE]", 37 Aliases: []string{"ls"}, 38 Args: cobra.MaximumNArgs(1), 39 Short: "List images in local storage", 40 Long: "Lists images previously pulled to the system or created on the system.", 41 RunE: images, 42 Example: `podman image list --format json 43 podman image list --sort repository --format "table {{.ID}} {{.Repository}} {{.Tag}}" 44 podman image list --filter dangling=true`, 45 } 46 47 // Options to pull data 48 listOptions = entities.ImageListOptions{} 49 50 // Options for presenting data 51 listFlag = listFlagType{} 52 53 sortFields = entities.NewStringSet( 54 "created", 55 "id", 56 "repository", 57 "size", 58 "tag") 59 ) 60 61 func init() { 62 registry.Commands = append(registry.Commands, registry.CliCommand{ 63 Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, 64 Command: listCmd, 65 Parent: imageCmd, 66 }) 67 imageListFlagSet(listCmd.Flags()) 68 } 69 70 func imageListFlagSet(flags *pflag.FlagSet) { 71 flags.BoolVarP(&listOptions.All, "all", "a", false, "Show all images (default hides intermediate images)") 72 flags.StringSliceVarP(&listOptions.Filter, "filter", "f", []string{}, "Filter output based on conditions provided (default [])") 73 flags.StringVar(&listFlag.format, "format", "", "Change the output format to JSON or a Go template") 74 flags.BoolVar(&listFlag.digests, "digests", false, "Show digests") 75 flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings") 76 flags.BoolVar(&listFlag.noTrunc, "no-trunc", false, "Do not truncate output") 77 flags.BoolVar(&listFlag.noTrunc, "notruncate", false, "Do not truncate output") 78 flags.BoolVarP(&listFlag.quiet, "quiet", "q", false, "Display only image IDs") 79 flags.StringVar(&listFlag.sort, "sort", "created", "Sort by "+sortFields.String()) 80 flags.BoolVarP(&listFlag.history, "history", "", false, "Display the image name history") 81 } 82 83 func images(cmd *cobra.Command, args []string) error { 84 if len(listOptions.Filter) > 0 && len(args) > 0 { 85 return errors.New("cannot specify an image and a filter(s)") 86 } 87 88 if len(listOptions.Filter) < 1 && len(args) > 0 { 89 listOptions.Filter = append(listOptions.Filter, "reference="+args[0]) 90 } 91 92 if cmd.Flag("sort").Changed && !sortFields.Contains(listFlag.sort) { 93 return fmt.Errorf("\"%s\" is not a valid field for sorting. Choose from: %s", 94 listFlag.sort, sortFields.String()) 95 } 96 97 summaries, err := registry.ImageEngine().List(registry.GetContext(), listOptions) 98 if err != nil { 99 return err 100 } 101 102 imageS := summaries 103 sort.Slice(imageS, sortFunc(listFlag.sort, imageS)) 104 105 if cmd.Flag("format").Changed && listFlag.format == "json" { 106 return writeJSON(imageS) 107 } else { 108 return writeTemplate(imageS, err) 109 } 110 } 111 112 func writeJSON(imageS []*entities.ImageSummary) error { 113 type image struct { 114 entities.ImageSummary 115 Created string 116 } 117 118 imgs := make([]image, 0, len(imageS)) 119 for _, e := range imageS { 120 var h image 121 h.ImageSummary = *e 122 h.Created = time.Unix(e.Created, 0).Format(time.RFC3339) 123 h.RepoTags = nil 124 125 imgs = append(imgs, h) 126 } 127 128 json := jsoniter.ConfigCompatibleWithStandardLibrary 129 enc := json.NewEncoder(os.Stdout) 130 return enc.Encode(imgs) 131 } 132 133 func writeTemplate(imageS []*entities.ImageSummary, err error) error { 134 var ( 135 hdr, row string 136 ) 137 imgs := make([]imageReporter, 0, len(imageS)) 138 for _, e := range imageS { 139 for _, tag := range e.RepoTags { 140 var h imageReporter 141 h.ImageSummary = *e 142 h.Repository, h.Tag = tokenRepoTag(tag) 143 imgs = append(imgs, h) 144 } 145 if e.IsReadOnly() { 146 listFlag.readOnly = true 147 } 148 } 149 if len(listFlag.format) < 1 { 150 hdr, row = imageListFormat(listFlag) 151 } else { 152 row = listFlag.format 153 if !strings.HasSuffix(row, "\n") { 154 row += "\n" 155 } 156 } 157 format := hdr + "{{range . }}" + row + "{{end}}" 158 tmpl := template.Must(template.New("list").Parse(format)) 159 w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) 160 defer w.Flush() 161 return tmpl.Execute(w, imgs) 162 } 163 164 func tokenRepoTag(tag string) (string, string) { 165 tokens := strings.SplitN(tag, ":", 2) 166 switch len(tokens) { 167 case 0: 168 return tag, "" 169 case 1: 170 return tokens[0], "" 171 case 2: 172 return tokens[0], tokens[1] 173 default: 174 return "<N/A>", "" 175 } 176 } 177 178 func sortFunc(key string, data []*entities.ImageSummary) func(i, j int) bool { 179 switch key { 180 case "id": 181 return func(i, j int) bool { 182 return data[i].ID < data[j].ID 183 } 184 case "repository": 185 return func(i, j int) bool { 186 return data[i].RepoTags[0] < data[j].RepoTags[0] 187 } 188 case "size": 189 return func(i, j int) bool { 190 return data[i].Size < data[j].Size 191 } 192 case "tag": 193 return func(i, j int) bool { 194 return data[i].RepoTags[0] < data[j].RepoTags[0] 195 } 196 default: 197 // case "created": 198 return func(i, j int) bool { 199 return data[i].Created >= data[j].Created 200 } 201 } 202 } 203 204 func imageListFormat(flags listFlagType) (string, string) { 205 if flags.quiet { 206 return "", "{{.ID}}\n" 207 } 208 209 // Defaults 210 hdr := "REPOSITORY\tTAG" 211 row := "{{.Repository}}\t{{if .Tag}}{{.Tag}}{{else}}<none>{{end}}" 212 213 if flags.digests { 214 hdr += "\tDIGEST" 215 row += "\t{{.Digest}}" 216 } 217 218 hdr += "\tIMAGE ID" 219 if flags.noTrunc { 220 row += "\tsha256:{{.ID}}" 221 } else { 222 row += "\t{{.ID}}" 223 } 224 225 hdr += "\tCREATED\tSIZE" 226 row += "\t{{.Created}}\t{{.Size}}" 227 228 if flags.history { 229 hdr += "\tHISTORY" 230 row += "\t{{if .History}}{{.History}}{{else}}<none>{{end}}" 231 } 232 233 if flags.readOnly { 234 hdr += "\tReadOnly" 235 row += "\t{{.ReadOnly}}" 236 } 237 238 if flags.noHeading { 239 hdr = "" 240 } else { 241 hdr += "\n" 242 } 243 244 row += "\n" 245 return hdr, row 246 } 247 248 type imageReporter struct { 249 Repository string `json:"repository,omitempty"` 250 Tag string `json:"tag,omitempty"` 251 entities.ImageSummary 252 } 253 254 func (i imageReporter) ID() string { 255 if !listFlag.noTrunc && len(i.ImageSummary.ID) >= 12 { 256 return i.ImageSummary.ID[0:12] 257 } 258 return i.ImageSummary.ID 259 } 260 261 func (i imageReporter) Created() string { 262 return units.HumanDuration(time.Since(time.Unix(i.ImageSummary.Created, 0))) + " ago" 263 } 264 265 func (i imageReporter) Size() string { 266 s := units.HumanSizeWithPrecision(float64(i.ImageSummary.Size), 3) 267 j := strings.LastIndexFunc(s, unicode.IsNumber) 268 return s[:j+1] + " " + s[j+1:] 269 } 270 271 func (i imageReporter) History() string { 272 return strings.Join(i.ImageSummary.History, ", ") 273 }