github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/cmd/podmanV2/common/specgen.go (about)

     1  package common
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/containers/image/v5/manifest"
    13  	"github.com/containers/libpod/cmd/podmanV2/parse"
    14  	"github.com/containers/libpod/libpod"
    15  	ann "github.com/containers/libpod/pkg/annotations"
    16  	envLib "github.com/containers/libpod/pkg/env"
    17  	ns "github.com/containers/libpod/pkg/namespaces"
    18  	"github.com/containers/libpod/pkg/specgen"
    19  	systemdGen "github.com/containers/libpod/pkg/systemd/generate"
    20  	"github.com/containers/libpod/pkg/util"
    21  	"github.com/docker/go-units"
    22  	"github.com/opencontainers/runtime-spec/specs-go"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error {
    27  	var (
    28  		err error
    29  		//namespaces map[string]string
    30  	)
    31  
    32  	// validate flags as needed
    33  	if err := c.validate(); err != nil {
    34  		return nil
    35  	}
    36  
    37  	inputCommand := args[1:]
    38  	if len(c.HealthCmd) > 0 {
    39  		s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod)
    40  		if err != nil {
    41  			return err
    42  		}
    43  	}
    44  
    45  	s.IDMappings, err = util.ParseIDMapping(ns.UsernsMode(c.UserNS), c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	if m := c.Memory; len(m) > 0 {
    50  		ml, err := units.RAMInBytes(m)
    51  		if err != nil {
    52  			return errors.Wrapf(err, "invalid value for memory")
    53  		}
    54  		s.ResourceLimits.Memory.Limit = &ml
    55  	}
    56  	if m := c.MemoryReservation; len(m) > 0 {
    57  		mr, err := units.RAMInBytes(m)
    58  		if err != nil {
    59  			return errors.Wrapf(err, "invalid value for memory")
    60  		}
    61  		s.ResourceLimits.Memory.Reservation = &mr
    62  	}
    63  	if m := c.MemorySwap; len(m) > 0 {
    64  		var ms int64
    65  		if m == "-1" {
    66  			ms = int64(-1)
    67  			s.ResourceLimits.Memory.Swap = &ms
    68  		} else {
    69  			ms, err = units.RAMInBytes(m)
    70  			if err != nil {
    71  				return errors.Wrapf(err, "invalid value for memory")
    72  			}
    73  		}
    74  		s.ResourceLimits.Memory.Swap = &ms
    75  	}
    76  	if m := c.KernelMemory; len(m) > 0 {
    77  		mk, err := units.RAMInBytes(m)
    78  		if err != nil {
    79  			return errors.Wrapf(err, "invalid value for kernel-memory")
    80  		}
    81  		s.ResourceLimits.Memory.Kernel = &mk
    82  	}
    83  	if b := c.BlkIOWeight; len(b) > 0 {
    84  		u, err := strconv.ParseUint(b, 10, 16)
    85  		if err != nil {
    86  			return errors.Wrapf(err, "invalid value for blkio-weight")
    87  		}
    88  		nu := uint16(u)
    89  		s.ResourceLimits.BlockIO.Weight = &nu
    90  	}
    91  
    92  	s.Terminal = c.TTY
    93  	ep, err := ExposedPorts(c.Expose, c.Net.PublishPorts, c.PublishAll, nil)
    94  	if err != nil {
    95  		return err
    96  	}
    97  	s.PortMappings = ep
    98  	s.Pod = c.Pod
    99  
   100  	//s.CgroupNS = specgen.Namespace{
   101  	//	NSMode: ,
   102  	//	Value:  "",
   103  	//}
   104  
   105  	//s.UserNS = specgen.Namespace{}
   106  
   107  	// Kernel Namespaces
   108  	// TODO Fix handling of namespace from pod
   109  	// Instead of integrating here, should be done in libpod
   110  	// However, that also involves setting up security opts
   111  	// when the pod's namespace is integrated
   112  	//namespaces = map[string]string{
   113  	//	"cgroup": c.CGroupsNS,
   114  	//	"pid":    c.PID,
   115  	//	//"net":    c.Net.Network.Value,   // TODO need help here
   116  	//	"ipc":  c.IPC,
   117  	//	"user": c.User,
   118  	//	"uts":  c.UTS,
   119  	//}
   120  	//
   121  	//if len(c.PID) > 0 {
   122  	//	split := strings.SplitN(c.PID, ":", 2)
   123  	//	// need a way to do thsi
   124  	//	specgen.Namespace{
   125  	//		NSMode: split[0],
   126  	//	}
   127  	//	//Value:  split1 if len allows
   128  	//}
   129  	// TODO this is going to have be done after things like pod creation are done because
   130  	// pod creation changes these values.
   131  	//pidMode := ns.PidMode(namespaces["pid"])
   132  	//usernsMode := ns.UsernsMode(namespaces["user"])
   133  	//utsMode := ns.UTSMode(namespaces["uts"])
   134  	//cgroupMode := ns.CgroupMode(namespaces["cgroup"])
   135  	//ipcMode := ns.IpcMode(namespaces["ipc"])
   136  	//// Make sure if network is set to container namespace, port binding is not also being asked for
   137  	//netMode := ns.NetworkMode(namespaces["net"])
   138  	//if netMode.IsContainer() {
   139  	//	if len(portBindings) > 0 {
   140  	//		return nil, errors.Errorf("cannot set port bindings on an existing container network namespace")
   141  	//	}
   142  	//}
   143  
   144  	// TODO Remove when done with namespaces for realz
   145  	// Setting a default for IPC to get this working
   146  	s.IpcNS = specgen.Namespace{
   147  		NSMode: specgen.Private,
   148  		Value:  "",
   149  	}
   150  
   151  	// TODO this is going to have to be done the libpod/server end of things
   152  	// USER
   153  	//user := c.String("user")
   154  	//if user == "" {
   155  	//	switch {
   156  	//	case usernsMode.IsKeepID():
   157  	//		user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID())
   158  	//	case data == nil:
   159  	//		user = "0"
   160  	//	default:
   161  	//		user = data.Config.User
   162  	//	}
   163  	//}
   164  
   165  	// STOP SIGNAL
   166  	signalString := "TERM"
   167  	if sig := c.StopSignal; len(sig) > 0 {
   168  		signalString = sig
   169  	}
   170  	stopSignal, err := util.ParseSignal(signalString)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	s.StopSignal = &stopSignal
   175  
   176  	// ENVIRONMENT VARIABLES
   177  	//
   178  	// Precedence order (higher index wins):
   179  	//  1) env-host, 2) image data, 3) env-file, 4) env
   180  	env := map[string]string{
   181  		"container": "podman",
   182  	}
   183  
   184  	// First transform the os env into a map. We need it for the labels later in
   185  	// any case.
   186  	osEnv, err := envLib.ParseSlice(os.Environ())
   187  	if err != nil {
   188  		return errors.Wrap(err, "error parsing host environment variables")
   189  	}
   190  
   191  	if c.EnvHost {
   192  		env = envLib.Join(env, osEnv)
   193  	}
   194  	// env-file overrides any previous variables
   195  	for _, f := range c.EnvFile {
   196  		fileEnv, err := envLib.ParseFile(f)
   197  		if err != nil {
   198  			return err
   199  		}
   200  		// File env is overridden by env.
   201  		env = envLib.Join(env, fileEnv)
   202  	}
   203  
   204  	// env overrides any previous variables
   205  	if cmdLineEnv := c.env; len(cmdLineEnv) > 0 {
   206  		parsedEnv, err := envLib.ParseSlice(cmdLineEnv)
   207  		if err != nil {
   208  			return err
   209  		}
   210  		env = envLib.Join(env, parsedEnv)
   211  	}
   212  	s.Env = env
   213  
   214  	// LABEL VARIABLES
   215  	labels, err := parse.GetAllLabels(c.LabelFile, c.Label)
   216  	if err != nil {
   217  		return errors.Wrapf(err, "unable to process labels")
   218  	}
   219  
   220  	if systemdUnit, exists := osEnv[systemdGen.EnvVariable]; exists {
   221  		labels[systemdGen.EnvVariable] = systemdUnit
   222  	}
   223  
   224  	s.Labels = labels
   225  
   226  	// ANNOTATIONS
   227  	annotations := make(map[string]string)
   228  
   229  	// First, add our default annotations
   230  	annotations[ann.TTY] = "false"
   231  	if c.TTY {
   232  		annotations[ann.TTY] = "true"
   233  	}
   234  
   235  	// Last, add user annotations
   236  	for _, annotation := range c.Annotation {
   237  		splitAnnotation := strings.SplitN(annotation, "=", 2)
   238  		if len(splitAnnotation) < 2 {
   239  			return errors.Errorf("Annotations must be formatted KEY=VALUE")
   240  		}
   241  		annotations[splitAnnotation[0]] = splitAnnotation[1]
   242  	}
   243  	s.Annotations = annotations
   244  
   245  	workDir := "/"
   246  	if wd := c.Workdir; len(wd) > 0 {
   247  		workDir = wd
   248  	}
   249  	s.WorkDir = workDir
   250  	entrypoint := []string{}
   251  	userCommand := []string{}
   252  	if ep := c.Entrypoint; len(ep) > 0 {
   253  		// Check if entrypoint specified is json
   254  		if err := json.Unmarshal([]byte(c.Entrypoint), &entrypoint); err != nil {
   255  			entrypoint = append(entrypoint, ep)
   256  		}
   257  	}
   258  
   259  	var command []string
   260  
   261  	// Build the command
   262  	// If we have an entry point, it goes first
   263  	if len(entrypoint) > 0 {
   264  		command = entrypoint
   265  	}
   266  	if len(inputCommand) > 0 {
   267  		// User command overrides data CMD
   268  		command = append(command, inputCommand...)
   269  		userCommand = append(userCommand, inputCommand...)
   270  	}
   271  
   272  	if len(inputCommand) > 0 {
   273  		s.Command = userCommand
   274  	} else {
   275  		s.Command = command
   276  	}
   277  
   278  	// SHM Size
   279  	shmSize, err := units.FromHumanSize(c.ShmSize)
   280  	if err != nil {
   281  		return errors.Wrapf(err, "unable to translate --shm-size")
   282  	}
   283  	s.ShmSize = &shmSize
   284  	s.HostAdd = c.Net.AddHosts
   285  	s.DNSServer = c.Net.DNSServers
   286  	s.DNSSearch = c.Net.DNSSearch
   287  	s.DNSOption = c.Net.DNSOptions
   288  
   289  	// deferred, must be added on libpod side
   290  	//var ImageVolumes map[string]struct{}
   291  	//if data != nil && c.String("image-volume") != "ignore" {
   292  	//	ImageVolumes = data.Config.Volumes
   293  	//}
   294  
   295  	s.ImageVolumeMode = c.ImageVolume
   296  	systemd := c.SystemdD == "always"
   297  	if !systemd && command != nil {
   298  		x, err := strconv.ParseBool(c.SystemdD)
   299  		if err != nil {
   300  			return errors.Wrapf(err, "cannot parse bool %s", c.SystemdD)
   301  		}
   302  		if x && (command[0] == "/usr/sbin/init" || command[0] == "/sbin/init" || (filepath.Base(command[0]) == "systemd")) {
   303  			systemd = true
   304  		}
   305  	}
   306  	if systemd {
   307  		if s.StopSignal == nil {
   308  			stopSignal, err = util.ParseSignal("RTMIN+3")
   309  			if err != nil {
   310  				return errors.Wrapf(err, "error parsing systemd signal")
   311  			}
   312  			s.StopSignal = &stopSignal
   313  		}
   314  	}
   315  	swappiness := uint64(c.MemorySwappiness)
   316  	if s.ResourceLimits == nil {
   317  		s.ResourceLimits = &specs.LinuxResources{}
   318  	}
   319  	if s.ResourceLimits.Memory == nil {
   320  		s.ResourceLimits.Memory = &specs.LinuxMemory{}
   321  	}
   322  	s.ResourceLimits.Memory.Swappiness = &swappiness
   323  
   324  	if s.LogConfiguration == nil {
   325  		s.LogConfiguration = &specgen.LogConfig{}
   326  	}
   327  	s.LogConfiguration.Driver = libpod.KubernetesLogging
   328  	if ld := c.LogDriver; len(ld) > 0 {
   329  		s.LogConfiguration.Driver = ld
   330  	}
   331  	if s.ResourceLimits.Pids == nil {
   332  		s.ResourceLimits.Pids = &specs.LinuxPids{}
   333  	}
   334  	s.ResourceLimits.Pids.Limit = c.PIDsLimit
   335  	if c.CGroups == "disabled" && c.PIDsLimit > 0 {
   336  		s.ResourceLimits.Pids.Limit = -1
   337  	}
   338  	// TODO WTF
   339  	//cgroup := &cc.CgroupConfig{
   340  	//	Cgroups:      c.String("cgroups"),
   341  	//	Cgroupns:     c.String("cgroupns"),
   342  	//	CgroupParent: c.String("cgroup-parent"),
   343  	//	CgroupMode:   cgroupMode,
   344  	//}
   345  	//
   346  	//userns := &cc.UserConfig{
   347  	//	GroupAdd:   c.StringSlice("group-add"),
   348  	//	IDMappings: idmappings,
   349  	//	UsernsMode: usernsMode,
   350  	//	User:       user,
   351  	//}
   352  	//
   353  	//uts := &cc.UtsConfig{
   354  	//	UtsMode:  utsMode,
   355  	//	NoHosts:  c.Bool("no-hosts"),
   356  	//	HostAdd:  c.StringSlice("add-host"),
   357  	//	Hostname: c.String("hostname"),
   358  	//}
   359  
   360  	sysctl := map[string]string{}
   361  	if ctl := c.Sysctl; len(ctl) > 0 {
   362  		sysctl, err = util.ValidateSysctls(ctl)
   363  		if err != nil {
   364  			return err
   365  		}
   366  	}
   367  	s.Sysctl = sysctl
   368  
   369  	s.CapAdd = c.CapAdd
   370  	s.CapDrop = c.CapDrop
   371  	s.Privileged = c.Privileged
   372  	s.ReadOnlyFilesystem = c.ReadOnly
   373  
   374  	// TODO
   375  	// ouitside of specgen and oci though
   376  	// defaults to true, check spec/storage
   377  	//s.readon = c.ReadOnlyTmpFS
   378  	//  TODO convert to map?
   379  	// check if key=value and convert
   380  	sysmap := make(map[string]string)
   381  	for _, ctl := range c.Sysctl {
   382  		splitCtl := strings.SplitN(ctl, "=", 2)
   383  		if len(splitCtl) < 2 {
   384  			return errors.Errorf("invalid sysctl value %q", ctl)
   385  		}
   386  		sysmap[splitCtl[0]] = splitCtl[1]
   387  	}
   388  	s.Sysctl = sysmap
   389  
   390  	for _, opt := range c.SecurityOpt {
   391  		if opt == "no-new-privileges" {
   392  			s.ContainerSecurityConfig.NoNewPrivileges = true
   393  		} else {
   394  			con := strings.SplitN(opt, "=", 2)
   395  			if len(con) != 2 {
   396  				return fmt.Errorf("invalid --security-opt 1: %q", opt)
   397  			}
   398  
   399  			switch con[0] {
   400  			case "label":
   401  				// TODO selinux opts and label opts are the same thing
   402  				s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1])
   403  			case "apparmor":
   404  				s.ContainerSecurityConfig.ApparmorProfile = con[1]
   405  			case "seccomp":
   406  				s.SeccompProfilePath = con[1]
   407  			default:
   408  				return fmt.Errorf("invalid --security-opt 2: %q", opt)
   409  			}
   410  		}
   411  	}
   412  
   413  	// TODO any idea why this was done
   414  	// storage.go from spec/
   415  	// grab it
   416  	//volumes := rtc.Containers.Volumes
   417  	// TODO conflict on populate?
   418  	//if v := c.Volume; len(v)> 0 {
   419  	//	s.Volumes = append(volumes, c.StringSlice("volume")...)
   420  	//}
   421  	//s.volu
   422  
   423  	//s.Mounts = c.Mount
   424  	s.VolumesFrom = c.VolumesFrom
   425  
   426  	// TODO any idea why this was done
   427  	//devices := rtc.Containers.Devices
   428  	// TODO conflict on populate?
   429  	//
   430  	//if c.Changed("device") {
   431  	//	devices = append(devices, c.StringSlice("device")...)
   432  	//}
   433  
   434  	// TODO things i cannot find in spec
   435  	// we dont think these are in the spec
   436  	// init - initbinary
   437  	// initpath
   438  	s.Stdin = c.Interactive
   439  	// quiet
   440  	//DeviceCgroupRules: c.StringSlice("device-cgroup-rule"),
   441  
   442  	if bps := c.DeviceReadBPs; len(bps) > 0 {
   443  		if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil {
   444  			return err
   445  		}
   446  	}
   447  
   448  	if bps := c.DeviceWriteBPs; len(bps) > 0 {
   449  		if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil {
   450  			return err
   451  		}
   452  	}
   453  
   454  	if iops := c.DeviceReadIOPs; len(iops) > 0 {
   455  		if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil {
   456  			return err
   457  		}
   458  	}
   459  
   460  	if iops := c.DeviceWriteIOPs; len(iops) > 0 {
   461  		if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil {
   462  			return err
   463  		}
   464  	}
   465  
   466  	s.ResourceLimits.Memory.DisableOOMKiller = &c.OOMKillDisable
   467  
   468  	// Rlimits/Ulimits
   469  	for _, u := range c.Ulimit {
   470  		if u == "host" {
   471  			s.Rlimits = nil
   472  			break
   473  		}
   474  		ul, err := units.ParseUlimit(u)
   475  		if err != nil {
   476  			return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u)
   477  		}
   478  		rl := specs.POSIXRlimit{
   479  			Type: ul.Name,
   480  			Hard: uint64(ul.Hard),
   481  			Soft: uint64(ul.Soft),
   482  		}
   483  		s.Rlimits = append(s.Rlimits, rl)
   484  	}
   485  
   486  	//Tmpfs:         c.StringArray("tmpfs"),
   487  
   488  	// TODO how to handle this?
   489  	//Syslog:        c.Bool("syslog"),
   490  
   491  	logOpts := make(map[string]string)
   492  	for _, o := range c.LogOptions {
   493  		split := strings.SplitN(o, "=", 2)
   494  		if len(split) < 2 {
   495  			return errors.Errorf("invalid log option %q", o)
   496  		}
   497  		logOpts[split[0]] = split[1]
   498  	}
   499  	s.LogConfiguration.Options = logOpts
   500  	s.Name = c.Name
   501  
   502  	if err := parseWeightDevices(c.BlkIOWeightDevice, s); err != nil {
   503  		return err
   504  	}
   505  
   506  	if s.ResourceLimits.CPU == nil {
   507  		s.ResourceLimits.CPU = &specs.LinuxCPU{}
   508  	}
   509  	s.ResourceLimits.CPU.Shares = &c.CPUShares
   510  	s.ResourceLimits.CPU.Period = &c.CPUPeriod
   511  
   512  	// TODO research these
   513  	//s.ResourceLimits.CPU.Cpus = c.CPUS
   514  	//s.ResourceLimits.CPU.Cpus = c.CPUSetCPUs
   515  
   516  	//s.ResourceLimits.CPU. = c.CPUSetCPUs
   517  	s.ResourceLimits.CPU.Mems = c.CPUSetMems
   518  	s.ResourceLimits.CPU.Quota = &c.CPUQuota
   519  	s.ResourceLimits.CPU.RealtimePeriod = &c.CPURTPeriod
   520  	s.ResourceLimits.CPU.RealtimeRuntime = &c.CPURTRuntime
   521  	s.OOMScoreAdj = &c.OOMScoreAdj
   522  	s.RestartPolicy = c.Restart
   523  	s.Remove = c.Rm
   524  	s.StopTimeout = &c.StopTimeout
   525  
   526  	// TODO where should we do this?
   527  	//func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) {
   528  	return nil
   529  }
   530  
   531  func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) {
   532  	// Every healthcheck requires a command
   533  	if len(inCmd) == 0 {
   534  		return nil, errors.New("Must define a healthcheck command for all healthchecks")
   535  	}
   536  
   537  	// first try to parse option value as JSON array of strings...
   538  	cmd := []string{}
   539  	err := json.Unmarshal([]byte(inCmd), &cmd)
   540  	if err != nil {
   541  		// ...otherwise pass it to "/bin/sh -c" inside the container
   542  		cmd = []string{"CMD-SHELL", inCmd}
   543  	}
   544  	hc := manifest.Schema2HealthConfig{
   545  		Test: cmd,
   546  	}
   547  
   548  	if interval == "disable" {
   549  		interval = "0"
   550  	}
   551  	intervalDuration, err := time.ParseDuration(interval)
   552  	if err != nil {
   553  		return nil, errors.Wrapf(err, "invalid healthcheck-interval %s ", interval)
   554  	}
   555  
   556  	hc.Interval = intervalDuration
   557  
   558  	if retries < 1 {
   559  		return nil, errors.New("healthcheck-retries must be greater than 0.")
   560  	}
   561  	hc.Retries = int(retries)
   562  	timeoutDuration, err := time.ParseDuration(timeout)
   563  	if err != nil {
   564  		return nil, errors.Wrapf(err, "invalid healthcheck-timeout %s", timeout)
   565  	}
   566  	if timeoutDuration < time.Duration(1) {
   567  		return nil, errors.New("healthcheck-timeout must be at least 1 second")
   568  	}
   569  	hc.Timeout = timeoutDuration
   570  
   571  	startPeriodDuration, err := time.ParseDuration(startPeriod)
   572  	if err != nil {
   573  		return nil, errors.Wrapf(err, "invalid healthcheck-start-period %s", startPeriod)
   574  	}
   575  	if startPeriodDuration < time.Duration(0) {
   576  		return nil, errors.New("healthcheck-start-period must be 0 seconds or greater")
   577  	}
   578  	hc.StartPeriod = startPeriodDuration
   579  
   580  	return &hc, nil
   581  }
   582  
   583  func parseWeightDevices(weightDevs []string, s *specgen.SpecGenerator) error {
   584  	for _, val := range weightDevs {
   585  		split := strings.SplitN(val, ":", 2)
   586  		if len(split) != 2 {
   587  			return fmt.Errorf("bad format: %s", val)
   588  		}
   589  		if !strings.HasPrefix(split[0], "/dev/") {
   590  			return fmt.Errorf("bad format for device path: %s", val)
   591  		}
   592  		weight, err := strconv.ParseUint(split[1], 10, 0)
   593  		if err != nil {
   594  			return fmt.Errorf("invalid weight for device: %s", val)
   595  		}
   596  		if weight > 0 && (weight < 10 || weight > 1000) {
   597  			return fmt.Errorf("invalid weight for device: %s", val)
   598  		}
   599  		w := uint16(weight)
   600  		s.WeightDevice[split[0]] = specs.LinuxWeightDevice{
   601  			Weight:     &w,
   602  			LeafWeight: nil,
   603  		}
   604  	}
   605  	return nil
   606  }
   607  
   608  func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) {
   609  	td := make(map[string]specs.LinuxThrottleDevice)
   610  	for _, val := range bpsDevices {
   611  		split := strings.SplitN(val, ":", 2)
   612  		if len(split) != 2 {
   613  			return nil, fmt.Errorf("bad format: %s", val)
   614  		}
   615  		if !strings.HasPrefix(split[0], "/dev/") {
   616  			return nil, fmt.Errorf("bad format for device path: %s", val)
   617  		}
   618  		rate, err := units.RAMInBytes(split[1])
   619  		if err != nil {
   620  			return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val)
   621  		}
   622  		if rate < 0 {
   623  			return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val)
   624  		}
   625  		td[split[0]] = specs.LinuxThrottleDevice{Rate: uint64(rate)}
   626  	}
   627  	return td, nil
   628  }
   629  
   630  func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) {
   631  	td := make(map[string]specs.LinuxThrottleDevice)
   632  	for _, val := range iopsDevices {
   633  		split := strings.SplitN(val, ":", 2)
   634  		if len(split) != 2 {
   635  			return nil, fmt.Errorf("bad format: %s", val)
   636  		}
   637  		if !strings.HasPrefix(split[0], "/dev/") {
   638  			return nil, fmt.Errorf("bad format for device path: %s", val)
   639  		}
   640  		rate, err := strconv.ParseUint(split[1], 10, 64)
   641  		if err != nil {
   642  			return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val)
   643  		}
   644  		td[split[0]] = specs.LinuxThrottleDevice{Rate: rate}
   645  	}
   646  	return td, nil
   647  }