diff options
author | OpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com> | 2020-04-03 23:53:45 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-03 23:53:45 +0200 |
commit | f7dffedeb610df662e69915fcff1bb37986baf55 (patch) | |
tree | 33ced51adee58d38b39416191e2e8a207ce4ec47 /cmd/podmanV2/common | |
parent | 35f586783388cdff6b4f15e7aff4df1ee72d9b67 (diff) | |
parent | 6514a5c80ef91ef6e16e283339cd0b5f78a42322 (diff) | |
download | podman-f7dffedeb610df662e69915fcff1bb37986baf55.tar.gz podman-f7dffedeb610df662e69915fcff1bb37986baf55.tar.bz2 podman-f7dffedeb610df662e69915fcff1bb37986baf55.zip |
Merge pull request #5710 from baude/v2create
v2podman container create
Diffstat (limited to 'cmd/podmanV2/common')
-rw-r--r-- | cmd/podmanV2/common/create.go | 534 | ||||
-rw-r--r-- | cmd/podmanV2/common/create_opts.go | 103 | ||||
-rw-r--r-- | cmd/podmanV2/common/createparse.go | 51 | ||||
-rw-r--r-- | cmd/podmanV2/common/default.go | 121 | ||||
-rw-r--r-- | cmd/podmanV2/common/ports.go | 126 | ||||
-rw-r--r-- | cmd/podmanV2/common/specgen.go | 647 |
6 files changed, 1582 insertions, 0 deletions
diff --git a/cmd/podmanV2/common/create.go b/cmd/podmanV2/common/create.go new file mode 100644 index 000000000..724ed2f42 --- /dev/null +++ b/cmd/podmanV2/common/create.go @@ -0,0 +1,534 @@ +package common + +import ( + "fmt" + "os" + + buildahcli "github.com/containers/buildah/pkg/cli" + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" +) + +const ( + sizeWithUnitFormat = "(format: `<number>[<unit>]`, where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))" +) + +var ( + defaultContainerConfig = getDefaultContainerConfig() +) + +func getDefaultContainerConfig() *config.Config { + defaultContainerConfig, err := config.Default() + if err != nil { + logrus.Error(err) + os.Exit(1) + } + return defaultContainerConfig +} + +func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { + //createFlags := c.Flags() + createFlags := pflag.FlagSet{} + createFlags.StringSliceVar( + &cf.Annotation, + "annotation", []string{}, + "Add annotations to container (key:value)", + ) + createFlags.StringSliceVarP( + &cf.Attach, + "attach", "a", []string{}, + "Attach to STDIN, STDOUT or STDERR", + ) + createFlags.StringVar( + &cf.Authfile, + "authfile", buildahcli.GetDefaultAuthFile(), + "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override", + ) + createFlags.StringVar( + &cf.BlkIOWeight, + "blkio-weight", "", + "Block IO weight (relative weight) accepts a weight value between 10 and 1000.", + ) + createFlags.StringSliceVar( + &cf.BlkIOWeightDevice, + "blkio-weight-device", []string{}, + "Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`)", + ) + createFlags.StringSliceVar( + &cf.CapAdd, + "cap-add", []string{}, + "Add capabilities to the container", + ) + createFlags.StringSliceVar( + &cf.CapDrop, + "cap-drop", []string{}, + "Drop capabilities from the container", + ) + createFlags.StringVar( + &cf.CGroupsNS, + "cgroupns", getDefaultCgroupNS(), + "cgroup namespace to use", + ) + createFlags.StringVar( + &cf.CGroups, + "cgroups", "enabled", + `control container cgroup configuration ("enabled"|"disabled"|"no-conmon")`, + ) + createFlags.StringVar( + &cf.CGroupParent, + "cgroup-parent", "", + "Optional parent cgroup for the container", + ) + createFlags.StringVar( + &cf.CIDFile, + "cidfile", "", + "Write the container ID to the file", + ) + createFlags.StringVar( + &cf.ConmonPIDFile, + "conmon-pidfile", "", + "Path to the file that will receive the PID of conmon", + ) + createFlags.Uint64Var( + &cf.CPUPeriod, + "cpu-period", 0, + "Limit the CPU CFS (Completely Fair Scheduler) period", + ) + createFlags.Int64Var( + &cf.CPUQuota, + "cpu-quota", 0, + "Limit the CPU CFS (Completely Fair Scheduler) quota", + ) + createFlags.Uint64Var( + &cf.CPURTPeriod, + "cpu-rt-period", 0, + "Limit the CPU real-time period in microseconds", + ) + createFlags.Int64Var( + &cf.CPURTRuntime, + "cpu-rt-runtime", 0, + "Limit the CPU real-time runtime in microseconds", + ) + createFlags.Uint64Var( + &cf.CPUShares, + "cpu-shares", 0, + "CPU shares (relative weight)", + ) + createFlags.Float64Var( + &cf.CPUS, + "cpus", 0, + "Number of CPUs. The default is 0.000 which means no limit", + ) + createFlags.StringVar( + &cf.CPUSetCPUs, + "cpuset-cpus", "", + "CPUs in which to allow execution (0-3, 0,1)", + ) + createFlags.StringVar( + &cf.CPUSetMems, + "cpuset-mems", "", + "Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.", + ) + createFlags.BoolVarP( + &cf.Detach, + "detach", "d", false, + "Run container in background and print container ID", + ) + createFlags.StringVar( + &cf.DetachKeys, + "detach-keys", getDefaultDetachKeys(), + "Override the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-cf`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`", + ) + createFlags.StringSliceVar( + &cf.Device, + "device", getDefaultDevices(), + fmt.Sprintf("Add a host device to the container"), + ) + createFlags.StringSliceVar( + &cf.DeviceCGroupRule, + "device-cgroup-rule", []string{}, + "Add a rule to the cgroup allowed devices list", + ) + createFlags.StringSliceVar( + &cf.DeviceReadBPs, + "device-read-bps", []string{}, + "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)", + ) + createFlags.StringSliceVar( + &cf.DeviceReadIOPs, + "device-read-iops", []string{}, + "Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)", + ) + createFlags.StringSliceVar( + &cf.DeviceWriteBPs, + "device-write-bps", []string{}, + "Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb)", + ) + createFlags.StringSliceVar( + &cf.DeviceWriteIOPs, + "device-write-iops", []string{}, + "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)", + ) + createFlags.StringVar( + &cf.Entrypoint, + "entrypoint", "", + "Overwrite the default ENTRYPOINT of the image", + ) + createFlags.StringArrayVarP( + &cf.env, + "env", "e", getDefaultEnv(), + "Set environment variables in container", + ) + createFlags.BoolVar( + &cf.EnvHost, + "env-host", false, "Use all current host environment variables in container", + ) + createFlags.StringSliceVar( + &cf.EnvFile, + "env-file", []string{}, + "Read in a file of environment variables", + ) + createFlags.StringSliceVar( + &cf.Expose, + "expose", []string{}, + "Expose a port or a range of ports", + ) + createFlags.StringSliceVar( + &cf.GIDMap, + "gidmap", []string{}, + "GID map to use for the user namespace", + ) + createFlags.StringSliceVar( + &cf.GroupAdd, + "group-add", []string{}, + "Add additional groups to join", + ) + createFlags.Bool( + "help", false, "", + ) + createFlags.StringVar( + &cf.HealthCmd, + "health-cmd", "", + "set a healthcheck command for the container ('none' disables the existing healthcheck)", + ) + createFlags.StringVar( + &cf.HealthInterval, + "health-interval", cliconfig.DefaultHealthCheckInterval, + "set an interval for the healthchecks (a value of disable results in no automatic timer setup)", + ) + createFlags.UintVar( + &cf.HealthRetries, + "health-retries", cliconfig.DefaultHealthCheckRetries, + "the number of retries allowed before a healthcheck is considered to be unhealthy", + ) + createFlags.StringVar( + &cf.HealthStartPeriod, + "health-start-period", cliconfig.DefaultHealthCheckStartPeriod, + "the initialization time needed for a container to bootstrap", + ) + createFlags.StringVar( + &cf.HealthTimeout, + "health-timeout", cliconfig.DefaultHealthCheckTimeout, + "the maximum time allowed to complete the healthcheck before an interval is considered failed", + ) + createFlags.StringVarP( + &cf.Hostname, + "hostname", "h", "", + "Set container hostname", + ) + createFlags.BoolVar( + &cf.HTTPProxy, + "http-proxy", true, + "Set proxy environment variables in the container based on the host proxy vars", + ) + createFlags.StringVar( + &cf.ImageVolume, + "image-volume", cliconfig.DefaultImageVolume, + `Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore")`, + ) + createFlags.BoolVar( + &cf.Init, + "init", false, + "Run an init binary inside the container that forwards signals and reaps processes", + ) + createFlags.StringVar( + &cf.InitPath, + "init-path", getDefaultInitPath(), + // Do not use the Value field for setting the default value to determine user input (i.e., non-empty string) + fmt.Sprintf("Path to the container-init binary"), + ) + createFlags.BoolVarP( + &cf.Interactive, + "interactive", "i", false, + "Keep STDIN open even if not attached", + ) + createFlags.StringVar( + &cf.IPC, + "ipc", getDefaultIPCNS(), + "IPC namespace to use", + ) + createFlags.StringVar( + &cf.KernelMemory, + "kernel-memory", "", + "Kernel memory limit "+sizeWithUnitFormat, + ) + createFlags.StringArrayVarP( + &cf.Label, + "label", "l", []string{}, + "Set metadata on container", + ) + createFlags.StringSliceVar( + &cf.LabelFile, + "label-file", []string{}, + "Read in a line delimited file of labels", + ) + createFlags.StringVar( + &cf.LogDriver, + "log-driver", "", + "Logging driver for the container", + ) + createFlags.StringSliceVar( + &cf.LogOptions, + "log-opt", []string{}, + "Logging driver options", + ) + createFlags.StringVarP( + &cf.Memory, + "memory", "m", "", + "Memory limit "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.MemoryReservation, + "memory-reservation", "", + "Memory soft limit "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.MemorySwap, + "memory-swap", "", + "Swap limit equal to memory plus swap: '-1' to enable unlimited swap", + ) + createFlags.Int64Var( + &cf.MemorySwappiness, + "memory-swappiness", -1, + "Tune container memory swappiness (0 to 100, or -1 for system default)", + ) + createFlags.StringVar( + &cf.Name, + "name", "", + "Assign a name to the container", + ) + createFlags.BoolVar( + &cf.NoHealthCheck, + "no-healthcheck", false, + "Disable healthchecks on container", + ) + createFlags.BoolVar( + &cf.OOMKillDisable, + "oom-kill-disable", false, + "Disable OOM Killer", + ) + createFlags.IntVar( + &cf.OOMScoreAdj, + "oom-score-adj", 0, + "Tune the host's OOM preferences (-1000 to 1000)", + ) + createFlags.StringVar( + &cf.OverrideArch, + "override-arch", "", + "use `ARCH` instead of the architecture of the machine for choosing images", + ) + //markFlagHidden(createFlags, "override-arch") + createFlags.StringVar( + &cf.OverrideOS, + "override-os", "", + "use `OS` instead of the running OS for choosing images", + ) + //markFlagHidden(createFlags, "override-os") + createFlags.StringVar( + &cf.PID, + "pid", getDefaultPidNS(), + "PID namespace to use", + ) + createFlags.Int64Var( + &cf.PIDsLimit, + "pids-limit", getDefaultPidsLimit(), + getDefaultPidsDescription(), + ) + createFlags.StringVar( + &cf.Pod, + "pod", "", + "Run container in an existing pod", + ) + createFlags.BoolVar( + &cf.Privileged, + "privileged", false, + "Give extended privileges to container", + ) + createFlags.BoolVarP( + &cf.PublishAll, + "publish-all", "P", false, + "Publish all exposed ports to random ports on the host interface", + ) + createFlags.StringVar( + &cf.Pull, + "pull", "missing", + `Pull image before creating ("always"|"missing"|"never")`, + ) + createFlags.BoolVarP( + &cf.Quiet, + "quiet", "q", false, + "Suppress output information when pulling images", + ) + createFlags.BoolVar( + &cf.ReadOnly, + "read-only", false, + "Make containers root filesystem read-only", + ) + createFlags.BoolVar( + &cf.ReadOnlyTmpFS, + "read-only-tmpfs", true, + "When running containers in read-only mode mount a read-write tmpfs on /run, /tmp and /var/tmp", + ) + createFlags.StringVar( + &cf.Restart, + "restart", "", + `Restart policy to apply when a container exits ("always"|"no"|"on-failure")`, + ) + createFlags.BoolVar( + &cf.Rm, + "rm", false, + "Remove container (and pod if created) after exit", + ) + createFlags.BoolVar( + &cf.RootFS, + "rootfs", false, + "The first argument is not an image but the rootfs to the exploded container", + ) + createFlags.StringArrayVar( + &cf.SecurityOpt, + "security-opt", getDefaultSecurityOptions(), + fmt.Sprintf("Security Options"), + ) + createFlags.StringVar( + &cf.ShmSize, + "shm-size", getDefaultShmSize(), + "Size of /dev/shm "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.StopSignal, + "stop-signal", "", + "Signal to stop a container. Default is SIGTERM", + ) + createFlags.UintVar( + &cf.StopTimeout, + "stop-timeout", defaultContainerConfig.Engine.StopTimeout, + "Timeout (in seconds) to stop a container. Default is 10", + ) + createFlags.StringSliceVar( + &cf.StoreageOpt, + "storage-opt", []string{}, + "Storage driver options per container", + ) + createFlags.StringVar( + &cf.SubUIDName, + "subgidname", "", + "Name of range listed in /etc/subgid for use in user namespace", + ) + createFlags.StringVar( + &cf.SubGIDName, + "subuidname", "", + "Name of range listed in /etc/subuid for use in user namespace", + ) + + createFlags.StringSliceVar( + &cf.Sysctl, + "sysctl", getDefaultSysctls(), + "Sysctl options", + ) + createFlags.StringVar( + &cf.SystemdD, + "systemd", "true", + `Run container in systemd mode ("true"|"false"|"always")`, + ) + createFlags.StringArrayVar( + &cf.TmpFS, + "tmpfs", []string{}, + "Mount a temporary filesystem (`tmpfs`) into a container", + ) + createFlags.BoolVarP( + &cf.TTY, + "tty", "t", false, + "Allocate a pseudo-TTY for container", + ) + createFlags.StringSliceVar( + &cf.UIDMap, + "uidmap", []string{}, + "UID map to use for the user namespace", + ) + createFlags.StringSliceVar( + &cf.Ulimit, + "ulimit", getDefaultUlimits(), + "Ulimit options", + ) + createFlags.StringVarP( + &cf.User, + "user", "u", "", + "Username or UID (format: <name|uid>[:<group|gid>])", + ) + createFlags.StringVar( + &cf.UserNS, + "userns", getDefaultUserNS(), + "User namespace to use", + ) + createFlags.StringVar( + &cf.UTS, + "uts", getDefaultUTSNS(), + "UTS namespace to use", + ) + createFlags.StringArrayVar( + &cf.Mount, + "mount", []string{}, + "Attach a filesystem mount to the container", + ) + createFlags.StringArrayVarP( + &cf.Volume, + "volume", "v", getDefaultVolumes(), + "Bind mount a volume into the container", + ) + createFlags.StringSliceVar( + &cf.VolumesFrom, + "volumes-from", []string{}, + "Mount volumes from the specified container(s)", + ) + createFlags.StringVarP( + &cf.Workdir, + "workdir", "w", "", + "Working directory inside the container", + ) + createFlags.StringVar( + &cf.SeccompPolicy, + "seccomp-policy", "default", + "Policy for selecting a seccomp profile (experimental)", + ) + return &createFlags +} + +func AliasFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "healthcheck-command": + name = "health-cmd" + case "healthcheck-interval": + name = "health-interval" + case "healthcheck-retries": + name = "health-retries" + case "healthcheck-start-period": + name = "health-start-period" + case "healthcheck-timeout": + name = "health-timeout" + case "net": + name = "network" + } + return pflag.NormalizedName(name) +} diff --git a/cmd/podmanV2/common/create_opts.go b/cmd/podmanV2/common/create_opts.go new file mode 100644 index 000000000..9d12e4b26 --- /dev/null +++ b/cmd/podmanV2/common/create_opts.go @@ -0,0 +1,103 @@ +package common + +import "github.com/containers/libpod/pkg/domain/entities" + +type ContainerCLIOpts struct { + Annotation []string + Attach []string + Authfile string + BlkIOWeight string + BlkIOWeightDevice []string + CapAdd []string + CapDrop []string + CGroupsNS string + CGroups string + CGroupParent string + CIDFile string + ConmonPIDFile string + CPUPeriod uint64 + CPUQuota int64 + CPURTPeriod uint64 + CPURTRuntime int64 + CPUShares uint64 + CPUS float64 + CPUSetCPUs string + CPUSetMems string + Detach bool + DetachKeys string + Device []string + DeviceCGroupRule []string + DeviceReadBPs []string + DeviceReadIOPs []string + DeviceWriteBPs []string + DeviceWriteIOPs []string + Entrypoint string + env []string + EnvHost bool + EnvFile []string + Expose []string + GIDMap []string + GroupAdd []string + HealthCmd string + HealthInterval string + HealthRetries uint + HealthStartPeriod string + HealthTimeout string + Hostname string + HTTPProxy bool + ImageVolume string + Init bool + InitPath string + Interactive bool + IPC string + KernelMemory string + Label []string + LabelFile []string + LogDriver string + LogOptions []string + Memory string + MemoryReservation string + MemorySwap string + MemorySwappiness int64 + Name string + NoHealthCheck bool + OOMKillDisable bool + OOMScoreAdj int + OverrideArch string + OverrideOS string + PID string + PIDsLimit int64 + Pod string + Privileged bool + PublishAll bool + Pull string + Quiet bool + ReadOnly bool + ReadOnlyTmpFS bool + Restart string + Rm bool + RootFS bool + SecurityOpt []string + ShmSize string + StopSignal string + StopTimeout uint + StoreageOpt []string + SubUIDName string + SubGIDName string + Sysctl []string + SystemdD string + TmpFS []string + TTY bool + UIDMap []string + Ulimit []string + User string + UserNS string + UTS string + Mount []string + Volume []string + VolumesFrom []string + Workdir string + SeccompPolicy string + + Net *entities.NetOptions +} diff --git a/cmd/podmanV2/common/createparse.go b/cmd/podmanV2/common/createparse.go new file mode 100644 index 000000000..89524a04b --- /dev/null +++ b/cmd/podmanV2/common/createparse.go @@ -0,0 +1,51 @@ +package common + +import ( + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +// validate determines if the flags and values given by the user are valid. things checked +// by validate must not need any state information on the flag (i.e. changed) +func (c *ContainerCLIOpts) validate() error { + var () + if c.Rm && c.Restart != "" && c.Restart != "no" { + return errors.Errorf("the --rm option conflicts with --restart") + } + + if _, err := util.ValidatePullType(c.Pull); err != nil { + return err + } + // Verify the additional hosts are in correct format + for _, host := range c.Net.AddHosts { + if _, err := parse.ValidateExtraHost(host); err != nil { + return err + } + } + + if dnsSearches := c.Net.DNSSearch; len(dnsSearches) > 0 { + // Validate domains are good + for _, dom := range dnsSearches { + if dom == "." { + if len(dnsSearches) > 1 { + return errors.Errorf("cannot pass additional search domains when also specifying '.'") + } + continue + } + if _, err := parse.ValidateDomain(dom); err != nil { + return err + } + } + } + var imageVolType = map[string]string{ + "bind": "", + "tmpfs": "", + "ignore": "", + } + if _, ok := imageVolType[c.ImageVolume]; !ok { + return errors.Errorf("invalid image-volume type %q. Pick one of bind, tmpfs, or ignore", c.ImageVolume) + } + return nil + +} diff --git a/cmd/podmanV2/common/default.go b/cmd/podmanV2/common/default.go new file mode 100644 index 000000000..fea161edf --- /dev/null +++ b/cmd/podmanV2/common/default.go @@ -0,0 +1,121 @@ +package common + +import ( + "fmt" + "os" + + "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/pkg/apparmor" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/sysinfo" + "github.com/opencontainers/selinux/go-selinux" +) + +// TODO these options are directly embedded into many of the CLI cobra values, as such +// this approach will not work in a remote client. so we will need to likely do something like a +// supported and unsupported approach here and backload these options into the specgen +// once we are "on" the host system. +func getDefaultSecurityOptions() []string { + securityOpts := []string{} + if defaultContainerConfig.Containers.SeccompProfile != "" && defaultContainerConfig.Containers.SeccompProfile != parse.SeccompDefaultPath { + securityOpts = append(securityOpts, fmt.Sprintf("seccomp=%s", defaultContainerConfig.Containers.SeccompProfile)) + } + if apparmor.IsEnabled() && defaultContainerConfig.Containers.ApparmorProfile != "" { + securityOpts = append(securityOpts, fmt.Sprintf("apparmor=%s", defaultContainerConfig.Containers.ApparmorProfile)) + } + if selinux.GetEnabled() && !defaultContainerConfig.Containers.EnableLabeling { + securityOpts = append(securityOpts, fmt.Sprintf("label=%s", selinux.DisableSecOpt()[0])) + } + return securityOpts +} + +// getDefaultSysctls +func getDefaultSysctls() []string { + return defaultContainerConfig.Containers.DefaultSysctls +} + +func getDefaultVolumes() []string { + return defaultContainerConfig.Containers.Volumes +} + +func getDefaultDevices() []string { + return defaultContainerConfig.Containers.Devices +} + +func getDefaultDNSServers() []string { //nolint + return defaultContainerConfig.Containers.DNSServers +} + +func getDefaultDNSSearches() []string { //nolint + return defaultContainerConfig.Containers.DNSSearches +} + +func getDefaultDNSOptions() []string { //nolint + return defaultContainerConfig.Containers.DNSOptions +} + +func getDefaultEnv() []string { + return defaultContainerConfig.Containers.Env +} + +func getDefaultInitPath() string { + return defaultContainerConfig.Containers.InitPath +} + +func getDefaultIPCNS() string { + return defaultContainerConfig.Containers.IPCNS +} + +func getDefaultPidNS() string { + return defaultContainerConfig.Containers.PidNS +} + +func getDefaultNetNS() string { //nolint + if defaultContainerConfig.Containers.NetNS == "private" && rootless.IsRootless() { + return "slirp4netns" + } + return defaultContainerConfig.Containers.NetNS +} + +func getDefaultCgroupNS() string { + return defaultContainerConfig.Containers.CgroupNS +} + +func getDefaultUTSNS() string { + return defaultContainerConfig.Containers.UTSNS +} + +func getDefaultShmSize() string { + return defaultContainerConfig.Containers.ShmSize +} + +func getDefaultUlimits() []string { + return defaultContainerConfig.Containers.DefaultUlimits +} + +func getDefaultUserNS() string { + userns := os.Getenv("PODMAN_USERNS") + if userns != "" { + return userns + } + return defaultContainerConfig.Containers.UserNS +} + +func getDefaultPidsLimit() int64 { + if rootless.IsRootless() { + cgroup2, _ := cgroups.IsCgroup2UnifiedMode() + if cgroup2 { + return defaultContainerConfig.Containers.PidsLimit + } + } + return sysinfo.GetDefaultPidsLimit() +} + +func getDefaultPidsDescription() string { + return "Tune container pids limit (set 0 for unlimited)" +} + +func getDefaultDetachKeys() string { + return defaultContainerConfig.Engine.DetachKeys +} diff --git a/cmd/podmanV2/common/ports.go b/cmd/podmanV2/common/ports.go new file mode 100644 index 000000000..7e2b1e79d --- /dev/null +++ b/cmd/podmanV2/common/ports.go @@ -0,0 +1,126 @@ +package common + +import ( + "fmt" + "net" + "strconv" + + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ExposedPorts parses user and image ports and returns binding information +func ExposedPorts(expose []string, publish []ocicni.PortMapping, publishAll bool, imageExposedPorts map[string]struct{}) ([]ocicni.PortMapping, error) { + containerPorts := make(map[string]string) + + // TODO this needs to be added into a something that + // has access to an imageengine + // add expose ports from the image itself + //for expose := range imageExposedPorts { + // _, port := nat.SplitProtoPort(expose) + // containerPorts[port] = "" + //} + + // add the expose ports from the user (--expose) + // can be single or a range + for _, expose := range expose { + //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + _, port := nat.SplitProtoPort(expose) + //parse the start and end port and create a sequence of ports to expose + //if expose a port, the start and end port are the same + start, end, err := nat.ParsePortRange(port) + if err != nil { + return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", expose, err) + } + for i := start; i <= end; i++ { + containerPorts[strconv.Itoa(int(i))] = "" + } + } + + // TODO/FIXME this is hell reencarnated + // parse user inputted port bindings + pbPorts, portBindings, err := nat.ParsePortSpecs([]string{}) + if err != nil { + return nil, err + } + + // delete exposed container ports if being used by -p + for i := range pbPorts { + delete(containerPorts, i.Port()) + } + + // iterate container ports and make port bindings from them + if publishAll { + for e := range containerPorts { + //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + //proto, port := nat.SplitProtoPort(e) + p, err := nat.NewPort("tcp", e) + if err != nil { + return nil, err + } + rp, err := getRandomPort() + if err != nil { + return nil, err + } + logrus.Debug(fmt.Sprintf("Using random host port %d with container port %d", rp, p.Int())) + portBindings[p] = CreatePortBinding(rp, "") + } + } + + // We need to see if any host ports are not populated and if so, we need to assign a + // random port to them. + for k, pb := range portBindings { + if pb[0].HostPort == "" { + hostPort, err := getRandomPort() + if err != nil { + return nil, err + } + logrus.Debug(fmt.Sprintf("Using random host port %d with container port %s", hostPort, k.Port())) + pb[0].HostPort = strconv.Itoa(hostPort) + } + } + var pms []ocicni.PortMapping + for k, v := range portBindings { + for _, pb := range v { + hp, err := strconv.Atoi(pb.HostPort) + if err != nil { + return nil, err + } + pms = append(pms, ocicni.PortMapping{ + HostPort: int32(hp), + ContainerPort: int32(k.Int()), + //Protocol: "", + HostIP: pb.HostIP, + }) + } + } + return pms, nil +} + +func getRandomPort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, errors.Wrapf(err, "unable to get free port") + } + defer l.Close() + _, randomPort, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return 0, errors.Wrapf(err, "unable to determine free port") + } + rp, err := strconv.Atoi(randomPort) + if err != nil { + return 0, errors.Wrapf(err, "unable to convert random port to int") + } + return rp, nil +} + +//CreatePortBinding takes port (int) and IP (string) and creates an array of portbinding structs +func CreatePortBinding(hostPort int, hostIP string) []nat.PortBinding { + pb := nat.PortBinding{ + HostPort: strconv.Itoa(hostPort), + } + pb.HostIP = hostIP + return []nat.PortBinding{pb} +} diff --git a/cmd/podmanV2/common/specgen.go b/cmd/podmanV2/common/specgen.go new file mode 100644 index 000000000..5245e206e --- /dev/null +++ b/cmd/podmanV2/common/specgen.go @@ -0,0 +1,647 @@ +package common + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/libpod" + ann "github.com/containers/libpod/pkg/annotations" + envLib "github.com/containers/libpod/pkg/env" + ns "github.com/containers/libpod/pkg/namespaces" + "github.com/containers/libpod/pkg/specgen" + systemdGen "github.com/containers/libpod/pkg/systemd/generate" + "github.com/containers/libpod/pkg/util" + "github.com/docker/go-units" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error { + var ( + err error + //namespaces map[string]string + ) + + // validate flags as needed + if err := c.validate(); err != nil { + return nil + } + + inputCommand := args[1:] + if len(c.HealthCmd) > 0 { + s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod) + if err != nil { + return err + } + } + + s.IDMappings, err = util.ParseIDMapping(ns.UsernsMode(c.UserNS), c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName) + if err != nil { + return err + } + if m := c.Memory; len(m) > 0 { + ml, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + s.ResourceLimits.Memory.Limit = &ml + } + if m := c.MemoryReservation; len(m) > 0 { + mr, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + s.ResourceLimits.Memory.Reservation = &mr + } + if m := c.MemorySwap; len(m) > 0 { + var ms int64 + if m == "-1" { + ms = int64(-1) + s.ResourceLimits.Memory.Swap = &ms + } else { + ms, err = units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + } + s.ResourceLimits.Memory.Swap = &ms + } + if m := c.KernelMemory; len(m) > 0 { + mk, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for kernel-memory") + } + s.ResourceLimits.Memory.Kernel = &mk + } + if b := c.BlkIOWeight; len(b) > 0 { + u, err := strconv.ParseUint(b, 10, 16) + if err != nil { + return errors.Wrapf(err, "invalid value for blkio-weight") + } + nu := uint16(u) + s.ResourceLimits.BlockIO.Weight = &nu + } + + s.Terminal = c.TTY + ep, err := ExposedPorts(c.Expose, c.Net.PublishPorts, c.PublishAll, nil) + if err != nil { + return err + } + s.PortMappings = ep + s.Pod = c.Pod + + //s.CgroupNS = specgen.Namespace{ + // NSMode: , + // Value: "", + //} + + //s.UserNS = specgen.Namespace{} + + // Kernel Namespaces + // TODO Fix handling of namespace from pod + // Instead of integrating here, should be done in libpod + // However, that also involves setting up security opts + // when the pod's namespace is integrated + //namespaces = map[string]string{ + // "cgroup": c.CGroupsNS, + // "pid": c.PID, + // //"net": c.Net.Network.Value, // TODO need help here + // "ipc": c.IPC, + // "user": c.User, + // "uts": c.UTS, + //} + // + //if len(c.PID) > 0 { + // split := strings.SplitN(c.PID, ":", 2) + // // need a way to do thsi + // specgen.Namespace{ + // NSMode: split[0], + // } + // //Value: split1 if len allows + //} + // TODO this is going to have be done after things like pod creation are done because + // pod creation changes these values. + //pidMode := ns.PidMode(namespaces["pid"]) + //usernsMode := ns.UsernsMode(namespaces["user"]) + //utsMode := ns.UTSMode(namespaces["uts"]) + //cgroupMode := ns.CgroupMode(namespaces["cgroup"]) + //ipcMode := ns.IpcMode(namespaces["ipc"]) + //// Make sure if network is set to container namespace, port binding is not also being asked for + //netMode := ns.NetworkMode(namespaces["net"]) + //if netMode.IsContainer() { + // if len(portBindings) > 0 { + // return nil, errors.Errorf("cannot set port bindings on an existing container network namespace") + // } + //} + + // TODO Remove when done with namespaces for realz + // Setting a default for IPC to get this working + s.IpcNS = specgen.Namespace{ + NSMode: specgen.Private, + Value: "", + } + + // TODO this is going to have to be done the libpod/server end of things + // USER + //user := c.String("user") + //if user == "" { + // switch { + // case usernsMode.IsKeepID(): + // user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID()) + // case data == nil: + // user = "0" + // default: + // user = data.Config.User + // } + //} + + // STOP SIGNAL + signalString := "TERM" + if sig := c.StopSignal; len(sig) > 0 { + signalString = sig + } + stopSignal, err := util.ParseSignal(signalString) + if err != nil { + return err + } + s.StopSignal = &stopSignal + + // ENVIRONMENT VARIABLES + // + // Precedence order (higher index wins): + // 1) env-host, 2) image data, 3) env-file, 4) env + env := map[string]string{ + "container": "podman", + } + + // First transform the os env into a map. We need it for the labels later in + // any case. + osEnv, err := envLib.ParseSlice(os.Environ()) + if err != nil { + return errors.Wrap(err, "error parsing host environment variables") + } + + if c.EnvHost { + env = envLib.Join(env, osEnv) + } + // env-file overrides any previous variables + for _, f := range c.EnvFile { + fileEnv, err := envLib.ParseFile(f) + if err != nil { + return err + } + // File env is overridden by env. + env = envLib.Join(env, fileEnv) + } + + // env overrides any previous variables + if cmdLineEnv := c.env; len(cmdLineEnv) > 0 { + parsedEnv, err := envLib.ParseSlice(cmdLineEnv) + if err != nil { + return err + } + env = envLib.Join(env, parsedEnv) + } + s.Env = env + + // LABEL VARIABLES + labels, err := parse.GetAllLabels(c.LabelFile, c.Label) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + + if systemdUnit, exists := osEnv[systemdGen.EnvVariable]; exists { + labels[systemdGen.EnvVariable] = systemdUnit + } + + s.Labels = labels + + // ANNOTATIONS + annotations := make(map[string]string) + + // First, add our default annotations + annotations[ann.TTY] = "false" + if c.TTY { + annotations[ann.TTY] = "true" + } + + // Last, add user annotations + for _, annotation := range c.Annotation { + splitAnnotation := strings.SplitN(annotation, "=", 2) + if len(splitAnnotation) < 2 { + return errors.Errorf("Annotations must be formatted KEY=VALUE") + } + annotations[splitAnnotation[0]] = splitAnnotation[1] + } + s.Annotations = annotations + + workDir := "/" + if wd := c.Workdir; len(wd) > 0 { + workDir = wd + } + s.WorkDir = workDir + entrypoint := []string{} + userCommand := []string{} + if ep := c.Entrypoint; len(ep) > 0 { + // Check if entrypoint specified is json + if err := json.Unmarshal([]byte(c.Entrypoint), &entrypoint); err != nil { + entrypoint = append(entrypoint, ep) + } + } + + var command []string + + // Build the command + // If we have an entry point, it goes first + if len(entrypoint) > 0 { + command = entrypoint + } + if len(inputCommand) > 0 { + // User command overrides data CMD + command = append(command, inputCommand...) + userCommand = append(userCommand, inputCommand...) + } + + if len(inputCommand) > 0 { + s.Command = userCommand + } else { + s.Command = command + } + + // SHM Size + shmSize, err := units.FromHumanSize(c.ShmSize) + if err != nil { + return errors.Wrapf(err, "unable to translate --shm-size") + } + s.ShmSize = &shmSize + s.HostAdd = c.Net.AddHosts + s.DNSServer = c.Net.DNSServers + s.DNSSearch = c.Net.DNSSearch + s.DNSOption = c.Net.DNSOptions + + // deferred, must be added on libpod side + //var ImageVolumes map[string]struct{} + //if data != nil && c.String("image-volume") != "ignore" { + // ImageVolumes = data.Config.Volumes + //} + + s.ImageVolumeMode = c.ImageVolume + systemd := c.SystemdD == "always" + if !systemd && command != nil { + x, err := strconv.ParseBool(c.SystemdD) + if err != nil { + return errors.Wrapf(err, "cannot parse bool %s", c.SystemdD) + } + if x && (command[0] == "/usr/sbin/init" || command[0] == "/sbin/init" || (filepath.Base(command[0]) == "systemd")) { + systemd = true + } + } + if systemd { + if s.StopSignal == nil { + stopSignal, err = util.ParseSignal("RTMIN+3") + if err != nil { + return errors.Wrapf(err, "error parsing systemd signal") + } + s.StopSignal = &stopSignal + } + } + swappiness := uint64(c.MemorySwappiness) + if s.ResourceLimits == nil { + s.ResourceLimits = &specs.LinuxResources{} + } + if s.ResourceLimits.Memory == nil { + s.ResourceLimits.Memory = &specs.LinuxMemory{} + } + s.ResourceLimits.Memory.Swappiness = &swappiness + + if s.LogConfiguration == nil { + s.LogConfiguration = &specgen.LogConfig{} + } + s.LogConfiguration.Driver = libpod.KubernetesLogging + if ld := c.LogDriver; len(ld) > 0 { + s.LogConfiguration.Driver = ld + } + if s.ResourceLimits.Pids == nil { + s.ResourceLimits.Pids = &specs.LinuxPids{} + } + s.ResourceLimits.Pids.Limit = c.PIDsLimit + if c.CGroups == "disabled" && c.PIDsLimit > 0 { + s.ResourceLimits.Pids.Limit = -1 + } + // TODO WTF + //cgroup := &cc.CgroupConfig{ + // Cgroups: c.String("cgroups"), + // Cgroupns: c.String("cgroupns"), + // CgroupParent: c.String("cgroup-parent"), + // CgroupMode: cgroupMode, + //} + // + //userns := &cc.UserConfig{ + // GroupAdd: c.StringSlice("group-add"), + // IDMappings: idmappings, + // UsernsMode: usernsMode, + // User: user, + //} + // + //uts := &cc.UtsConfig{ + // UtsMode: utsMode, + // NoHosts: c.Bool("no-hosts"), + // HostAdd: c.StringSlice("add-host"), + // Hostname: c.String("hostname"), + //} + + sysctl := map[string]string{} + if ctl := c.Sysctl; len(ctl) > 0 { + sysctl, err = util.ValidateSysctls(ctl) + if err != nil { + return err + } + } + s.Sysctl = sysctl + + s.CapAdd = c.CapAdd + s.CapDrop = c.CapDrop + s.Privileged = c.Privileged + s.ReadOnlyFilesystem = c.ReadOnly + + // TODO + // ouitside of specgen and oci though + // defaults to true, check spec/storage + //s.readon = c.ReadOnlyTmpFS + // TODO convert to map? + // check if key=value and convert + sysmap := make(map[string]string) + for _, ctl := range c.Sysctl { + splitCtl := strings.SplitN(ctl, "=", 2) + if len(splitCtl) < 2 { + return errors.Errorf("invalid sysctl value %q", ctl) + } + sysmap[splitCtl[0]] = splitCtl[1] + } + s.Sysctl = sysmap + + for _, opt := range c.SecurityOpt { + if opt == "no-new-privileges" { + s.ContainerSecurityConfig.NoNewPrivileges = true + } else { + con := strings.SplitN(opt, "=", 2) + if len(con) != 2 { + return fmt.Errorf("invalid --security-opt 1: %q", opt) + } + + switch con[0] { + case "label": + // TODO selinux opts and label opts are the same thing + s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) + case "apparmor": + s.ContainerSecurityConfig.ApparmorProfile = con[1] + case "seccomp": + s.SeccompProfilePath = con[1] + default: + return fmt.Errorf("invalid --security-opt 2: %q", opt) + } + } + } + + // TODO any idea why this was done + // storage.go from spec/ + // grab it + //volumes := rtc.Containers.Volumes + // TODO conflict on populate? + //if v := c.Volume; len(v)> 0 { + // s.Volumes = append(volumes, c.StringSlice("volume")...) + //} + //s.volu + + //s.Mounts = c.Mount + s.VolumesFrom = c.VolumesFrom + + // TODO any idea why this was done + //devices := rtc.Containers.Devices + // TODO conflict on populate? + // + //if c.Changed("device") { + // devices = append(devices, c.StringSlice("device")...) + //} + + // TODO things i cannot find in spec + // we dont think these are in the spec + // init - initbinary + // initpath + s.Stdin = c.Interactive + // quiet + //DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), + + if bps := c.DeviceReadBPs; len(bps) > 0 { + if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return err + } + } + + if bps := c.DeviceWriteBPs; len(bps) > 0 { + if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return err + } + } + + if iops := c.DeviceReadIOPs; len(iops) > 0 { + if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return err + } + } + + if iops := c.DeviceWriteIOPs; len(iops) > 0 { + if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return err + } + } + + s.ResourceLimits.Memory.DisableOOMKiller = &c.OOMKillDisable + + // Rlimits/Ulimits + for _, u := range c.Ulimit { + if u == "host" { + s.Rlimits = nil + break + } + ul, err := units.ParseUlimit(u) + if err != nil { + return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) + } + rl := specs.POSIXRlimit{ + Type: ul.Name, + Hard: uint64(ul.Hard), + Soft: uint64(ul.Soft), + } + s.Rlimits = append(s.Rlimits, rl) + } + + //Tmpfs: c.StringArray("tmpfs"), + + // TODO how to handle this? + //Syslog: c.Bool("syslog"), + + logOpts := make(map[string]string) + for _, o := range c.LogOptions { + split := strings.SplitN(o, "=", 2) + if len(split) < 2 { + return errors.Errorf("invalid log option %q", o) + } + logOpts[split[0]] = split[1] + } + s.LogConfiguration.Options = logOpts + s.Name = c.Name + + if err := parseWeightDevices(c.BlkIOWeightDevice, s); err != nil { + return err + } + + if s.ResourceLimits.CPU == nil { + s.ResourceLimits.CPU = &specs.LinuxCPU{} + } + s.ResourceLimits.CPU.Shares = &c.CPUShares + s.ResourceLimits.CPU.Period = &c.CPUPeriod + + // TODO research these + //s.ResourceLimits.CPU.Cpus = c.CPUS + //s.ResourceLimits.CPU.Cpus = c.CPUSetCPUs + + //s.ResourceLimits.CPU. = c.CPUSetCPUs + s.ResourceLimits.CPU.Mems = c.CPUSetMems + s.ResourceLimits.CPU.Quota = &c.CPUQuota + s.ResourceLimits.CPU.RealtimePeriod = &c.CPURTPeriod + s.ResourceLimits.CPU.RealtimeRuntime = &c.CPURTRuntime + s.OOMScoreAdj = &c.OOMScoreAdj + s.RestartPolicy = c.Restart + s.Remove = c.Rm + s.StopTimeout = &c.StopTimeout + + // TODO where should we do this? + //func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) { + return nil +} + +func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) { + // Every healthcheck requires a command + if len(inCmd) == 0 { + return nil, errors.New("Must define a healthcheck command for all healthchecks") + } + + // first try to parse option value as JSON array of strings... + cmd := []string{} + err := json.Unmarshal([]byte(inCmd), &cmd) + if err != nil { + // ...otherwise pass it to "/bin/sh -c" inside the container + cmd = []string{"CMD-SHELL", inCmd} + } + hc := manifest.Schema2HealthConfig{ + Test: cmd, + } + + if interval == "disable" { + interval = "0" + } + intervalDuration, err := time.ParseDuration(interval) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-interval %s ", interval) + } + + hc.Interval = intervalDuration + + if retries < 1 { + return nil, errors.New("healthcheck-retries must be greater than 0.") + } + hc.Retries = int(retries) + timeoutDuration, err := time.ParseDuration(timeout) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-timeout %s", timeout) + } + if timeoutDuration < time.Duration(1) { + return nil, errors.New("healthcheck-timeout must be at least 1 second") + } + hc.Timeout = timeoutDuration + + startPeriodDuration, err := time.ParseDuration(startPeriod) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-start-period %s", startPeriod) + } + if startPeriodDuration < time.Duration(0) { + return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") + } + hc.StartPeriod = startPeriodDuration + + return &hc, nil +} + +func parseWeightDevices(weightDevs []string, s *specgen.SpecGenerator) error { + for _, val := range weightDevs { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return fmt.Errorf("bad format for device path: %s", val) + } + weight, err := strconv.ParseUint(split[1], 10, 0) + if err != nil { + return fmt.Errorf("invalid weight for device: %s", val) + } + if weight > 0 && (weight < 10 || weight > 1000) { + return fmt.Errorf("invalid weight for device: %s", val) + } + w := uint16(weight) + s.WeightDevice[split[0]] = specs.LinuxWeightDevice{ + Weight: &w, + LeafWeight: nil, + } + } + return nil +} + +func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range bpsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := units.RAMInBytes(split[1]) + if err != nil { + 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) + } + if rate < 0 { + 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) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: uint64(rate)} + } + return td, nil +} + +func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range iopsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := strconv.ParseUint(split[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: rate} + } + return td, nil +} |