1// +build linux
2
3// Package specconv implements conversion of specifications to libcontainer
4// configurations
5package specconv
6
7import (
8	"fmt"
9	"os"
10	"path/filepath"
11	"strconv"
12	"strings"
13	"syscall"
14	"time"
15
16	"github.com/opencontainers/runc/libcontainer/cgroups"
17	"github.com/opencontainers/runc/libcontainer/configs"
18	"github.com/opencontainers/runc/libcontainer/seccomp"
19	libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils"
20	"github.com/opencontainers/runtime-spec/specs-go"
21)
22
23const wildcard = -1
24
25var namespaceMapping = map[specs.NamespaceType]configs.NamespaceType{
26	specs.PIDNamespace:     configs.NEWPID,
27	specs.NetworkNamespace: configs.NEWNET,
28	specs.MountNamespace:   configs.NEWNS,
29	specs.UserNamespace:    configs.NEWUSER,
30	specs.IPCNamespace:     configs.NEWIPC,
31	specs.UTSNamespace:     configs.NEWUTS,
32}
33
34var mountPropagationMapping = map[string]int{
35	"rprivate": syscall.MS_PRIVATE | syscall.MS_REC,
36	"private":  syscall.MS_PRIVATE,
37	"rslave":   syscall.MS_SLAVE | syscall.MS_REC,
38	"slave":    syscall.MS_SLAVE,
39	"rshared":  syscall.MS_SHARED | syscall.MS_REC,
40	"shared":   syscall.MS_SHARED,
41	"":         syscall.MS_PRIVATE | syscall.MS_REC,
42}
43
44var allowedDevices = []*configs.Device{
45	// allow mknod for any device
46	{
47		Type:        'c',
48		Major:       wildcard,
49		Minor:       wildcard,
50		Permissions: "m",
51		Allow:       true,
52	},
53	{
54		Type:        'b',
55		Major:       wildcard,
56		Minor:       wildcard,
57		Permissions: "m",
58		Allow:       true,
59	},
60	{
61		Type:        'c',
62		Path:        "/dev/null",
63		Major:       1,
64		Minor:       3,
65		Permissions: "rwm",
66		Allow:       true,
67	},
68	{
69		Type:        'c',
70		Path:        "/dev/random",
71		Major:       1,
72		Minor:       8,
73		Permissions: "rwm",
74		Allow:       true,
75	},
76	{
77		Type:        'c',
78		Path:        "/dev/full",
79		Major:       1,
80		Minor:       7,
81		Permissions: "rwm",
82		Allow:       true,
83	},
84	{
85		Type:        'c',
86		Path:        "/dev/tty",
87		Major:       5,
88		Minor:       0,
89		Permissions: "rwm",
90		Allow:       true,
91	},
92	{
93		Type:        'c',
94		Path:        "/dev/zero",
95		Major:       1,
96		Minor:       5,
97		Permissions: "rwm",
98		Allow:       true,
99	},
100	{
101		Type:        'c',
102		Path:        "/dev/urandom",
103		Major:       1,
104		Minor:       9,
105		Permissions: "rwm",
106		Allow:       true,
107	},
108	{
109		Path:        "/dev/console",
110		Type:        'c',
111		Major:       5,
112		Minor:       1,
113		Permissions: "rwm",
114		Allow:       true,
115	},
116	// /dev/pts/ - pts namespaces are "coming soon"
117	{
118		Path:        "",
119		Type:        'c',
120		Major:       136,
121		Minor:       wildcard,
122		Permissions: "rwm",
123		Allow:       true,
124	},
125	{
126		Path:        "",
127		Type:        'c',
128		Major:       5,
129		Minor:       2,
130		Permissions: "rwm",
131		Allow:       true,
132	},
133	// tuntap
134	{
135		Path:        "",
136		Type:        'c',
137		Major:       10,
138		Minor:       200,
139		Permissions: "rwm",
140		Allow:       true,
141	},
142}
143
144type CreateOpts struct {
145	CgroupName       string
146	UseSystemdCgroup bool
147	NoPivotRoot      bool
148	Spec             *specs.Spec
149}
150
151// CreateLibcontainerConfig creates a new libcontainer configuration from a
152// given specification and a cgroup name
153func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
154	// runc's cwd will always be the bundle path
155	rcwd, err := os.Getwd()
156	if err != nil {
157		return nil, err
158	}
159	cwd, err := filepath.Abs(rcwd)
160	if err != nil {
161		return nil, err
162	}
163	spec := opts.Spec
164	rootfsPath := spec.Root.Path
165	if !filepath.IsAbs(rootfsPath) {
166		rootfsPath = filepath.Join(cwd, rootfsPath)
167	}
168	config := &configs.Config{
169		Rootfs:      rootfsPath,
170		NoPivotRoot: opts.NoPivotRoot,
171		Readonlyfs:  spec.Root.Readonly,
172		Hostname:    spec.Hostname,
173		Labels: []string{
174			"bundle=" + cwd,
175		},
176	}
177
178	exists := false
179	if config.RootPropagation, exists = mountPropagationMapping[spec.Linux.RootfsPropagation]; !exists {
180		return nil, fmt.Errorf("rootfsPropagation=%v is not supported", spec.Linux.RootfsPropagation)
181	}
182
183	for _, ns := range spec.Linux.Namespaces {
184		t, exists := namespaceMapping[ns.Type]
185		if !exists {
186			return nil, fmt.Errorf("namespace %q does not exist", ns)
187		}
188		config.Namespaces.Add(t, ns.Path)
189	}
190	if config.Namespaces.Contains(configs.NEWNET) {
191		config.Networks = []*configs.Network{
192			{
193				Type: "loopback",
194			},
195		}
196	}
197	for _, m := range spec.Mounts {
198		config.Mounts = append(config.Mounts, createLibcontainerMount(cwd, m))
199	}
200	if err := createDevices(spec, config); err != nil {
201		return nil, err
202	}
203	if err := setupUserNamespace(spec, config); err != nil {
204		return nil, err
205	}
206	c, err := createCgroupConfig(opts.CgroupName, opts.UseSystemdCgroup, spec)
207	if err != nil {
208		return nil, err
209	}
210	config.Cgroups = c
211	// set extra path masking for libcontainer for the various unsafe places in proc
212	config.MaskPaths = spec.Linux.MaskedPaths
213	config.ReadonlyPaths = spec.Linux.ReadonlyPaths
214	if spec.Linux.Seccomp != nil {
215		seccomp, err := setupSeccomp(spec.Linux.Seccomp)
216		if err != nil {
217			return nil, err
218		}
219		config.Seccomp = seccomp
220	}
221	config.Sysctl = spec.Linux.Sysctl
222	if oomScoreAdj := spec.Linux.Resources.OOMScoreAdj; oomScoreAdj != nil {
223		config.OomScoreAdj = *oomScoreAdj
224	}
225	for _, g := range spec.Process.User.AdditionalGids {
226		config.AdditionalGroups = append(config.AdditionalGroups, strconv.FormatUint(uint64(g), 10))
227	}
228	createHooks(spec, config)
229	config.MountLabel = spec.Linux.MountLabel
230	config.Version = specs.Version
231	return config, nil
232}
233
234func createLibcontainerMount(cwd string, m specs.Mount) *configs.Mount {
235	flags, pgflags, data := parseMountOptions(m.Options)
236	source := m.Source
237	if m.Type == "bind" {
238		if !filepath.IsAbs(source) {
239			source = filepath.Join(cwd, m.Source)
240		}
241	}
242	return &configs.Mount{
243		Device:           m.Type,
244		Source:           source,
245		Destination:      m.Destination,
246		Data:             data,
247		Flags:            flags,
248		PropagationFlags: pgflags,
249	}
250}
251
252func createCgroupConfig(name string, useSystemdCgroup bool, spec *specs.Spec) (*configs.Cgroup, error) {
253	var (
254		err          error
255		myCgroupPath string
256	)
257
258	c := &configs.Cgroup{
259		Resources: &configs.Resources{},
260	}
261
262	if spec.Linux.CgroupsPath != nil {
263		myCgroupPath = libcontainerUtils.CleanPath(*spec.Linux.CgroupsPath)
264		if useSystemdCgroup {
265			myCgroupPath = *spec.Linux.CgroupsPath
266		}
267	}
268
269	if useSystemdCgroup {
270		if myCgroupPath == "" {
271			c.Parent = "system.slice"
272			c.ScopePrefix = "runc"
273			c.Name = name
274		} else {
275			// Parse the path from expected "slice:prefix:name"
276			// for e.g. "system.slice:docker:1234"
277			parts := strings.Split(myCgroupPath, ":")
278			if len(parts) != 3 {
279				return nil, fmt.Errorf("expected cgroupsPath to be of format \"slice:prefix:name\" for systemd cgroups")
280			}
281			c.Parent = parts[0]
282			c.ScopePrefix = parts[1]
283			c.Name = parts[2]
284		}
285	} else {
286		if myCgroupPath == "" {
287			myCgroupPath, err = cgroups.GetThisCgroupDir("devices")
288			if err != nil {
289				return nil, err
290			}
291			myCgroupPath = filepath.Join(myCgroupPath, name)
292		}
293		c.Path = myCgroupPath
294	}
295
296	c.Resources.AllowedDevices = allowedDevices
297	r := spec.Linux.Resources
298	if r == nil {
299		return c, nil
300	}
301	for i, d := range spec.Linux.Resources.Devices {
302		var (
303			t     = "a"
304			major = int64(-1)
305			minor = int64(-1)
306		)
307		if d.Type != nil {
308			t = *d.Type
309		}
310		if d.Major != nil {
311			major = *d.Major
312		}
313		if d.Minor != nil {
314			minor = *d.Minor
315		}
316		if d.Access == nil || *d.Access == "" {
317			return nil, fmt.Errorf("device access at %d field cannot be empty", i)
318		}
319		dt, err := stringToDeviceRune(t)
320		if err != nil {
321			return nil, err
322		}
323		dd := &configs.Device{
324			Type:        dt,
325			Major:       major,
326			Minor:       minor,
327			Permissions: *d.Access,
328			Allow:       d.Allow,
329		}
330		c.Resources.Devices = append(c.Resources.Devices, dd)
331	}
332	// append the default allowed devices to the end of the list
333	c.Resources.Devices = append(c.Resources.Devices, allowedDevices...)
334	if r.Memory != nil {
335		if r.Memory.Limit != nil {
336			c.Resources.Memory = int64(*r.Memory.Limit)
337		}
338		if r.Memory.Reservation != nil {
339			c.Resources.MemoryReservation = int64(*r.Memory.Reservation)
340		}
341		if r.Memory.Swap != nil {
342			c.Resources.MemorySwap = int64(*r.Memory.Swap)
343		}
344		if r.Memory.Kernel != nil {
345			c.Resources.KernelMemory = int64(*r.Memory.Kernel)
346		}
347		if r.Memory.KernelTCP != nil {
348			c.Resources.KernelMemoryTCP = int64(*r.Memory.KernelTCP)
349		}
350		if r.Memory.Swappiness != nil {
351			swappiness := int64(*r.Memory.Swappiness)
352			c.Resources.MemorySwappiness = &swappiness
353		}
354	}
355	if r.CPU != nil {
356		if r.CPU.Shares != nil {
357			c.Resources.CpuShares = int64(*r.CPU.Shares)
358		}
359		if r.CPU.Quota != nil {
360			c.Resources.CpuQuota = int64(*r.CPU.Quota)
361		}
362		if r.CPU.Period != nil {
363			c.Resources.CpuPeriod = int64(*r.CPU.Period)
364		}
365		if r.CPU.RealtimeRuntime != nil {
366			c.Resources.CpuRtRuntime = int64(*r.CPU.RealtimeRuntime)
367		}
368		if r.CPU.RealtimePeriod != nil {
369			c.Resources.CpuRtPeriod = int64(*r.CPU.RealtimePeriod)
370		}
371		if r.CPU.Cpus != nil {
372			c.Resources.CpusetCpus = *r.CPU.Cpus
373		}
374		if r.CPU.Mems != nil {
375			c.Resources.CpusetMems = *r.CPU.Mems
376		}
377	}
378	if r.Pids != nil {
379		c.Resources.PidsLimit = *r.Pids.Limit
380	}
381	if r.BlockIO != nil {
382		if r.BlockIO.Weight != nil {
383			c.Resources.BlkioWeight = *r.BlockIO.Weight
384		}
385		if r.BlockIO.LeafWeight != nil {
386			c.Resources.BlkioLeafWeight = *r.BlockIO.LeafWeight
387		}
388		if r.BlockIO.WeightDevice != nil {
389			for _, wd := range r.BlockIO.WeightDevice {
390				weightDevice := configs.NewWeightDevice(wd.Major, wd.Minor, *wd.Weight, *wd.LeafWeight)
391				c.Resources.BlkioWeightDevice = append(c.Resources.BlkioWeightDevice, weightDevice)
392			}
393		}
394		if r.BlockIO.ThrottleReadBpsDevice != nil {
395			for _, td := range r.BlockIO.ThrottleReadBpsDevice {
396				throttleDevice := configs.NewThrottleDevice(td.Major, td.Minor, *td.Rate)
397				c.Resources.BlkioThrottleReadBpsDevice = append(c.Resources.BlkioThrottleReadBpsDevice, throttleDevice)
398			}
399		}
400		if r.BlockIO.ThrottleWriteBpsDevice != nil {
401			for _, td := range r.BlockIO.ThrottleWriteBpsDevice {
402				throttleDevice := configs.NewThrottleDevice(td.Major, td.Minor, *td.Rate)
403				c.Resources.BlkioThrottleWriteBpsDevice = append(c.Resources.BlkioThrottleWriteBpsDevice, throttleDevice)
404			}
405		}
406		if r.BlockIO.ThrottleReadIOPSDevice != nil {
407			for _, td := range r.BlockIO.ThrottleReadIOPSDevice {
408				throttleDevice := configs.NewThrottleDevice(td.Major, td.Minor, *td.Rate)
409				c.Resources.BlkioThrottleReadIOPSDevice = append(c.Resources.BlkioThrottleReadIOPSDevice, throttleDevice)
410			}
411		}
412		if r.BlockIO.ThrottleWriteIOPSDevice != nil {
413			for _, td := range r.BlockIO.ThrottleWriteIOPSDevice {
414				throttleDevice := configs.NewThrottleDevice(td.Major, td.Minor, *td.Rate)
415				c.Resources.BlkioThrottleWriteIOPSDevice = append(c.Resources.BlkioThrottleWriteIOPSDevice, throttleDevice)
416			}
417		}
418	}
419	for _, l := range r.HugepageLimits {
420		c.Resources.HugetlbLimit = append(c.Resources.HugetlbLimit, &configs.HugepageLimit{
421			Pagesize: *l.Pagesize,
422			Limit:    *l.Limit,
423		})
424	}
425	if r.DisableOOMKiller != nil {
426		c.Resources.OomKillDisable = *r.DisableOOMKiller
427	}
428	if r.Network != nil {
429		if r.Network.ClassID != nil {
430			c.Resources.NetClsClassid = string(*r.Network.ClassID)
431		}
432		for _, m := range r.Network.Priorities {
433			c.Resources.NetPrioIfpriomap = append(c.Resources.NetPrioIfpriomap, &configs.IfPrioMap{
434				Interface: m.Name,
435				Priority:  int64(m.Priority),
436			})
437		}
438	}
439	return c, nil
440}
441
442func stringToDeviceRune(s string) (rune, error) {
443	switch s {
444	case "a":
445		return 'a', nil
446	case "b":
447		return 'b', nil
448	case "c":
449		return 'c', nil
450	default:
451		return 0, fmt.Errorf("invalid device type %q", s)
452	}
453}
454
455func createDevices(spec *specs.Spec, config *configs.Config) error {
456	// add whitelisted devices
457	config.Devices = []*configs.Device{
458		{
459			Type:     'c',
460			Path:     "/dev/null",
461			Major:    1,
462			Minor:    3,
463			FileMode: 0666,
464			Uid:      0,
465			Gid:      0,
466		},
467		{
468			Type:     'c',
469			Path:     "/dev/random",
470			Major:    1,
471			Minor:    8,
472			FileMode: 0666,
473			Uid:      0,
474			Gid:      0,
475		},
476		{
477			Type:     'c',
478			Path:     "/dev/full",
479			Major:    1,
480			Minor:    7,
481			FileMode: 0666,
482			Uid:      0,
483			Gid:      0,
484		},
485		{
486			Type:     'c',
487			Path:     "/dev/tty",
488			Major:    5,
489			Minor:    0,
490			FileMode: 0666,
491			Uid:      0,
492			Gid:      0,
493		},
494		{
495			Type:     'c',
496			Path:     "/dev/zero",
497			Major:    1,
498			Minor:    5,
499			FileMode: 0666,
500			Uid:      0,
501			Gid:      0,
502		},
503		{
504			Type:     'c',
505			Path:     "/dev/urandom",
506			Major:    1,
507			Minor:    9,
508			FileMode: 0666,
509			Uid:      0,
510			Gid:      0,
511		},
512	}
513	// merge in additional devices from the spec
514	for _, d := range spec.Linux.Devices {
515		var uid, gid uint32
516		if d.UID != nil {
517			uid = *d.UID
518		}
519		if d.GID != nil {
520			gid = *d.GID
521		}
522		dt, err := stringToDeviceRune(d.Type)
523		if err != nil {
524			return err
525		}
526		device := &configs.Device{
527			Type:     dt,
528			Path:     d.Path,
529			Major:    d.Major,
530			Minor:    d.Minor,
531			FileMode: *d.FileMode,
532			Uid:      uid,
533			Gid:      gid,
534		}
535		config.Devices = append(config.Devices, device)
536	}
537	return nil
538}
539
540func setupUserNamespace(spec *specs.Spec, config *configs.Config) error {
541	if len(spec.Linux.UIDMappings) == 0 {
542		return nil
543	}
544	// do not override the specified user namespace path
545	if config.Namespaces.PathOf(configs.NEWUSER) == "" {
546		config.Namespaces.Add(configs.NEWUSER, "")
547	}
548	create := func(m specs.IDMapping) configs.IDMap {
549		return configs.IDMap{
550			HostID:      int(m.HostID),
551			ContainerID: int(m.ContainerID),
552			Size:        int(m.Size),
553		}
554	}
555	for _, m := range spec.Linux.UIDMappings {
556		config.UidMappings = append(config.UidMappings, create(m))
557	}
558	for _, m := range spec.Linux.GIDMappings {
559		config.GidMappings = append(config.GidMappings, create(m))
560	}
561	rootUID, err := config.HostUID()
562	if err != nil {
563		return err
564	}
565	rootGID, err := config.HostGID()
566	if err != nil {
567		return err
568	}
569	for _, node := range config.Devices {
570		node.Uid = uint32(rootUID)
571		node.Gid = uint32(rootGID)
572	}
573	return nil
574}
575
576// parseMountOptions parses the string and returns the flags, propagation
577// flags and any mount data that it contains.
578func parseMountOptions(options []string) (int, []int, string) {
579	var (
580		flag   int
581		pgflag []int
582		data   []string
583	)
584	flags := map[string]struct {
585		clear bool
586		flag  int
587	}{
588		"async":         {true, syscall.MS_SYNCHRONOUS},
589		"atime":         {true, syscall.MS_NOATIME},
590		"bind":          {false, syscall.MS_BIND},
591		"defaults":      {false, 0},
592		"dev":           {true, syscall.MS_NODEV},
593		"diratime":      {true, syscall.MS_NODIRATIME},
594		"dirsync":       {false, syscall.MS_DIRSYNC},
595		"exec":          {true, syscall.MS_NOEXEC},
596		"mand":          {false, syscall.MS_MANDLOCK},
597		"noatime":       {false, syscall.MS_NOATIME},
598		"nodev":         {false, syscall.MS_NODEV},
599		"nodiratime":    {false, syscall.MS_NODIRATIME},
600		"noexec":        {false, syscall.MS_NOEXEC},
601		"nomand":        {true, syscall.MS_MANDLOCK},
602		"norelatime":    {true, syscall.MS_RELATIME},
603		"nostrictatime": {true, syscall.MS_STRICTATIME},
604		"nosuid":        {false, syscall.MS_NOSUID},
605		"rbind":         {false, syscall.MS_BIND | syscall.MS_REC},
606		"relatime":      {false, syscall.MS_RELATIME},
607		"remount":       {false, syscall.MS_REMOUNT},
608		"ro":            {false, syscall.MS_RDONLY},
609		"rw":            {true, syscall.MS_RDONLY},
610		"strictatime":   {false, syscall.MS_STRICTATIME},
611		"suid":          {true, syscall.MS_NOSUID},
612		"sync":          {false, syscall.MS_SYNCHRONOUS},
613	}
614	propagationFlags := map[string]struct {
615		clear bool
616		flag  int
617	}{
618		"private":     {false, syscall.MS_PRIVATE},
619		"shared":      {false, syscall.MS_SHARED},
620		"slave":       {false, syscall.MS_SLAVE},
621		"unbindable":  {false, syscall.MS_UNBINDABLE},
622		"rprivate":    {false, syscall.MS_PRIVATE | syscall.MS_REC},
623		"rshared":     {false, syscall.MS_SHARED | syscall.MS_REC},
624		"rslave":      {false, syscall.MS_SLAVE | syscall.MS_REC},
625		"runbindable": {false, syscall.MS_UNBINDABLE | syscall.MS_REC},
626	}
627	for _, o := range options {
628		// If the option does not exist in the flags table or the flag
629		// is not supported on the platform,
630		// then it is a data value for a specific fs type
631		if f, exists := flags[o]; exists && f.flag != 0 {
632			if f.clear {
633				flag &= ^f.flag
634			} else {
635				flag |= f.flag
636			}
637		} else if f, exists := propagationFlags[o]; exists && f.flag != 0 {
638			pgflag = append(pgflag, f.flag)
639		} else {
640			data = append(data, o)
641		}
642	}
643	return flag, pgflag, strings.Join(data, ",")
644}
645
646func setupSeccomp(config *specs.Seccomp) (*configs.Seccomp, error) {
647	if config == nil {
648		return nil, nil
649	}
650
651	// No default action specified, no syscalls listed, assume seccomp disabled
652	if config.DefaultAction == "" && len(config.Syscalls) == 0 {
653		return nil, nil
654	}
655
656	newConfig := new(configs.Seccomp)
657	newConfig.Syscalls = []*configs.Syscall{}
658
659	if len(config.Architectures) > 0 {
660		newConfig.Architectures = []string{}
661		for _, arch := range config.Architectures {
662			newArch, err := seccomp.ConvertStringToArch(string(arch))
663			if err != nil {
664				return nil, err
665			}
666			newConfig.Architectures = append(newConfig.Architectures, newArch)
667		}
668	}
669
670	// Convert default action from string representation
671	newDefaultAction, err := seccomp.ConvertStringToAction(string(config.DefaultAction))
672	if err != nil {
673		return nil, err
674	}
675	newConfig.DefaultAction = newDefaultAction
676
677	// Loop through all syscall blocks and convert them to libcontainer format
678	for _, call := range config.Syscalls {
679		newAction, err := seccomp.ConvertStringToAction(string(call.Action))
680		if err != nil {
681			return nil, err
682		}
683
684		newCall := configs.Syscall{
685			Name:   call.Name,
686			Action: newAction,
687			Args:   []*configs.Arg{},
688		}
689
690		// Loop through all the arguments of the syscall and convert them
691		for _, arg := range call.Args {
692			newOp, err := seccomp.ConvertStringToOperator(string(arg.Op))
693			if err != nil {
694				return nil, err
695			}
696
697			newArg := configs.Arg{
698				Index:    arg.Index,
699				Value:    arg.Value,
700				ValueTwo: arg.ValueTwo,
701				Op:       newOp,
702			}
703
704			newCall.Args = append(newCall.Args, &newArg)
705		}
706
707		newConfig.Syscalls = append(newConfig.Syscalls, &newCall)
708	}
709
710	return newConfig, nil
711}
712
713func createHooks(rspec *specs.Spec, config *configs.Config) {
714	config.Hooks = &configs.Hooks{}
715	for _, h := range rspec.Hooks.Prestart {
716		cmd := createCommandHook(h)
717		config.Hooks.Prestart = append(config.Hooks.Prestart, configs.NewCommandHook(cmd))
718	}
719	for _, h := range rspec.Hooks.Poststart {
720		cmd := createCommandHook(h)
721		config.Hooks.Poststart = append(config.Hooks.Poststart, configs.NewCommandHook(cmd))
722	}
723	for _, h := range rspec.Hooks.Poststop {
724		cmd := createCommandHook(h)
725		config.Hooks.Poststop = append(config.Hooks.Poststop, configs.NewCommandHook(cmd))
726	}
727}
728
729func createCommandHook(h specs.Hook) configs.Command {
730	cmd := configs.Command{
731		Path: h.Path,
732		Args: h.Args,
733		Env:  h.Env,
734	}
735	if h.Timeout != nil {
736		d := time.Duration(*h.Timeout) * time.Second
737		cmd.Timeout = &d
738	}
739	return cmd
740}
741