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