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 containers
18
19import (
20	"context"
21	"fmt"
22	"os"
23	"strings"
24	"text/tabwriter"
25
26	"github.com/containerd/containerd"
27	"github.com/containerd/containerd/cio"
28	"github.com/containerd/containerd/cmd/ctr/commands"
29	"github.com/containerd/containerd/cmd/ctr/commands/run"
30	"github.com/containerd/containerd/containers"
31	"github.com/containerd/containerd/errdefs"
32	"github.com/containerd/containerd/log"
33	"github.com/containerd/typeurl"
34	"github.com/pkg/errors"
35	"github.com/urfave/cli"
36)
37
38// Command is the cli command for managing containers
39var Command = cli.Command{
40	Name:    "containers",
41	Usage:   "manage containers",
42	Aliases: []string{"c", "container"},
43	Subcommands: []cli.Command{
44		createCommand,
45		deleteCommand,
46		infoCommand,
47		listCommand,
48		setLabelsCommand,
49		checkpointCommand,
50		restoreCommand,
51	},
52}
53
54var createCommand = cli.Command{
55	Name:      "create",
56	Usage:     "create container",
57	ArgsUsage: "[flags] Image|RootFS CONTAINER [COMMAND] [ARG...]",
58	Flags:     append(commands.SnapshotterFlags, commands.ContainerFlags...),
59	Action: func(context *cli.Context) error {
60		var (
61			id     string
62			ref    string
63			config = context.IsSet("config")
64		)
65
66		if config {
67			id = context.Args().First()
68			if context.NArg() > 1 {
69				return errors.Wrap(errdefs.ErrInvalidArgument, "with spec config file, only container id should be provided")
70			}
71		} else {
72			id = context.Args().Get(1)
73			ref = context.Args().First()
74			if ref == "" {
75				return errors.Wrap(errdefs.ErrInvalidArgument, "image ref must be provided")
76			}
77		}
78		if id == "" {
79			return errors.Wrap(errdefs.ErrInvalidArgument, "container id must be provided")
80		}
81		client, ctx, cancel, err := commands.NewClient(context)
82		if err != nil {
83			return err
84		}
85		defer cancel()
86		_, err = run.NewContainer(ctx, client, context)
87		if err != nil {
88			return err
89		}
90		return nil
91	},
92}
93
94var listCommand = cli.Command{
95	Name:      "list",
96	Aliases:   []string{"ls"},
97	Usage:     "list containers",
98	ArgsUsage: "[flags] [<filter>, ...]",
99	Flags: []cli.Flag{
100		cli.BoolFlag{
101			Name:  "quiet, q",
102			Usage: "print only the container id",
103		},
104	},
105	Action: func(context *cli.Context) error {
106		var (
107			filters = context.Args()
108			quiet   = context.Bool("quiet")
109		)
110		client, ctx, cancel, err := commands.NewClient(context)
111		if err != nil {
112			return err
113		}
114		defer cancel()
115		containers, err := client.Containers(ctx, filters...)
116		if err != nil {
117			return err
118		}
119		if quiet {
120			for _, c := range containers {
121				fmt.Printf("%s\n", c.ID())
122			}
123			return nil
124		}
125		w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
126		fmt.Fprintln(w, "CONTAINER\tIMAGE\tRUNTIME\t")
127		for _, c := range containers {
128			info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata)
129			if err != nil {
130				return err
131			}
132			imageName := info.Image
133			if imageName == "" {
134				imageName = "-"
135			}
136			if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t\n",
137				c.ID(),
138				imageName,
139				info.Runtime.Name,
140			); err != nil {
141				return err
142			}
143		}
144		return w.Flush()
145	},
146}
147
148var deleteCommand = cli.Command{
149	Name:      "delete",
150	Usage:     "delete one or more existing containers",
151	ArgsUsage: "[flags] CONTAINER [CONTAINER, ...]",
152	Aliases:   []string{"del", "rm"},
153	Flags: []cli.Flag{
154		cli.BoolFlag{
155			Name:  "keep-snapshot",
156			Usage: "do not clean up snapshot with container",
157		},
158	},
159	Action: func(context *cli.Context) error {
160		var exitErr error
161		client, ctx, cancel, err := commands.NewClient(context)
162		if err != nil {
163			return err
164		}
165		defer cancel()
166		deleteOpts := []containerd.DeleteOpts{}
167		if !context.Bool("keep-snapshot") {
168			deleteOpts = append(deleteOpts, containerd.WithSnapshotCleanup)
169		}
170
171		if context.NArg() == 0 {
172			return errors.Wrap(errdefs.ErrInvalidArgument, "must specify at least one container to delete")
173		}
174		for _, arg := range context.Args() {
175			if err := deleteContainer(ctx, client, arg, deleteOpts...); err != nil {
176				if exitErr == nil {
177					exitErr = err
178				}
179				log.G(ctx).WithError(err).Errorf("failed to delete container %q", arg)
180			}
181		}
182		return exitErr
183	},
184}
185
186func deleteContainer(ctx context.Context, client *containerd.Client, id string, opts ...containerd.DeleteOpts) error {
187	container, err := client.LoadContainer(ctx, id)
188	if err != nil {
189		return err
190	}
191	task, err := container.Task(ctx, cio.Load)
192	if err != nil {
193		return container.Delete(ctx, opts...)
194	}
195	status, err := task.Status(ctx)
196	if err != nil {
197		return err
198	}
199	if status.Status == containerd.Stopped || status.Status == containerd.Created {
200		if _, err := task.Delete(ctx); err != nil {
201			return err
202		}
203		return container.Delete(ctx, opts...)
204	}
205	return fmt.Errorf("cannot delete a non stopped container: %v", status)
206
207}
208
209var setLabelsCommand = cli.Command{
210	Name:        "label",
211	Usage:       "set and clear labels for a container",
212	ArgsUsage:   "[flags] CONTAINER [<key>=<value>, ...]",
213	Description: "set and clear labels for a container",
214	Flags:       []cli.Flag{},
215	Action: func(context *cli.Context) error {
216		containerID, labels := commands.ObjectWithLabelArgs(context)
217		if containerID == "" {
218			return errors.Wrap(errdefs.ErrInvalidArgument, "container id must be provided")
219		}
220		client, ctx, cancel, err := commands.NewClient(context)
221		if err != nil {
222			return err
223		}
224		defer cancel()
225
226		container, err := client.LoadContainer(ctx, containerID)
227		if err != nil {
228			return err
229		}
230
231		setlabels, err := container.SetLabels(ctx, labels)
232		if err != nil {
233			return err
234		}
235
236		var labelStrings []string
237		for k, v := range setlabels {
238			labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
239		}
240
241		fmt.Println(strings.Join(labelStrings, ","))
242
243		return nil
244	},
245}
246
247var infoCommand = cli.Command{
248	Name:      "info",
249	Usage:     "get info about a container",
250	ArgsUsage: "CONTAINER",
251	Action: func(context *cli.Context) error {
252		id := context.Args().First()
253		if id == "" {
254			return errors.Wrap(errdefs.ErrInvalidArgument, "container id must be provided")
255		}
256		client, ctx, cancel, err := commands.NewClient(context)
257		if err != nil {
258			return err
259		}
260		defer cancel()
261		container, err := client.LoadContainer(ctx, id)
262		if err != nil {
263			return err
264		}
265		info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
266		if err != nil {
267			return err
268		}
269
270		if info.Spec != nil && info.Spec.Value != nil {
271			v, err := typeurl.UnmarshalAny(info.Spec)
272			if err != nil {
273				return err
274			}
275			commands.PrintAsJSON(struct {
276				containers.Container
277				Spec interface{} `json:"Spec,omitempty"`
278			}{
279				Container: info,
280				Spec:      v,
281			})
282			return nil
283		}
284		commands.PrintAsJSON(info)
285		return nil
286	},
287}
288