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 run
18
19import (
20	gocontext "context"
21	"encoding/csv"
22	"fmt"
23	"strings"
24
25	"github.com/containerd/console"
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/tasks"
30	"github.com/containerd/containerd/containers"
31	"github.com/containerd/containerd/oci"
32	specs "github.com/opencontainers/runtime-spec/specs-go"
33	"github.com/pkg/errors"
34	"github.com/sirupsen/logrus"
35	"github.com/urfave/cli"
36)
37
38func withMounts(context *cli.Context) oci.SpecOpts {
39	return func(ctx gocontext.Context, client oci.Client, container *containers.Container, s *specs.Spec) error {
40		mounts := make([]specs.Mount, 0)
41		for _, mount := range context.StringSlice("mount") {
42			m, err := parseMountFlag(mount)
43			if err != nil {
44				return err
45			}
46			mounts = append(mounts, m)
47		}
48		return oci.WithMounts(mounts)(ctx, client, container, s)
49	}
50}
51
52// parseMountFlag parses a mount string in the form "type=foo,source=/path,destination=/target,options=rbind:rw"
53func parseMountFlag(m string) (specs.Mount, error) {
54	mount := specs.Mount{}
55	r := csv.NewReader(strings.NewReader(m))
56
57	fields, err := r.Read()
58	if err != nil {
59		return mount, err
60	}
61
62	for _, field := range fields {
63		v := strings.Split(field, "=")
64		if len(v) != 2 {
65			return mount, fmt.Errorf("invalid mount specification: expected key=val")
66		}
67
68		key := v[0]
69		val := v[1]
70		switch key {
71		case "type":
72			mount.Type = val
73		case "source", "src":
74			mount.Source = val
75		case "destination", "dst":
76			mount.Destination = val
77		case "options":
78			mount.Options = strings.Split(val, ":")
79		default:
80			return mount, fmt.Errorf("mount option %q not supported", key)
81		}
82	}
83
84	return mount, nil
85}
86
87// Command runs a container
88var Command = cli.Command{
89	Name:           "run",
90	Usage:          "run a container",
91	ArgsUsage:      "[flags] Image|RootFS ID [COMMAND] [ARG...]",
92	SkipArgReorder: true,
93	Flags: append([]cli.Flag{
94		cli.BoolFlag{
95			Name:  "rm",
96			Usage: "remove the container after running",
97		},
98		cli.BoolFlag{
99			Name:  "null-io",
100			Usage: "send all IO to /dev/null",
101		},
102		cli.StringFlag{
103			Name:  "log-uri",
104			Usage: "log uri",
105		},
106		cli.BoolFlag{
107			Name:  "detach,d",
108			Usage: "detach from the task after it has started execution",
109		},
110		cli.StringFlag{
111			Name:  "fifo-dir",
112			Usage: "directory used for storing IO FIFOs",
113		},
114		cli.StringFlag{
115			Name:  "cgroup",
116			Usage: "cgroup path (To disable use of cgroup, set to \"\" explicitly)",
117		},
118		cli.StringFlag{
119			Name:  "platform",
120			Usage: "run image for specific platform",
121		},
122	}, append(platformRunFlags, append(commands.SnapshotterFlags, commands.ContainerFlags...)...)...),
123	Action: func(context *cli.Context) error {
124		var (
125			err error
126			id  string
127			ref string
128
129			tty    = context.Bool("tty")
130			detach = context.Bool("detach")
131			config = context.IsSet("config")
132		)
133
134		if config {
135			id = context.Args().First()
136			if context.NArg() > 1 {
137				return errors.New("with spec config file, only container id should be provided")
138			}
139		} else {
140			id = context.Args().Get(1)
141			ref = context.Args().First()
142
143			if ref == "" {
144				return errors.New("image ref must be provided")
145			}
146		}
147		if id == "" {
148			return errors.New("container id must be provided")
149		}
150		client, ctx, cancel, err := commands.NewClient(context)
151		if err != nil {
152			return err
153		}
154		defer cancel()
155		container, err := NewContainer(ctx, client, context)
156		if err != nil {
157			return err
158		}
159		if context.Bool("rm") && !detach {
160			defer container.Delete(ctx, containerd.WithSnapshotCleanup)
161		}
162		var con console.Console
163		if tty {
164			con = console.Current()
165			defer con.Reset()
166			if err := con.SetRaw(); err != nil {
167				return err
168			}
169		}
170		opts := getNewTaskOpts(context)
171		ioOpts := []cio.Opt{cio.WithFIFODir(context.String("fifo-dir"))}
172		task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), con, context.Bool("null-io"), context.String("log-uri"), ioOpts, opts...)
173		if err != nil {
174			return err
175		}
176		var statusC <-chan containerd.ExitStatus
177		if !detach {
178			defer task.Delete(ctx)
179			if statusC, err = task.Wait(ctx); err != nil {
180				return err
181			}
182		}
183		if context.IsSet("pid-file") {
184			if err := commands.WritePidFile(context.String("pid-file"), int(task.Pid())); err != nil {
185				return err
186			}
187		}
188		if err := task.Start(ctx); err != nil {
189			return err
190		}
191		if detach {
192			return nil
193		}
194		if tty {
195			if err := tasks.HandleConsoleResize(ctx, task, con); err != nil {
196				logrus.WithError(err).Error("console resize")
197			}
198		} else {
199			sigc := commands.ForwardAllSignals(ctx, task)
200			defer commands.StopCatch(sigc)
201		}
202		status := <-statusC
203		code, _, err := status.Result()
204		if err != nil {
205			return err
206		}
207		if _, err := task.Delete(ctx); err != nil {
208			return err
209		}
210		if code != 0 {
211			return cli.NewExitError("", int(code))
212		}
213		return nil
214	},
215}
216