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
17package images
18
19import (
20	"fmt"
21	"os"
22	"sort"
23	"strings"
24	"text/tabwriter"
25
26	"github.com/containerd/containerd/cmd/ctr/commands"
27	"github.com/containerd/containerd/errdefs"
28	"github.com/containerd/containerd/images"
29	"github.com/containerd/containerd/log"
30	"github.com/containerd/containerd/pkg/progress"
31	"github.com/containerd/containerd/platforms"
32	"github.com/pkg/errors"
33	"github.com/urfave/cli"
34)
35
36// Command is the cli command for managing images
37var Command = cli.Command{
38	Name:    "images",
39	Aliases: []string{"image", "i"},
40	Usage:   "manage images",
41	Subcommands: cli.Commands{
42		checkCommand,
43		exportCommand,
44		importCommand,
45		listCommand,
46		mountCommand,
47		unmountCommand,
48		pullCommand,
49		pushCommand,
50		removeCommand,
51		tagCommand,
52		setLabelsCommand,
53	},
54}
55
56var listCommand = cli.Command{
57	Name:        "list",
58	Aliases:     []string{"ls"},
59	Usage:       "list images known to containerd",
60	ArgsUsage:   "[flags] [<filter>, ...]",
61	Description: "list images registered with containerd",
62	Flags: []cli.Flag{
63		cli.BoolFlag{
64			Name:  "quiet, q",
65			Usage: "print only the image refs",
66		},
67	},
68	Action: func(context *cli.Context) error {
69		var (
70			filters = context.Args()
71			quiet   = context.Bool("quiet")
72		)
73		client, ctx, cancel, err := commands.NewClient(context)
74		if err != nil {
75			return err
76		}
77		defer cancel()
78		var (
79			imageStore = client.ImageService()
80			cs         = client.ContentStore()
81		)
82		imageList, err := imageStore.List(ctx, filters...)
83		if err != nil {
84			return errors.Wrap(err, "failed to list images")
85		}
86		if quiet {
87			for _, image := range imageList {
88				fmt.Println(image.Name)
89			}
90			return nil
91		}
92		tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
93		fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSIZE\tPLATFORMS\tLABELS\t")
94		for _, image := range imageList {
95			size, err := image.Size(ctx, cs, platforms.Default())
96			if err != nil {
97				log.G(ctx).WithError(err).Errorf("failed calculating size for image %s", image.Name)
98			}
99
100			platformColumn := "-"
101			specs, err := images.Platforms(ctx, cs, image.Target)
102			if err != nil {
103				log.G(ctx).WithError(err).Errorf("failed resolving platform for image %s", image.Name)
104			} else if len(specs) > 0 {
105				psm := map[string]struct{}{}
106				for _, p := range specs {
107					psm[platforms.Format(p)] = struct{}{}
108				}
109				var ps []string
110				for p := range psm {
111					ps = append(ps, p)
112				}
113				sort.Stable(sort.StringSlice(ps))
114				platformColumn = strings.Join(ps, ",")
115			}
116
117			labels := "-"
118			if len(image.Labels) > 0 {
119				var pairs []string
120				for k, v := range image.Labels {
121					pairs = append(pairs, fmt.Sprintf("%v=%v", k, v))
122				}
123				sort.Strings(pairs)
124				labels = strings.Join(pairs, ",")
125			}
126
127			fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%s\t\n",
128				image.Name,
129				image.Target.MediaType,
130				image.Target.Digest,
131				progress.Bytes(size),
132				platformColumn,
133				labels)
134		}
135
136		return tw.Flush()
137	},
138}
139
140var setLabelsCommand = cli.Command{
141	Name:        "label",
142	Usage:       "set and clear labels for an image",
143	ArgsUsage:   "[flags] <name> [<key>=<value>, ...]",
144	Description: "set and clear labels for an image",
145	Flags: []cli.Flag{
146		cli.BoolFlag{
147			Name:  "replace-all, r",
148			Usage: "replace all labels",
149		},
150	},
151	Action: func(context *cli.Context) error {
152		var (
153			replaceAll   = context.Bool("replace-all")
154			name, labels = commands.ObjectWithLabelArgs(context)
155		)
156		client, ctx, cancel, err := commands.NewClient(context)
157		if err != nil {
158			return err
159		}
160		defer cancel()
161		if name == "" {
162			return errors.New("please specify an image")
163		}
164
165		var (
166			is         = client.ImageService()
167			fieldpaths []string
168		)
169
170		for k := range labels {
171			if replaceAll {
172				fieldpaths = append(fieldpaths, "labels")
173			} else {
174				fieldpaths = append(fieldpaths, strings.Join([]string{"labels", k}, "."))
175			}
176		}
177
178		image := images.Image{
179			Name:   name,
180			Labels: labels,
181		}
182
183		updated, err := is.Update(ctx, image, fieldpaths...)
184		if err != nil {
185			return err
186		}
187
188		var labelStrings []string
189		for k, v := range updated.Labels {
190			labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
191		}
192
193		fmt.Println(strings.Join(labelStrings, ","))
194
195		return nil
196	},
197}
198
199var checkCommand = cli.Command{
200	Name:        "check",
201	Usage:       "check that an image has all content available locally",
202	ArgsUsage:   "[flags] [<filter>, ...]",
203	Description: "check that an image has all content available locally",
204	Flags:       commands.SnapshotterFlags,
205	Action: func(context *cli.Context) error {
206		var (
207			exitErr error
208		)
209		client, ctx, cancel, err := commands.NewClient(context)
210		if err != nil {
211			return err
212		}
213		defer cancel()
214		var (
215			contentStore = client.ContentStore()
216			tw           = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
217		)
218		fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSTATUS\tSIZE\tUNPACKED\t")
219
220		args := []string(context.Args())
221		imageList, err := client.ListImages(ctx, args...)
222		if err != nil {
223			return errors.Wrap(err, "failed listing images")
224		}
225
226		for _, image := range imageList {
227			var (
228				status       string = "complete"
229				size         string
230				requiredSize int64
231				presentSize  int64
232			)
233
234			available, required, present, missing, err := images.Check(ctx, contentStore, image.Target(), platforms.Default())
235			if err != nil {
236				if exitErr == nil {
237					exitErr = errors.Wrapf(err, "unable to check %v", image.Name())
238				}
239				log.G(ctx).WithError(err).Errorf("unable to check %v", image.Name())
240				status = "error"
241			}
242
243			if status != "error" {
244				for _, d := range required {
245					requiredSize += d.Size
246				}
247
248				for _, d := range present {
249					presentSize += d.Size
250				}
251
252				if len(missing) > 0 {
253					status = "incomplete"
254				}
255
256				if available {
257					status += fmt.Sprintf(" (%v/%v)", len(present), len(required))
258					size = fmt.Sprintf("%v/%v", progress.Bytes(presentSize), progress.Bytes(requiredSize))
259				} else {
260					status = fmt.Sprintf("unavailable (%v/?)", len(present))
261					size = fmt.Sprintf("%v/?", progress.Bytes(presentSize))
262				}
263			} else {
264				size = "-"
265			}
266
267			unpacked, err := image.IsUnpacked(ctx, context.String("snapshotter"))
268			if err != nil {
269				if exitErr == nil {
270					exitErr = errors.Wrapf(err, "unable to check unpack for %v", image.Name())
271				}
272				log.G(ctx).WithError(err).Errorf("unable to check unpack for %v", image.Name())
273			}
274
275			fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%t\n",
276				image.Name(),
277				image.Target().MediaType,
278				image.Target().Digest,
279				status,
280				size,
281				unpacked)
282		}
283		tw.Flush()
284
285		return exitErr
286	},
287}
288
289var removeCommand = cli.Command{
290	Name:        "remove",
291	Aliases:     []string{"rm"},
292	Usage:       "remove one or more images by reference",
293	ArgsUsage:   "[flags] <ref> [<ref>, ...]",
294	Description: "remove one or more images by reference",
295	Flags: []cli.Flag{
296		cli.BoolFlag{
297			Name:  "sync",
298			Usage: "Synchronously remove image and all associated resources",
299		},
300	},
301	Action: func(context *cli.Context) error {
302		client, ctx, cancel, err := commands.NewClient(context)
303		if err != nil {
304			return err
305		}
306		defer cancel()
307		var (
308			exitErr    error
309			imageStore = client.ImageService()
310		)
311		for i, target := range context.Args() {
312			var opts []images.DeleteOpt
313			if context.Bool("sync") && i == context.NArg()-1 {
314				opts = append(opts, images.SynchronousDelete())
315			}
316			if err := imageStore.Delete(ctx, target, opts...); err != nil {
317				if !errdefs.IsNotFound(err) {
318					if exitErr == nil {
319						exitErr = errors.Wrapf(err, "unable to delete %v", target)
320					}
321					log.G(ctx).WithError(err).Errorf("unable to delete %v", target)
322					continue
323				}
324				// image ref not found in metadata store; log not found condition
325				log.G(ctx).Warnf("%v: image not found", target)
326			} else {
327				fmt.Println(target)
328			}
329		}
330
331		return exitErr
332	},
333}
334