From 6514a5c80ef91ef6e16e283339cd0b5f78a42322 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Sun, 29 Mar 2020 11:25:56 -0500 Subject: v2podman container create create a container in podmanv2 using specgen approach. this is the core implementation and still has quite a bit of code commented out specifically around volumes, devices, and namespaces. need contributions from smes on these parts. Signed-off-by: Brent Baude --- cmd/podmanV2/Makefile | 2 +- cmd/podmanV2/common/create.go | 534 ++++++++++++++++ cmd/podmanV2/common/create_opts.go | 103 ++++ cmd/podmanV2/common/createparse.go | 51 ++ cmd/podmanV2/common/default.go | 121 ++++ cmd/podmanV2/common/ports.go | 126 ++++ cmd/podmanV2/common/specgen.go | 647 ++++++++++++++++++++ cmd/podmanV2/containers/create.go | 102 +++ cmd/podmanV2/parse/common.go | 50 ++ cmd/podmanV2/parse/net.go | 188 ++++++ cmd/podmanV2/parse/net_test.go | 152 +++++ cmd/podmanV2/parse/parse.go | 233 ------- cmd/podmanV2/parse/parse_test.go | 152 ----- pkg/api/handlers/libpod/containers_create.go | 7 +- pkg/domain/entities/containers.go | 4 + pkg/domain/entities/engine_container.go | 2 + pkg/domain/infra/abi/containers.go | 13 + pkg/domain/infra/tunnel/containers.go | 9 + pkg/specgen/config_linux.go | 93 +++ pkg/specgen/config_linux_cgo.go | 1 - pkg/specgen/container_create.go | 189 ------ pkg/specgen/container_validate.go | 4 +- pkg/specgen/generate/container.go | 168 +++++ pkg/specgen/generate/container_create.go | 190 ++++++ pkg/specgen/namespaces.go | 7 +- pkg/specgen/oci.go | 2 +- pkg/specgen/security.go | 165 +++++ pkg/specgen/specgen.go | 13 +- pkg/specgen/storage.go | 885 +++++++++++++++++++++++++++ 29 files changed, 3630 insertions(+), 583 deletions(-) create mode 100644 cmd/podmanV2/common/create.go create mode 100644 cmd/podmanV2/common/create_opts.go create mode 100644 cmd/podmanV2/common/createparse.go create mode 100644 cmd/podmanV2/common/default.go create mode 100644 cmd/podmanV2/common/ports.go create mode 100644 cmd/podmanV2/common/specgen.go create mode 100644 cmd/podmanV2/containers/create.go create mode 100644 cmd/podmanV2/parse/common.go create mode 100644 cmd/podmanV2/parse/net.go create mode 100644 cmd/podmanV2/parse/net_test.go delete mode 100644 cmd/podmanV2/parse/parse.go delete mode 100644 cmd/podmanV2/parse/parse_test.go create mode 100644 pkg/specgen/config_linux.go delete mode 100644 pkg/specgen/container_create.go create mode 100644 pkg/specgen/generate/container.go create mode 100644 pkg/specgen/generate/container_create.go create mode 100644 pkg/specgen/security.go create mode 100644 pkg/specgen/storage.go diff --git a/cmd/podmanV2/Makefile b/cmd/podmanV2/Makefile index f2f7bd73c..b847a9385 100644 --- a/cmd/podmanV2/Makefile +++ b/cmd/podmanV2/Makefile @@ -1,2 +1,2 @@ all: - GO111MODULE=off go build -tags 'ABISupport systemd' + CGO_ENABLED=1 GO111MODULE=off go build -tags 'ABISupport systemd seccomp' 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: `[]`, 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-`, where `` 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: [:])", + ) + 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 /[] or /[] + _, 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 /[] or /[] + //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 :[]. 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 :[]. 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 :. Number must be a positive integer", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: rate} + } + return td, nil +} diff --git a/cmd/podmanV2/containers/create.go b/cmd/podmanV2/containers/create.go new file mode 100644 index 000000000..fd5300966 --- /dev/null +++ b/cmd/podmanV2/containers/create.go @@ -0,0 +1,102 @@ +package containers + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/common" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/specgen" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + createDescription = `Creates a new container from the given image or storage and prepares it for running the specified command. + + The container ID is then printed to stdout. You can then start it at any time with the podman start command. The container will be created with the initial state 'created'.` + createCommand = &cobra.Command{ + Use: "create [flags] IMAGE [COMMAND [ARG...]]", + Short: "Create but do not start a container", + Long: createDescription, + RunE: create, + PersistentPreRunE: preRunE, + Args: cobra.MinimumNArgs(1), + Example: `podman create alpine ls + podman create --annotation HELLO=WORLD alpine ls + podman create -t -i --name myctr alpine ls`, + } +) + +var ( + cliVals common.ContainerCLIOpts +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: createCommand, + }) + //common.GetCreateFlags(createCommand) + flags := createCommand.Flags() + flags.AddFlagSet(common.GetCreateFlags(&cliVals)) + flags.AddFlagSet(common.GetNetFlags()) + flags.SetNormalizeFunc(common.AliasFlags) +} + +func create(cmd *cobra.Command, args []string) error { + var ( + err error + rawImageInput string + ) + cliVals.Net, err = common.NetFlagsToNetOptions(cmd) + if err != nil { + return err + } + if rfs := cliVals.RootFS; !rfs { + rawImageInput = args[0] + } + + if err := createInit(cmd); err != nil { + return err + } + //TODO rootfs still + s := specgen.NewSpecGenerator(rawImageInput) + if err := common.FillOutSpecGen(s, &cliVals, args); err != nil { + return err + } + + report, err := registry.ContainerEngine().ContainerCreate(registry.GetContext(), s) + if err != nil { + return err + } + fmt.Println(report.Id) + return nil +} + +func createInit(c *cobra.Command) error { + if c.Flag("privileged").Changed && c.Flag("security-opt").Changed { + logrus.Warn("setting security options with --privileged has no effect") + } + + if (c.Flag("dns").Changed || c.Flag("dns-opt").Changed || c.Flag("dns-search").Changed) && (cliVals.Net.Network.NSMode == specgen.NoNetwork || cliVals.Net.Network.IsContainer()) { + return errors.Errorf("conflicting options: dns and the network mode.") + } + + if c.Flag("cpu-period").Changed && c.Flag("cpus").Changed { + return errors.Errorf("--cpu-period and --cpus cannot be set together") + } + if c.Flag("cpu-quota").Changed && c.Flag("cpus").Changed { + return errors.Errorf("--cpu-quota and --cpus cannot be set together") + } + + if c.Flag("no-hosts").Changed && c.Flag("add-host").Changed { + return errors.Errorf("--no-hosts and --add-host cannot be set together") + } + + // Docker-compatibility: the "-h" flag for run/create is reserved for + // the hostname (see https://github.com/containers/libpod/issues/1367). + + return nil +} diff --git a/cmd/podmanV2/parse/common.go b/cmd/podmanV2/parse/common.go new file mode 100644 index 000000000..a5e9b4fc2 --- /dev/null +++ b/cmd/podmanV2/parse/common.go @@ -0,0 +1,50 @@ +package parse + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// CheckAllLatestAndCIDFile checks that --all and --latest are used correctly. +// If cidfile is set, also check for the --cidfile flag. +func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { + argLen := len(args) + if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { + if !cidfile { + return errors.New("unable to lookup values for 'latest' or 'all'") + } else if c.Flags().Lookup("cidfile") == nil { + return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") + } + } + + specifiedAll, _ := c.Flags().GetBool("all") + specifiedLatest, _ := c.Flags().GetBool("latest") + specifiedCIDFile := false + if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { + specifiedCIDFile = true + } + + if specifiedCIDFile && (specifiedAll || specifiedLatest) { + return errors.Errorf("--all, --latest and --cidfile cannot be used together") + } else if specifiedAll && specifiedLatest { + return errors.Errorf("--all and --latest cannot be used together") + } + + if ignoreArgLen { + return nil + } + if (argLen > 0) && (specifiedAll || specifiedLatest) { + return errors.Errorf("no arguments are needed with --all or --latest") + } else if cidfile && (argLen > 0) && (specifiedAll || specifiedLatest || specifiedCIDFile) { + return errors.Errorf("no arguments are needed with --all, --latest or --cidfile") + } + + if specifiedCIDFile { + return nil + } + + if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { + return errors.Errorf("you must provide at least one name or id") + } + return nil +} diff --git a/cmd/podmanV2/parse/net.go b/cmd/podmanV2/parse/net.go new file mode 100644 index 000000000..03cda268c --- /dev/null +++ b/cmd/podmanV2/parse/net.go @@ -0,0 +1,188 @@ +//nolint +// most of these validate and parse functions have been taken from projectatomic/docker +// and modified for cri-o +package parse + +import ( + "bufio" + "fmt" + "net" + "net/url" + "os" + "regexp" + "strings" + + "github.com/pkg/errors" +) + +const ( + Protocol_TCP Protocol = 0 + Protocol_UDP Protocol = 1 +) + +type Protocol int32 + +// PortMapping specifies the port mapping configurations of a sandbox. +type PortMapping struct { + // Protocol of the port mapping. + Protocol Protocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=runtime.Protocol" json:"protocol,omitempty"` + // Port number within the container. Default: 0 (not specified). + ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` + // Port number on the host. Default: 0 (not specified). + HostPort int32 `protobuf:"varint,3,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` + // Host IP. + HostIp string `protobuf:"bytes,4,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"` +} + +// Note: for flags that are in the form , use the RAMInBytes function +// from the units package in docker/go-units/size.go + +var ( + whiteSpaces = " \t" + alphaRegexp = regexp.MustCompile(`[a-zA-Z]`) + domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`) +) + +// validateExtraHost validates that the specified string is a valid extrahost and returns it. +// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6). +// for add-host flag +func ValidateExtraHost(val string) (string, error) { //nolint + // allow for IPv6 addresses in extra hosts by only splitting on first ":" + arr := strings.SplitN(val, ":", 2) + if len(arr) != 2 || len(arr[0]) == 0 { + return "", fmt.Errorf("bad format for add-host: %q", val) + } + if _, err := validateIPAddress(arr[1]); err != nil { + return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1]) + } + return val, nil +} + +// validateIPAddress validates an Ip address. +// for dns, ip, and ip6 flags also +func validateIPAddress(val string) (string, error) { + var ip = net.ParseIP(strings.TrimSpace(val)) + if ip != nil { + return ip.String(), nil + } + return "", fmt.Errorf("%s is not an ip address", val) +} + +func ValidateDomain(val string) (string, error) { + if alphaRegexp.FindString(val) == "" { + return "", fmt.Errorf("%s is not a valid domain", val) + } + ns := domainRegexp.FindSubmatch([]byte(val)) + if len(ns) > 0 && len(ns[1]) < 255 { + return string(ns[1]), nil + } + return "", fmt.Errorf("%s is not a valid domain", val) +} + +// GetAllLabels retrieves all labels given a potential label file and a number +// of labels provided from the command line. +func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) { + labels := make(map[string]string) + for _, file := range labelFile { + // Use of parseEnvFile still seems safe, as it's missing the + // extra parsing logic of parseEnv. + // There's an argument that we SHOULD be doing that parsing for + // all environment variables, even those sourced from files, but + // that would require a substantial rework. + if err := parseEnvFile(labels, file); err != nil { + // FIXME: parseEnvFile is using parseEnv, so we need to add extra + // logic for labels. + return nil, err + } + } + for _, label := range inputLabels { + split := strings.SplitN(label, "=", 2) + if split[0] == "" { + return nil, errors.Errorf("invalid label format: %q", label) + } + value := "" + if len(split) > 1 { + value = split[1] + } + labels[split[0]] = value + } + return labels, nil +} + +func parseEnv(env map[string]string, line string) error { + data := strings.SplitN(line, "=", 2) + + // catch invalid variables such as "=" or "=A" + if data[0] == "" { + return errors.Errorf("invalid environment variable: %q", line) + } + + // trim the front of a variable, but nothing else + name := strings.TrimLeft(data[0], whiteSpaces) + if strings.ContainsAny(name, whiteSpaces) { + return errors.Errorf("name %q has white spaces, poorly formatted name", name) + } + + if len(data) > 1 { + env[name] = data[1] + } else { + if strings.HasSuffix(name, "*") { + name = strings.TrimSuffix(name, "*") + for _, e := range os.Environ() { + part := strings.SplitN(e, "=", 2) + if len(part) < 2 { + continue + } + if strings.HasPrefix(part[0], name) { + env[part[0]] = part[1] + } + } + } else { + // if only a pass-through variable is given, clean it up. + if val, ok := os.LookupEnv(name); ok { + env[name] = val + } + } + } + return nil +} + +// parseEnvFile reads a file with environment variables enumerated by lines +func parseEnvFile(env map[string]string, filename string) error { + fh, err := os.Open(filename) + if err != nil { + return err + } + defer fh.Close() + + scanner := bufio.NewScanner(fh) + for scanner.Scan() { + // trim the line from all leading whitespace first + line := strings.TrimLeft(scanner.Text(), whiteSpaces) + // line is not empty, and not starting with '#' + if len(line) > 0 && !strings.HasPrefix(line, "#") { + if err := parseEnv(env, line); err != nil { + return err + } + } + } + return scanner.Err() +} + +// ValidateFileName returns an error if filename contains ":" +// as it is currently not supported +func ValidateFileName(filename string) error { + if strings.Contains(filename, ":") { + return errors.Errorf("invalid filename (should not contain ':') %q", filename) + } + return nil +} + +// ValidURL checks a string urlStr is a url or not +func ValidURL(urlStr string) error { + _, err := url.ParseRequestURI(urlStr) + if err != nil { + return errors.Wrapf(err, "invalid url path: %q", urlStr) + } + return nil +} diff --git a/cmd/podmanV2/parse/net_test.go b/cmd/podmanV2/parse/net_test.go new file mode 100644 index 000000000..a6ddc2be9 --- /dev/null +++ b/cmd/podmanV2/parse/net_test.go @@ -0,0 +1,152 @@ +//nolint +// most of these validate and parse functions have been taken from projectatomic/docker +// and modified for cri-o +package parse + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + Var1 = []string{"ONE=1", "TWO=2"} +) + +func createTmpFile(content []byte) (string, error) { + tmpfile, err := ioutil.TempFile(os.TempDir(), "unittest") + if err != nil { + return "", err + } + + if _, err := tmpfile.Write(content); err != nil { + return "", err + + } + if err := tmpfile.Close(); err != nil { + return "", err + } + return tmpfile.Name(), nil +} + +func TestValidateExtraHost(t *testing.T) { + type args struct { + val string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + //2001:0db8:85a3:0000:0000:8a2e:0370:7334 + {name: "good-ipv4", args: args{val: "foobar:192.168.1.1"}, want: "foobar:192.168.1.1", wantErr: false}, + {name: "bad-ipv4", args: args{val: "foobar:999.999.999.99"}, want: "", wantErr: true}, + {name: "bad-ipv4", args: args{val: "foobar:999.999.999"}, want: "", wantErr: true}, + {name: "noname-ipv4", args: args{val: "192.168.1.1"}, want: "", wantErr: true}, + {name: "noname-ipv4", args: args{val: ":192.168.1.1"}, want: "", wantErr: true}, + {name: "noip", args: args{val: "foobar:"}, want: "", wantErr: true}, + {name: "noip", args: args{val: "foobar"}, want: "", wantErr: true}, + {name: "good-ipv6", args: args{val: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334", wantErr: false}, + {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, + {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334.0000.0000.000"}, want: "", wantErr: true}, + {name: "noname-ipv6", args: args{val: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, + {name: "noname-ipv6", args: args{val: ":2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ValidateExtraHost(tt.args.val) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateExtraHost() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ValidateExtraHost() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_validateIPAddress(t *testing.T) { + type args struct { + val string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {name: "ipv4-good", args: args{val: "192.168.1.1"}, want: "192.168.1.1", wantErr: false}, + {name: "ipv4-bad", args: args{val: "192.168.1.1.1"}, want: "", wantErr: true}, + {name: "ipv4-bad", args: args{val: "192."}, want: "", wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateIPAddress(tt.args.val) + if (err != nil) != tt.wantErr { + t.Errorf("validateIPAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("validateIPAddress() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValidateFileName(t *testing.T) { + type args struct { + filename string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {name: "good", args: args{filename: "/some/rand/path"}, wantErr: false}, + {name: "good", args: args{filename: "some/rand/path"}, wantErr: false}, + {name: "good", args: args{filename: "/"}, wantErr: false}, + {name: "bad", args: args{filename: "/:"}, wantErr: true}, + {name: "bad", args: args{filename: ":/"}, wantErr: true}, + {name: "bad", args: args{filename: "/some/rand:/path"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateFileName(tt.args.filename); (err != nil) != tt.wantErr { + t.Errorf("ValidateFileName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAllLabels(t *testing.T) { + fileLabels := []string{} + labels, _ := GetAllLabels(fileLabels, Var1) + assert.Equal(t, len(labels), 2) +} + +func TestGetAllLabelsBadKeyValue(t *testing.T) { + inLabels := []string{"=badValue", "="} + fileLabels := []string{} + _, err := GetAllLabels(fileLabels, inLabels) + assert.Error(t, err, assert.AnError) +} + +func TestGetAllLabelsBadLabelFile(t *testing.T) { + fileLabels := []string{"/foobar5001/be"} + _, err := GetAllLabels(fileLabels, Var1) + assert.Error(t, err, assert.AnError) +} + +func TestGetAllLabelsFile(t *testing.T) { + content := []byte("THREE=3") + tFile, err := createTmpFile(content) + defer os.Remove(tFile) + assert.NoError(t, err) + fileLabels := []string{tFile} + result, _ := GetAllLabels(fileLabels, Var1) + assert.Equal(t, len(result), 3) +} diff --git a/cmd/podmanV2/parse/parse.go b/cmd/podmanV2/parse/parse.go deleted file mode 100644 index 10d2146fa..000000000 --- a/cmd/podmanV2/parse/parse.go +++ /dev/null @@ -1,233 +0,0 @@ -//nolint -// most of these validate and parse functions have been taken from projectatomic/docker -// and modified for cri-o -package parse - -import ( - "bufio" - "fmt" - "net" - "net/url" - "os" - "regexp" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -const ( - Protocol_TCP Protocol = 0 - Protocol_UDP Protocol = 1 -) - -type Protocol int32 - -// PortMapping specifies the port mapping configurations of a sandbox. -type PortMapping struct { - // Protocol of the port mapping. - Protocol Protocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=runtime.Protocol" json:"protocol,omitempty"` - // Port number within the container. Default: 0 (not specified). - ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"` - // Port number on the host. Default: 0 (not specified). - HostPort int32 `protobuf:"varint,3,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"` - // Host IP. - HostIp string `protobuf:"bytes,4,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"` -} - -// Note: for flags that are in the form , use the RAMInBytes function -// from the units package in docker/go-units/size.go - -var ( - whiteSpaces = " \t" - alphaRegexp = regexp.MustCompile(`[a-zA-Z]`) - domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`) -) - -// validateExtraHost validates that the specified string is a valid extrahost and returns it. -// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6). -// for add-host flag -func ValidateExtraHost(val string) (string, error) { //nolint - // allow for IPv6 addresses in extra hosts by only splitting on first ":" - arr := strings.SplitN(val, ":", 2) - if len(arr) != 2 || len(arr[0]) == 0 { - return "", fmt.Errorf("bad format for add-host: %q", val) - } - if _, err := validateIPAddress(arr[1]); err != nil { - return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1]) - } - return val, nil -} - -// validateIPAddress validates an Ip address. -// for dns, ip, and ip6 flags also -func validateIPAddress(val string) (string, error) { - var ip = net.ParseIP(strings.TrimSpace(val)) - if ip != nil { - return ip.String(), nil - } - return "", fmt.Errorf("%s is not an ip address", val) -} - -func ValidateDomain(val string) (string, error) { - if alphaRegexp.FindString(val) == "" { - return "", fmt.Errorf("%s is not a valid domain", val) - } - ns := domainRegexp.FindSubmatch([]byte(val)) - if len(ns) > 0 && len(ns[1]) < 255 { - return string(ns[1]), nil - } - return "", fmt.Errorf("%s is not a valid domain", val) -} - -// GetAllLabels retrieves all labels given a potential label file and a number -// of labels provided from the command line. -func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) { - labels := make(map[string]string) - for _, file := range labelFile { - // Use of parseEnvFile still seems safe, as it's missing the - // extra parsing logic of parseEnv. - // There's an argument that we SHOULD be doing that parsing for - // all environment variables, even those sourced from files, but - // that would require a substantial rework. - if err := parseEnvFile(labels, file); err != nil { - // FIXME: parseEnvFile is using parseEnv, so we need to add extra - // logic for labels. - return nil, err - } - } - for _, label := range inputLabels { - split := strings.SplitN(label, "=", 2) - if split[0] == "" { - return nil, errors.Errorf("invalid label format: %q", label) - } - value := "" - if len(split) > 1 { - value = split[1] - } - labels[split[0]] = value - } - return labels, nil -} - -func parseEnv(env map[string]string, line string) error { - data := strings.SplitN(line, "=", 2) - - // catch invalid variables such as "=" or "=A" - if data[0] == "" { - return errors.Errorf("invalid environment variable: %q", line) - } - - // trim the front of a variable, but nothing else - name := strings.TrimLeft(data[0], whiteSpaces) - if strings.ContainsAny(name, whiteSpaces) { - return errors.Errorf("name %q has white spaces, poorly formatted name", name) - } - - if len(data) > 1 { - env[name] = data[1] - } else { - if strings.HasSuffix(name, "*") { - name = strings.TrimSuffix(name, "*") - for _, e := range os.Environ() { - part := strings.SplitN(e, "=", 2) - if len(part) < 2 { - continue - } - if strings.HasPrefix(part[0], name) { - env[part[0]] = part[1] - } - } - } else { - // if only a pass-through variable is given, clean it up. - if val, ok := os.LookupEnv(name); ok { - env[name] = val - } - } - } - return nil -} - -// parseEnvFile reads a file with environment variables enumerated by lines -func parseEnvFile(env map[string]string, filename string) error { - fh, err := os.Open(filename) - if err != nil { - return err - } - defer fh.Close() - - scanner := bufio.NewScanner(fh) - for scanner.Scan() { - // trim the line from all leading whitespace first - line := strings.TrimLeft(scanner.Text(), whiteSpaces) - // line is not empty, and not starting with '#' - if len(line) > 0 && !strings.HasPrefix(line, "#") { - if err := parseEnv(env, line); err != nil { - return err - } - } - } - return scanner.Err() -} - -// ValidateFileName returns an error if filename contains ":" -// as it is currently not supported -func ValidateFileName(filename string) error { - if strings.Contains(filename, ":") { - return errors.Errorf("invalid filename (should not contain ':') %q", filename) - } - return nil -} - -// ValidURL checks a string urlStr is a url or not -func ValidURL(urlStr string) error { - _, err := url.ParseRequestURI(urlStr) - if err != nil { - return errors.Wrapf(err, "invalid url path: %q", urlStr) - } - return nil -} - -// checkAllLatestAndCIDFile checks that --all and --latest are used correctly. -// If cidfile is set, also check for the --cidfile flag. -func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { - argLen := len(args) - if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { - if !cidfile { - return errors.New("unable to lookup values for 'latest' or 'all'") - } else if c.Flags().Lookup("cidfile") == nil { - return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") - } - } - - specifiedAll, _ := c.Flags().GetBool("all") - specifiedLatest, _ := c.Flags().GetBool("latest") - specifiedCIDFile := false - if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { - specifiedCIDFile = true - } - - if specifiedCIDFile && (specifiedAll || specifiedLatest) { - return errors.Errorf("--all, --latest and --cidfile cannot be used together") - } else if specifiedAll && specifiedLatest { - return errors.Errorf("--all and --latest cannot be used together") - } - - if ignoreArgLen { - return nil - } - if (argLen > 0) && (specifiedAll || specifiedLatest) { - return errors.Errorf("no arguments are needed with --all or --latest") - } else if cidfile && (argLen > 0) && (specifiedAll || specifiedLatest || specifiedCIDFile) { - return errors.Errorf("no arguments are needed with --all, --latest or --cidfile") - } - - if specifiedCIDFile { - return nil - } - - if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { - return errors.Errorf("you must provide at least one name or id") - } - return nil -} diff --git a/cmd/podmanV2/parse/parse_test.go b/cmd/podmanV2/parse/parse_test.go deleted file mode 100644 index a6ddc2be9..000000000 --- a/cmd/podmanV2/parse/parse_test.go +++ /dev/null @@ -1,152 +0,0 @@ -//nolint -// most of these validate and parse functions have been taken from projectatomic/docker -// and modified for cri-o -package parse - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - Var1 = []string{"ONE=1", "TWO=2"} -) - -func createTmpFile(content []byte) (string, error) { - tmpfile, err := ioutil.TempFile(os.TempDir(), "unittest") - if err != nil { - return "", err - } - - if _, err := tmpfile.Write(content); err != nil { - return "", err - - } - if err := tmpfile.Close(); err != nil { - return "", err - } - return tmpfile.Name(), nil -} - -func TestValidateExtraHost(t *testing.T) { - type args struct { - val string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - //2001:0db8:85a3:0000:0000:8a2e:0370:7334 - {name: "good-ipv4", args: args{val: "foobar:192.168.1.1"}, want: "foobar:192.168.1.1", wantErr: false}, - {name: "bad-ipv4", args: args{val: "foobar:999.999.999.99"}, want: "", wantErr: true}, - {name: "bad-ipv4", args: args{val: "foobar:999.999.999"}, want: "", wantErr: true}, - {name: "noname-ipv4", args: args{val: "192.168.1.1"}, want: "", wantErr: true}, - {name: "noname-ipv4", args: args{val: ":192.168.1.1"}, want: "", wantErr: true}, - {name: "noip", args: args{val: "foobar:"}, want: "", wantErr: true}, - {name: "noip", args: args{val: "foobar"}, want: "", wantErr: true}, - {name: "good-ipv6", args: args{val: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334", wantErr: false}, - {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, - {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334.0000.0000.000"}, want: "", wantErr: true}, - {name: "noname-ipv6", args: args{val: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, - {name: "noname-ipv6", args: args{val: ":2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ValidateExtraHost(tt.args.val) - if (err != nil) != tt.wantErr { - t.Errorf("ValidateExtraHost() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("ValidateExtraHost() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_validateIPAddress(t *testing.T) { - type args struct { - val string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - {name: "ipv4-good", args: args{val: "192.168.1.1"}, want: "192.168.1.1", wantErr: false}, - {name: "ipv4-bad", args: args{val: "192.168.1.1.1"}, want: "", wantErr: true}, - {name: "ipv4-bad", args: args{val: "192."}, want: "", wantErr: true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := validateIPAddress(tt.args.val) - if (err != nil) != tt.wantErr { - t.Errorf("validateIPAddress() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("validateIPAddress() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestValidateFileName(t *testing.T) { - type args struct { - filename string - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "good", args: args{filename: "/some/rand/path"}, wantErr: false}, - {name: "good", args: args{filename: "some/rand/path"}, wantErr: false}, - {name: "good", args: args{filename: "/"}, wantErr: false}, - {name: "bad", args: args{filename: "/:"}, wantErr: true}, - {name: "bad", args: args{filename: ":/"}, wantErr: true}, - {name: "bad", args: args{filename: "/some/rand:/path"}, wantErr: true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidateFileName(tt.args.filename); (err != nil) != tt.wantErr { - t.Errorf("ValidateFileName() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestGetAllLabels(t *testing.T) { - fileLabels := []string{} - labels, _ := GetAllLabels(fileLabels, Var1) - assert.Equal(t, len(labels), 2) -} - -func TestGetAllLabelsBadKeyValue(t *testing.T) { - inLabels := []string{"=badValue", "="} - fileLabels := []string{} - _, err := GetAllLabels(fileLabels, inLabels) - assert.Error(t, err, assert.AnError) -} - -func TestGetAllLabelsBadLabelFile(t *testing.T) { - fileLabels := []string{"/foobar5001/be"} - _, err := GetAllLabels(fileLabels, Var1) - assert.Error(t, err, assert.AnError) -} - -func TestGetAllLabelsFile(t *testing.T) { - content := []byte("THREE=3") - tFile, err := createTmpFile(content) - defer os.Remove(tFile) - assert.NoError(t, err) - fileLabels := []string{tFile} - result, _ := GetAllLabels(fileLabels, Var1) - assert.Equal(t, len(result), 3) -} diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go index ebca41151..38a341a89 100644 --- a/pkg/api/handlers/libpod/containers_create.go +++ b/pkg/api/handlers/libpod/containers_create.go @@ -7,6 +7,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/specgen/generate" "github.com/pkg/errors" ) @@ -19,7 +20,11 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) return } - ctr, err := sg.MakeContainer(runtime) + if err := generate.CompleteSpec(r.Context(), runtime, &sg); err != nil { + utils.InternalServerError(w, err) + return + } + ctr, err := generate.MakeContainer(runtime, &sg) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 45dea98bf..74b23cd71 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -153,3 +153,7 @@ type RestoreReport struct { Err error Id string } + +type ContainerCreateReport struct { + Id string +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 3aaa7136f..025da50f3 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -4,12 +4,14 @@ import ( "context" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/specgen" ) type ContainerEngine interface { ContainerCommit(ctx context.Context, nameOrId string, options CommitOptions) (*CommitReport, error) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) ContainerRestore(ctx context.Context, namesOrIds []string, options RestoreOptions) ([]*RestoreReport, error) + ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]*ContainerInspectReport, error) ContainerExport(ctx context.Context, nameOrId string, options ContainerExportOptions) error diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 3c38b2093..d3d51db82 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -16,6 +16,8 @@ import ( "github.com/containers/libpod/pkg/checkpoint" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/signal" + "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/specgen/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -442,3 +444,14 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st } return reports, nil } + +func (ic *ContainerEngine) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*entities.ContainerCreateReport, error) { + if err := generate.CompleteSpec(ctx, ic.Libpod, s); err != nil { + return nil, err + } + ctr, err := generate.MakeContainer(ic.Libpod, s) + if err != nil { + return nil, err + } + return &entities.ContainerCreateReport{Id: ctr.ID()}, nil +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 5832d41be..ae8994cba 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -10,6 +10,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/specgen" "github.com/pkg/errors" ) @@ -296,3 +297,11 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st } return reports, nil } + +func (ic *ContainerEngine) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*entities.ContainerCreateReport, error) { + response, err := containers.CreateWithSpec(ic.ClientCxt, s) + if err != nil { + return nil, err + } + return &entities.ContainerCreateReport{Id: response.ID}, nil +} diff --git a/pkg/specgen/config_linux.go b/pkg/specgen/config_linux.go new file mode 100644 index 000000000..82a371492 --- /dev/null +++ b/pkg/specgen/config_linux.go @@ -0,0 +1,93 @@ +package specgen + +//func createBlockIO() (*spec.LinuxBlockIO, error) { +// var ret *spec.LinuxBlockIO +// bio := &spec.LinuxBlockIO{} +// if c.Resources.BlkioWeight > 0 { +// ret = bio +// bio.Weight = &c.Resources.BlkioWeight +// } +// if len(c.Resources.BlkioWeightDevice) > 0 { +// var lwds []spec.LinuxWeightDevice +// ret = bio +// for _, i := range c.Resources.BlkioWeightDevice { +// wd, err := ValidateweightDevice(i) +// if err != nil { +// return ret, errors.Wrapf(err, "invalid values for blkio-weight-device") +// } +// wdStat, err := GetStatFromPath(wd.Path) +// if err != nil { +// return ret, errors.Wrapf(err, "error getting stat from path %q", wd.Path) +// } +// lwd := spec.LinuxWeightDevice{ +// Weight: &wd.Weight, +// } +// lwd.Major = int64(unix.Major(wdStat.Rdev)) +// lwd.Minor = int64(unix.Minor(wdStat.Rdev)) +// lwds = append(lwds, lwd) +// } +// bio.WeightDevice = lwds +// } +// if len(c.Resources.DeviceReadBps) > 0 { +// ret = bio +// readBps, err := makeThrottleArray(c.Resources.DeviceReadBps, bps) +// if err != nil { +// return ret, err +// } +// bio.ThrottleReadBpsDevice = readBps +// } +// if len(c.Resources.DeviceWriteBps) > 0 { +// ret = bio +// writeBpds, err := makeThrottleArray(c.Resources.DeviceWriteBps, bps) +// if err != nil { +// return ret, err +// } +// bio.ThrottleWriteBpsDevice = writeBpds +// } +// if len(c.Resources.DeviceReadIOps) > 0 { +// ret = bio +// readIOps, err := makeThrottleArray(c.Resources.DeviceReadIOps, iops) +// if err != nil { +// return ret, err +// } +// bio.ThrottleReadIOPSDevice = readIOps +// } +// if len(c.Resources.DeviceWriteIOps) > 0 { +// ret = bio +// writeIOps, err := makeThrottleArray(c.Resources.DeviceWriteIOps, iops) +// if err != nil { +// return ret, err +// } +// bio.ThrottleWriteIOPSDevice = writeIOps +// } +// return ret, nil +//} + +//func makeThrottleArray(throttleInput []string, rateType int) ([]spec.LinuxThrottleDevice, error) { +// var ( +// ltds []spec.LinuxThrottleDevice +// t *throttleDevice +// err error +// ) +// for _, i := range throttleInput { +// if rateType == bps { +// t, err = validateBpsDevice(i) +// } else { +// t, err = validateIOpsDevice(i) +// } +// if err != nil { +// return []spec.LinuxThrottleDevice{}, err +// } +// ltdStat, err := GetStatFromPath(t.path) +// if err != nil { +// return ltds, errors.Wrapf(err, "error getting stat from path %q", t.path) +// } +// ltd := spec.LinuxThrottleDevice{ +// Rate: t.rate, +// } +// ltd.Major = int64(unix.Major(ltdStat.Rdev)) +// ltd.Minor = int64(unix.Minor(ltdStat.Rdev)) +// ltds = append(ltds, ltd) +// } +// return ltds, nil +//} diff --git a/pkg/specgen/config_linux_cgo.go b/pkg/specgen/config_linux_cgo.go index 6f547a40d..ef6c6e951 100644 --- a/pkg/specgen/config_linux_cgo.go +++ b/pkg/specgen/config_linux_cgo.go @@ -17,7 +17,6 @@ import ( func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec, img *image.Image) (*spec.LinuxSeccomp, error) { var seccompConfig *spec.LinuxSeccomp var err error - scp, err := seccomp.LookupPolicy(s.SeccompPolicy) if err != nil { return nil, err diff --git a/pkg/specgen/container_create.go b/pkg/specgen/container_create.go deleted file mode 100644 index b4039bb91..000000000 --- a/pkg/specgen/container_create.go +++ /dev/null @@ -1,189 +0,0 @@ -package specgen - -import ( - "context" - "os" - - "github.com/containers/common/pkg/config" - "github.com/containers/libpod/libpod" - "github.com/containers/libpod/libpod/define" - "github.com/containers/storage" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// MakeContainer creates a container based on the SpecGenerator -func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, error) { - if err := s.validate(); err != nil { - return nil, errors.Wrap(err, "invalid config provided") - } - rtc, err := rt.GetConfig() - if err != nil { - return nil, err - } - - options, err := s.createContainerOptions(rt) - if err != nil { - return nil, err - } - - podmanPath, err := os.Executable() - if err != nil { - return nil, err - } - options = append(options, s.createExitCommandOption(rt.StorageConfig(), rtc, podmanPath)) - newImage, err := rt.ImageRuntime().NewFromLocal(s.Image) - if err != nil { - return nil, err - } - - options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) - - runtimeSpec, err := s.toOCISpec(rt, newImage) - if err != nil { - return nil, err - } - return rt.NewContainer(context.Background(), runtimeSpec, options...) -} - -func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { - var options []libpod.CtrCreateOption - var err error - - if s.Stdin { - options = append(options, libpod.WithStdin()) - } - if len(s.Systemd) > 0 { - options = append(options, libpod.WithSystemd()) - } - if len(s.Name) > 0 { - logrus.Debugf("setting container name %s", s.Name) - options = append(options, libpod.WithName(s.Name)) - } - if s.Pod != "" { - pod, err := rt.LookupPod(s.Pod) - if err != nil { - return nil, err - } - logrus.Debugf("adding container to pod %s", s.Pod) - options = append(options, rt.WithPod(pod)) - } - destinations := []string{} - // // Take all mount and named volume destinations. - for _, mount := range s.Mounts { - destinations = append(destinations, mount.Destination) - } - for _, volume := range s.Volumes { - destinations = append(destinations, volume.Dest) - } - options = append(options, libpod.WithUserVolumes(destinations)) - - if len(s.Volumes) != 0 { - options = append(options, libpod.WithNamedVolumes(s.Volumes)) - } - - if len(s.Command) != 0 { - options = append(options, libpod.WithCommand(s.Command)) - } - - options = append(options, libpod.WithEntrypoint(s.Entrypoint)) - if s.StopSignal != nil { - options = append(options, libpod.WithStopSignal(*s.StopSignal)) - } - if s.StopTimeout != nil { - options = append(options, libpod.WithStopTimeout(*s.StopTimeout)) - } - if s.LogConfiguration != nil { - if len(s.LogConfiguration.Path) > 0 { - options = append(options, libpod.WithLogPath(s.LogConfiguration.Path)) - } - if len(s.LogConfiguration.Options) > 0 && s.LogConfiguration.Options["tag"] != "" { - // Note: I'm really guessing here. - options = append(options, libpod.WithLogTag(s.LogConfiguration.Options["tag"])) - } - - if len(s.LogConfiguration.Driver) > 0 { - options = append(options, libpod.WithLogDriver(s.LogConfiguration.Driver)) - } - } - - // Security options - if len(s.SelinuxOpts) > 0 { - options = append(options, libpod.WithSecLabels(s.SelinuxOpts)) - } - options = append(options, libpod.WithPrivileged(s.Privileged)) - - // Get namespace related options - namespaceOptions, err := s.generateNamespaceContainerOpts(rt) - if err != nil { - return nil, err - } - options = append(options, namespaceOptions...) - - if len(s.ConmonPidFile) > 0 { - options = append(options, libpod.WithConmonPidFile(s.ConmonPidFile)) - } - options = append(options, libpod.WithLabels(s.Labels)) - if s.ShmSize != nil { - options = append(options, libpod.WithShmSize(*s.ShmSize)) - } - if s.Rootfs != "" { - options = append(options, libpod.WithRootFS(s.Rootfs)) - } - // Default used if not overridden on command line - - if s.RestartPolicy != "" { - if s.RestartPolicy == "unless-stopped" { - return nil, errors.Wrapf(define.ErrInvalidArg, "the unless-stopped restart policy is not supported") - } - if s.RestartRetries != nil { - options = append(options, libpod.WithRestartRetries(*s.RestartRetries)) - } - options = append(options, libpod.WithRestartPolicy(s.RestartPolicy)) - } - - if s.ContainerHealthCheckConfig.HealthConfig != nil { - options = append(options, libpod.WithHealthCheck(s.ContainerHealthCheckConfig.HealthConfig)) - logrus.Debugf("New container has a health check") - } - return options, nil -} - -func (s *SpecGenerator) createExitCommandOption(storageConfig storage.StoreOptions, config *config.Config, podmanPath string) libpod.CtrCreateOption { - // We need a cleanup process for containers in the current model. - // But we can't assume that the caller is Podman - it could be another - // user of the API. - // As such, provide a way to specify a path to Podman, so we can - // still invoke a cleanup process. - - command := []string{podmanPath, - "--root", storageConfig.GraphRoot, - "--runroot", storageConfig.RunRoot, - "--log-level", logrus.GetLevel().String(), - "--cgroup-manager", config.Engine.CgroupManager, - "--tmpdir", config.Engine.TmpDir, - } - if config.Engine.OCIRuntime != "" { - command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) - } - if storageConfig.GraphDriverName != "" { - command = append(command, []string{"--storage-driver", storageConfig.GraphDriverName}...) - } - for _, opt := range storageConfig.GraphDriverOptions { - command = append(command, []string{"--storage-opt", opt}...) - } - if config.Engine.EventsLogger != "" { - command = append(command, []string{"--events-backend", config.Engine.EventsLogger}...) - } - - // TODO Mheon wants to leave this for now - //if s.sys { - // command = append(command, "--syslog", "true") - //} - command = append(command, []string{"container", "cleanup"}...) - - if s.Remove { - command = append(command, "--rm") - } - return libpod.WithExitCommand(command) -} diff --git a/pkg/specgen/container_validate.go b/pkg/specgen/container_validate.go index b27659f5f..aad14ddcb 100644 --- a/pkg/specgen/container_validate.go +++ b/pkg/specgen/container_validate.go @@ -14,7 +14,7 @@ var ( // SystemDValues describes the only values that SystemD can be SystemDValues = []string{"true", "false", "always"} // ImageVolumeModeValues describes the only values that ImageVolumeMode can be - ImageVolumeModeValues = []string{"ignore", "tmpfs", "anonymous"} + ImageVolumeModeValues = []string{"ignore", "tmpfs", "bind"} ) func exclusiveOptions(opt1, opt2 string) error { @@ -23,7 +23,7 @@ func exclusiveOptions(opt1, opt2 string) error { // Validate verifies that the given SpecGenerator is valid and satisfies required // input for creating a container. -func (s *SpecGenerator) validate() error { +func (s *SpecGenerator) Validate() error { // // ContainerBasicConfig diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go new file mode 100644 index 000000000..78c77fec1 --- /dev/null +++ b/pkg/specgen/generate/container.go @@ -0,0 +1,168 @@ +package generate + +import ( + "context" + + "github.com/containers/libpod/libpod" + ann "github.com/containers/libpod/pkg/annotations" + envLib "github.com/containers/libpod/pkg/env" + "github.com/containers/libpod/pkg/signal" + "github.com/containers/libpod/pkg/specgen" + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerator) error { + + newImage, err := r.ImageRuntime().NewFromLocal(s.Image) + if err != nil { + return err + } + + // Image stop signal + if s.StopSignal == nil && newImage.Config != nil { + sig, err := signal.ParseSignalNameOrNumber(newImage.Config.StopSignal) + if err != nil { + return err + } + s.StopSignal = &sig + } + // Image envs from the image if they don't exist + // already + if newImage.Config != nil && len(newImage.Config.Env) > 0 { + envs, err := envLib.ParseSlice(newImage.Config.Env) + if err != nil { + return err + } + for k, v := range envs { + if _, exists := s.Env[k]; !exists { + s.Env[v] = k + } + } + } + + // labels from the image that dont exist already + if config := newImage.Config; config != nil { + for k, v := range config.Labels { + if _, exists := s.Labels[k]; !exists { + s.Labels[k] = v + } + } + } + + // annotations + // in the event this container is in a pod, and the pod has an infra container + // we will want to configure it as a type "container" instead defaulting to + // the behavior of a "sandbox" container + // In Kata containers: + // - "sandbox" is the annotation that denotes the container should use its own + // VM, which is the default behavior + // - "container" denotes the container should join the VM of the SandboxID + // (the infra container) + s.Annotations = make(map[string]string) + if len(s.Pod) > 0 { + s.Annotations[ann.SandboxID] = s.Pod + s.Annotations[ann.ContainerType] = ann.ContainerTypeContainer + } + // + // Next, add annotations from the image + annotations, err := newImage.Annotations(ctx) + if err != nil { + return err + } + for k, v := range annotations { + annotations[k] = v + } + + // entrypoint + if config := newImage.Config; config != nil { + if len(s.Entrypoint) < 1 && len(config.Entrypoint) > 0 { + s.Entrypoint = config.Entrypoint + } + if len(s.Command) < 1 && len(config.Cmd) > 0 { + s.Command = config.Cmd + } + if len(s.Command) < 1 && len(s.Entrypoint) < 1 { + return errors.Errorf("No command provided or as CMD or ENTRYPOINT in this image") + } + // workdir + if len(s.WorkDir) < 1 && len(config.WorkingDir) > 1 { + s.WorkDir = config.WorkingDir + } + } + + if len(s.SeccompProfilePath) < 1 { + p, err := libpod.DefaultSeccompPath() + if err != nil { + return err + } + s.SeccompProfilePath = p + } + + if user := s.User; len(user) == 0 { + switch { + // TODO This should be enabled when namespaces actually work + //case usernsMode.IsKeepID(): + // user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID()) + case newImage.Config == nil || (newImage.Config != nil && len(newImage.Config.User) == 0): + s.User = "0" + default: + s.User = newImage.Config.User + } + } + if err := finishThrottleDevices(s); err != nil { + return err + } + return nil +} + +// finishThrottleDevices takes the temporary representation of the throttle +// devices in the specgen and looks up the major and major minors. it then +// sets the throttle devices proper in the specgen +func finishThrottleDevices(s *specgen.SpecGenerator) error { + if bps := s.ThrottleReadBpsDevice; len(bps) > 0 { + for k, v := range bps { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return err + } + v.Major = (int64(unix.Major(statT.Rdev))) + v.Minor = (int64(unix.Minor(statT.Rdev))) + s.ResourceLimits.BlockIO.ThrottleReadBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleReadBpsDevice, v) + } + } + if bps := s.ThrottleWriteBpsDevice; len(bps) > 0 { + for k, v := range bps { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return err + } + v.Major = (int64(unix.Major(statT.Rdev))) + v.Minor = (int64(unix.Minor(statT.Rdev))) + s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteBpsDevice, v) + } + } + if iops := s.ThrottleReadIOPSDevice; len(iops) > 0 { + for k, v := range iops { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return err + } + v.Major = (int64(unix.Major(statT.Rdev))) + v.Minor = (int64(unix.Minor(statT.Rdev))) + s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleReadIOPSDevice, v) + } + } + if iops := s.ThrottleWriteBpsDevice; len(iops) > 0 { + for k, v := range iops { + statT := unix.Stat_t{} + if err := unix.Stat(k, &statT); err != nil { + return err + } + v.Major = (int64(unix.Major(statT.Rdev))) + v.Minor = (int64(unix.Minor(statT.Rdev))) + s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice = append(s.ResourceLimits.BlockIO.ThrottleWriteIOPSDevice, v) + } + } + return nil +} diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go new file mode 100644 index 000000000..aad59a861 --- /dev/null +++ b/pkg/specgen/generate/container_create.go @@ -0,0 +1,190 @@ +package generate + +import ( + "context" + "os" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/specgen" + "github.com/containers/storage" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// MakeContainer creates a container based on the SpecGenerator +func MakeContainer(rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Container, error) { + if err := s.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid config provided") + } + rtc, err := rt.GetConfig() + if err != nil { + return nil, err + } + + options, err := createContainerOptions(rt, s) + if err != nil { + return nil, err + } + + podmanPath, err := os.Executable() + if err != nil { + return nil, err + } + options = append(options, createExitCommandOption(s, rt.StorageConfig(), rtc, podmanPath)) + newImage, err := rt.ImageRuntime().NewFromLocal(s.Image) + if err != nil { + return nil, err + } + + options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) + + runtimeSpec, err := s.ToOCISpec(rt, newImage) + if err != nil { + return nil, err + } + return rt.NewContainer(context.Background(), runtimeSpec, options...) +} + +func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator) ([]libpod.CtrCreateOption, error) { + var options []libpod.CtrCreateOption + var err error + + if s.Stdin { + options = append(options, libpod.WithStdin()) + } + if len(s.Systemd) > 0 { + options = append(options, libpod.WithSystemd()) + } + if len(s.Name) > 0 { + logrus.Debugf("setting container name %s", s.Name) + options = append(options, libpod.WithName(s.Name)) + } + if s.Pod != "" { + pod, err := rt.LookupPod(s.Pod) + if err != nil { + return nil, err + } + logrus.Debugf("adding container to pod %s", s.Pod) + options = append(options, rt.WithPod(pod)) + } + destinations := []string{} + // // Take all mount and named volume destinations. + for _, mount := range s.Mounts { + destinations = append(destinations, mount.Destination) + } + for _, volume := range s.Volumes { + destinations = append(destinations, volume.Dest) + } + options = append(options, libpod.WithUserVolumes(destinations)) + + if len(s.Volumes) != 0 { + options = append(options, libpod.WithNamedVolumes(s.Volumes)) + } + + if len(s.Command) != 0 { + options = append(options, libpod.WithCommand(s.Command)) + } + + options = append(options, libpod.WithEntrypoint(s.Entrypoint)) + if s.StopSignal != nil { + options = append(options, libpod.WithStopSignal(*s.StopSignal)) + } + if s.StopTimeout != nil { + options = append(options, libpod.WithStopTimeout(*s.StopTimeout)) + } + if s.LogConfiguration != nil { + if len(s.LogConfiguration.Path) > 0 { + options = append(options, libpod.WithLogPath(s.LogConfiguration.Path)) + } + if len(s.LogConfiguration.Options) > 0 && s.LogConfiguration.Options["tag"] != "" { + // Note: I'm really guessing here. + options = append(options, libpod.WithLogTag(s.LogConfiguration.Options["tag"])) + } + + if len(s.LogConfiguration.Driver) > 0 { + options = append(options, libpod.WithLogDriver(s.LogConfiguration.Driver)) + } + } + + // Security options + if len(s.SelinuxOpts) > 0 { + options = append(options, libpod.WithSecLabels(s.SelinuxOpts)) + } + options = append(options, libpod.WithPrivileged(s.Privileged)) + + // Get namespace related options + namespaceOptions, err := s.GenerateNamespaceContainerOpts(rt) + if err != nil { + return nil, err + } + options = append(options, namespaceOptions...) + + if len(s.ConmonPidFile) > 0 { + options = append(options, libpod.WithConmonPidFile(s.ConmonPidFile)) + } + options = append(options, libpod.WithLabels(s.Labels)) + if s.ShmSize != nil { + options = append(options, libpod.WithShmSize(*s.ShmSize)) + } + if s.Rootfs != "" { + options = append(options, libpod.WithRootFS(s.Rootfs)) + } + // Default used if not overridden on command line + + if s.RestartPolicy != "" { + if s.RestartPolicy == "unless-stopped" { + return nil, errors.Wrapf(define.ErrInvalidArg, "the unless-stopped restart policy is not supported") + } + if s.RestartRetries != nil { + options = append(options, libpod.WithRestartRetries(*s.RestartRetries)) + } + options = append(options, libpod.WithRestartPolicy(s.RestartPolicy)) + } + + if s.ContainerHealthCheckConfig.HealthConfig != nil { + options = append(options, libpod.WithHealthCheck(s.ContainerHealthCheckConfig.HealthConfig)) + logrus.Debugf("New container has a health check") + } + return options, nil +} + +func createExitCommandOption(s *specgen.SpecGenerator, storageConfig storage.StoreOptions, config *config.Config, podmanPath string) libpod.CtrCreateOption { + // We need a cleanup process for containers in the current model. + // But we can't assume that the caller is Podman - it could be another + // user of the API. + // As such, provide a way to specify a path to Podman, so we can + // still invoke a cleanup process. + + command := []string{podmanPath, + "--root", storageConfig.GraphRoot, + "--runroot", storageConfig.RunRoot, + "--log-level", logrus.GetLevel().String(), + "--cgroup-manager", config.Engine.CgroupManager, + "--tmpdir", config.Engine.TmpDir, + } + if config.Engine.OCIRuntime != "" { + command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) + } + if storageConfig.GraphDriverName != "" { + command = append(command, []string{"--storage-driver", storageConfig.GraphDriverName}...) + } + for _, opt := range storageConfig.GraphDriverOptions { + command = append(command, []string{"--storage-opt", opt}...) + } + if config.Engine.EventsLogger != "" { + command = append(command, []string{"--events-backend", config.Engine.EventsLogger}...) + } + + // TODO Mheon wants to leave this for now + //if s.sys { + // command = append(command, "--syslog", "true") + //} + command = append(command, []string{"container", "cleanup"}...) + + if s.Remove { + command = append(command, "--rm") + } + return libpod.WithExitCommand(command) +} diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index fa2dee77d..2a7bb3495 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -16,6 +16,9 @@ import ( type NamespaceMode string const ( + // Default indicates the spec generator should determine + // a sane default + Default NamespaceMode = "default" // Host means the the namespace is derived from // the host Host NamespaceMode = "host" @@ -83,7 +86,7 @@ func validateNetNS(n *Namespace) error { return nil } -// validate perform simple validation on the namespace to make sure it is not +// Validate perform simple validation on the namespace to make sure it is not // invalid from the get-go func (n *Namespace) validate() error { if n == nil { @@ -103,7 +106,7 @@ func (n *Namespace) validate() error { return nil } -func (s *SpecGenerator) generateNamespaceContainerOpts(rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { +func (s *SpecGenerator) GenerateNamespaceContainerOpts(rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { var portBindings []ocicni.PortMapping options := make([]libpod.CtrCreateOption, 0) diff --git a/pkg/specgen/oci.go b/pkg/specgen/oci.go index db60dc25e..0756782b4 100644 --- a/pkg/specgen/oci.go +++ b/pkg/specgen/oci.go @@ -11,7 +11,7 @@ import ( "github.com/opencontainers/runtime-tools/generate" ) -func (s *SpecGenerator) toOCISpec(rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { +func (s *SpecGenerator) ToOCISpec(rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { var ( inUserNS bool ) diff --git a/pkg/specgen/security.go b/pkg/specgen/security.go new file mode 100644 index 000000000..158e4a7b3 --- /dev/null +++ b/pkg/specgen/security.go @@ -0,0 +1,165 @@ +package specgen + +// ToCreateOptions convert the SecurityConfig to a slice of container create +// options. +/* +func (c *SecurityConfig) ToCreateOptions() ([]libpod.CtrCreateOption, error) { + options := make([]libpod.CtrCreateOption, 0) + options = append(options, libpod.WithSecLabels(c.LabelOpts)) + options = append(options, libpod.WithPrivileged(c.Privileged)) + return options, nil +} +*/ + +// SetLabelOpts sets the label options of the SecurityConfig according to the +// input. +/* +func (c *SecurityConfig) SetLabelOpts(runtime *libpod.Runtime, pidConfig *PidConfig, ipcConfig *IpcConfig) error { + if c.Privileged { + c.LabelOpts = label.DisableSecOpt() + return nil + } + + var labelOpts []string + if pidConfig.PidMode.IsHost() { + labelOpts = append(labelOpts, label.DisableSecOpt()...) + } else if pidConfig.PidMode.IsContainer() { + ctr, err := runtime.LookupContainer(pidConfig.PidMode.Container()) + if err != nil { + return errors.Wrapf(err, "container %q not found", pidConfig.PidMode.Container()) + } + secopts, err := label.DupSecOpt(ctr.ProcessLabel()) + if err != nil { + return errors.Wrapf(err, "failed to duplicate label %q ", ctr.ProcessLabel()) + } + labelOpts = append(labelOpts, secopts...) + } + + if ipcConfig.IpcMode.IsHost() { + labelOpts = append(labelOpts, label.DisableSecOpt()...) + } else if ipcConfig.IpcMode.IsContainer() { + ctr, err := runtime.LookupContainer(ipcConfig.IpcMode.Container()) + if err != nil { + return errors.Wrapf(err, "container %q not found", ipcConfig.IpcMode.Container()) + } + secopts, err := label.DupSecOpt(ctr.ProcessLabel()) + if err != nil { + return errors.Wrapf(err, "failed to duplicate label %q ", ctr.ProcessLabel()) + } + labelOpts = append(labelOpts, secopts...) + } + + c.LabelOpts = append(c.LabelOpts, labelOpts...) + return nil +} +*/ + +// SetSecurityOpts the the security options (labels, apparmor, seccomp, etc.). +func SetSecurityOpts(securityOpts []string) error { + return nil +} + +// ConfigureGenerator configures the generator according to the input. +/* +func (c *SecurityConfig) ConfigureGenerator(g *generate.Generator, user *UserConfig) error { + // HANDLE CAPABILITIES + // NOTE: Must happen before SECCOMP + if c.Privileged { + g.SetupPrivileged(true) + } + + useNotRoot := func(user string) bool { + if user == "" || user == "root" || user == "0" { + return false + } + return true + } + + configSpec := g.Config + var err error + var defaultCaplist []string + bounding := configSpec.Process.Capabilities.Bounding + if useNotRoot(user.User) { + configSpec.Process.Capabilities.Bounding = defaultCaplist + } + defaultCaplist, err = capabilities.MergeCapabilities(configSpec.Process.Capabilities.Bounding, c.CapAdd, c.CapDrop) + if err != nil { + return err + } + + privCapRequired := []string{} + + if !c.Privileged && len(c.CapRequired) > 0 { + // Pass CapRequired in CapAdd field to normalize capabilities names + capRequired, err := capabilities.MergeCapabilities(nil, c.CapRequired, nil) + if err != nil { + logrus.Errorf("capabilities requested by user or image are not valid: %q", strings.Join(c.CapRequired, ",")) + } else { + // Verify all capRequiered are in the defaultCapList + for _, cap := range capRequired { + if !util.StringInSlice(cap, defaultCaplist) { + privCapRequired = append(privCapRequired, cap) + } + } + } + if len(privCapRequired) == 0 { + defaultCaplist = capRequired + } else { + logrus.Errorf("capabilities requested by user or image are not allowed by default: %q", strings.Join(privCapRequired, ",")) + } + } + configSpec.Process.Capabilities.Bounding = defaultCaplist + configSpec.Process.Capabilities.Permitted = defaultCaplist + configSpec.Process.Capabilities.Inheritable = defaultCaplist + configSpec.Process.Capabilities.Effective = defaultCaplist + configSpec.Process.Capabilities.Ambient = defaultCaplist + if useNotRoot(user.User) { + defaultCaplist, err = capabilities.MergeCapabilities(bounding, c.CapAdd, c.CapDrop) + if err != nil { + return err + } + } + configSpec.Process.Capabilities.Bounding = defaultCaplist + + // HANDLE SECCOMP + if c.SeccompProfilePath != "unconfined" { + seccompConfig, err := getSeccompConfig(c, configSpec) + if err != nil { + return err + } + configSpec.Linux.Seccomp = seccompConfig + } + + // Clear default Seccomp profile from Generator for privileged containers + if c.SeccompProfilePath == "unconfined" || c.Privileged { + configSpec.Linux.Seccomp = nil + } + + for _, opt := range c.SecurityOpts { + // Split on both : and = + splitOpt := strings.Split(opt, "=") + if len(splitOpt) == 1 { + splitOpt = strings.Split(opt, ":") + } + if len(splitOpt) < 2 { + continue + } + switch splitOpt[0] { + case "label": + configSpec.Annotations[libpod.InspectAnnotationLabel] = splitOpt[1] + case "seccomp": + configSpec.Annotations[libpod.InspectAnnotationSeccomp] = splitOpt[1] + case "apparmor": + configSpec.Annotations[libpod.InspectAnnotationApparmor] = splitOpt[1] + } + } + + g.SetRootReadonly(c.ReadOnlyRootfs) + for sysctlKey, sysctlVal := range c.Sysctl { + g.AddLinuxSysctl(sysctlKey, sysctlVal) + } + + return nil +} + +*/ diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 89c76c273..2e6dd9c1d 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -4,8 +4,9 @@ import ( "net" "syscall" - "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod" + + "github.com/containers/image/v5/manifest" "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage" "github.com/cri-o/ocicni/pkg/ocicni" @@ -371,6 +372,16 @@ type ContainerResourceConfig struct { // processes to kill for the container's process. // Optional. OOMScoreAdj *int `json:"oom_score_adj,omitempty"` + // Weight per cgroup per device, can override BlkioWeight + WeightDevice map[string]spec.LinuxWeightDevice `json:"weightDevice,omitempty"` + // IO read rate limit per cgroup per device, bytes per second + ThrottleReadBpsDevice map[string]spec.LinuxThrottleDevice `json:"throttleReadBpsDevice,omitempty"` + // IO write rate limit per cgroup per device, bytes per second + ThrottleWriteBpsDevice map[string]spec.LinuxThrottleDevice `json:"throttleWriteBpsDevice,omitempty"` + // IO read rate limit per cgroup per device, IO per second + ThrottleReadIOPSDevice map[string]spec.LinuxThrottleDevice `json:"throttleReadIOPSDevice,omitempty"` + // IO write rate limit per cgroup per device, IO per second + ThrottleWriteIOPSDevice map[string]spec.LinuxThrottleDevice `json:"throttleWriteIOPSDevice,omitempty"` } // ContainerHealthCheckConfig describes a container healthcheck with attributes diff --git a/pkg/specgen/storage.go b/pkg/specgen/storage.go new file mode 100644 index 000000000..1b903f608 --- /dev/null +++ b/pkg/specgen/storage.go @@ -0,0 +1,885 @@ +package specgen + +//nolint + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/containers/libpod/libpod" + + "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/pkg/util" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const ( + // TypeBind is the type for mounting host dir + TypeBind = "bind" + // TypeVolume is the type for named volumes + TypeVolume = "volume" + // TypeTmpfs is the type for mounting tmpfs + TypeTmpfs = "tmpfs" +) + +var ( + errDuplicateDest = errors.Errorf("duplicate mount destination") //nolint + optionArgError = errors.Errorf("must provide an argument for option") //nolint + noDestError = errors.Errorf("must set volume destination") //nolint +) + +// Parse all volume-related options in the create config into a set of mounts +// and named volumes to add to the container. +// Handles --volumes-from, --volumes, --tmpfs, --init, and --init-path flags. +// TODO: Named volume options - should we default to rprivate? It bakes into a +// bind mount under the hood... +// TODO: handle options parsing/processing via containers/storage/pkg/mount +func (s *SpecGenerator) parseVolumes(mounts, volMounts, tmpMounts []string) error { //nolint + + // TODO this needs to come from the image and erquires a runtime + + // Add image volumes. + //baseMounts, baseVolumes, err := config.getImageVolumes() + //if err != nil { + // return nil, nil, err + //} + + // Add --volumes-from. + // Overrides image volumes unconditionally. + //vFromMounts, vFromVolumes, err := config.getVolumesFrom(runtime) + //if err != nil { + // return nil, nil, err + //} + //for dest, mount := range vFromMounts { + // baseMounts[dest] = mount + //} + //for dest, volume := range vFromVolumes { + // baseVolumes[dest] = volume + //} + + // Next mounts from the --mounts flag. + // Do not override yet. + //unifiedMounts, _, err := getMounts(mounts) + //if err != nil { + // return err + //} + // + //// Next --volumes flag. + //// Do not override yet. + //volumeMounts, _ , err := getVolumeMounts(volMounts) + //if err != nil { + // return err + //} + // + //// Next --tmpfs flag. + //// Do not override yet. + //tmpfsMounts, err := getTmpfsMounts(tmpMounts) + //if err != nil { + // return err + //} + + //// Unify mounts from --mount, --volume, --tmpfs. + //// Also add mounts + volumes directly from createconfig. + //// Start with --volume. + //for dest, mount := range volumeMounts { + // if _, ok := unifiedMounts[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, dest) + // } + // unifiedMounts[dest] = mount + //} + //for dest, volume := range volumeVolumes { + // if _, ok := unifiedVolumes[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, dest) + // } + // unifiedVolumes[dest] = volume + //} + //// Now --tmpfs + //for dest, tmpfs := range tmpfsMounts { + // if _, ok := unifiedMounts[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, dest) + // } + // unifiedMounts[dest] = tmpfs + //} + //// Now spec mounts and volumes + //for _, mount := range config.Mounts { + // dest := mount.Destination + // if _, ok := unifiedMounts[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, dest) + // } + // unifiedMounts[dest] = mount + //} + //for _, volume := range config.NamedVolumes { + // dest := volume.Dest + // if _, ok := unifiedVolumes[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, dest) + // } + // unifiedVolumes[dest] = volume + //} + // + //// If requested, add container init binary + //if config.Init { + // initPath := config.InitPath + // if initPath == "" { + // rtc, err := runtime.GetConfig() + // if err != nil { + // return nil, nil, err + // } + // initPath = rtc.Engine.InitPath + // } + // initMount, err := config.addContainerInitBinary(initPath) + // if err != nil { + // return nil, nil, err + // } + // if _, ok := unifiedMounts[initMount.Destination]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, "conflict with mount added by --init to %q", initMount.Destination) + // } + // unifiedMounts[initMount.Destination] = initMount + //} + // + //// Before superseding, we need to find volume mounts which conflict with + //// named volumes, and vice versa. + //// We'll delete the conflicts here as we supersede. + //for dest := range unifiedMounts { + // if _, ok := baseVolumes[dest]; ok { + // delete(baseVolumes, dest) + // } + //} + //for dest := range unifiedVolumes { + // if _, ok := baseMounts[dest]; ok { + // delete(baseMounts, dest) + // } + //} + // + //// Supersede volumes-from/image volumes with unified volumes from above. + //// This is an unconditional replacement. + //for dest, mount := range unifiedMounts { + // baseMounts[dest] = mount + //} + //for dest, volume := range unifiedVolumes { + // baseVolumes[dest] = volume + //} + // + //// If requested, add tmpfs filesystems for read-only containers. + //if config.Security.ReadOnlyRootfs && config.Security.ReadOnlyTmpfs { + // readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"} + // options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"} + // for _, dest := range readonlyTmpfs { + // if _, ok := baseMounts[dest]; ok { + // continue + // } + // if _, ok := baseVolumes[dest]; ok { + // continue + // } + // localOpts := options + // if dest == "/run" { + // localOpts = append(localOpts, "noexec", "size=65536k") + // } else { + // localOpts = append(localOpts, "exec") + // } + // baseMounts[dest] = spec.Mount{ + // Destination: dest, + // Type: "tmpfs", + // Source: "tmpfs", + // Options: localOpts, + // } + // } + //} + // + //// Check for conflicts between named volumes and mounts + //for dest := range baseMounts { + // if _, ok := baseVolumes[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + // } + //} + //for dest := range baseVolumes { + // if _, ok := baseMounts[dest]; ok { + // return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + // } + //} + // + //// Final step: maps to arrays + //finalMounts := make([]spec.Mount, 0, len(baseMounts)) + //for _, mount := range baseMounts { + // if mount.Type == TypeBind { + // absSrc, err := filepath.Abs(mount.Source) + // if err != nil { + // return nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) + // } + // mount.Source = absSrc + // } + // finalMounts = append(finalMounts, mount) + //} + //finalVolumes := make([]*define.ContainerNamedVolume, 0, len(baseVolumes)) + //for _, volume := range baseVolumes { + // finalVolumes = append(finalVolumes, volume) + //} + + //return finalMounts, finalVolumes, nil + return nil +} + +// Parse volumes from - a set of containers whose volumes we will mount in. +// Grab the containers, retrieve any user-created spec mounts and all named +// volumes, and return a list of them. +// Conflicts are resolved simply - the last container specified wins. +// Container names may be suffixed by mount options after a colon. +// TODO: We should clean these paths if possible +// TODO deferred baude +func getVolumesFrom() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint + // Both of these are maps of mount destination to mount type. + // We ensure that each destination is only mounted to once in this way. + //finalMounts := make(map[string]spec.Mount) + //finalNamedVolumes := make(map[string]*define.ContainerNamedVolume) + // + //for _, vol := range config.VolumesFrom { + // var ( + // options = []string{} + // err error + // splitVol = strings.SplitN(vol, ":", 2) + // ) + // if len(splitVol) == 2 { + // splitOpts := strings.Split(splitVol[1], ",") + // for _, checkOpt := range splitOpts { + // switch checkOpt { + // case "z", "ro", "rw": + // // Do nothing, these are valid options + // default: + // return nil, nil, errors.Errorf("invalid options %q, can only specify 'ro', 'rw', and 'z'", splitVol[1]) + // } + // } + // + // if options, err = parse.ValidateVolumeOpts(splitOpts); err != nil { + // return nil, nil, err + // } + // } + // ctr, err := runtime.LookupContainer(splitVol[0]) + // if err != nil { + // return nil, nil, errors.Wrapf(err, "error looking up container %q for volumes-from", splitVol[0]) + // } + // + // logrus.Debugf("Adding volumes from container %s", ctr.ID()) + // + // // Look up the container's user volumes. This gets us the + // // destinations of all mounts the user added to the container. + // userVolumesArr := ctr.UserVolumes() + // + // // We're going to need to access them a lot, so convert to a map + // // to reduce looping. + // // We'll also use the map to indicate if we missed any volumes along the way. + // userVolumes := make(map[string]bool) + // for _, dest := range userVolumesArr { + // userVolumes[dest] = false + // } + // + // // Now we get the container's spec and loop through its volumes + // // and append them in if we can find them. + // spec := ctr.Spec() + // if spec == nil { + // return nil, nil, errors.Errorf("error retrieving container %s spec for volumes-from", ctr.ID()) + // } + // for _, mnt := range spec.Mounts { + // if mnt.Type != TypeBind { + // continue + // } + // if _, exists := userVolumes[mnt.Destination]; exists { + // userVolumes[mnt.Destination] = true + // + // if len(options) != 0 { + // mnt.Options = options + // } + // + // if _, ok := finalMounts[mnt.Destination]; ok { + // logrus.Debugf("Overriding mount to %s with new mount from container %s", mnt.Destination, ctr.ID()) + // } + // finalMounts[mnt.Destination] = mnt + // } + // } + // + // // We're done with the spec mounts. Add named volumes. + // // Add these unconditionally - none of them are automatically + // // part of the container, as some spec mounts are. + // namedVolumes := ctr.NamedVolumes() + // for _, namedVol := range namedVolumes { + // if _, exists := userVolumes[namedVol.Dest]; exists { + // userVolumes[namedVol.Dest] = true + // } + // + // if len(options) != 0 { + // namedVol.Options = options + // } + // + // if _, ok := finalMounts[namedVol.Dest]; ok { + // logrus.Debugf("Overriding named volume mount to %s with new named volume from container %s", namedVol.Dest, ctr.ID()) + // } + // finalNamedVolumes[namedVol.Dest] = namedVol + // } + // + // // Check if we missed any volumes + // for volDest, found := range userVolumes { + // if !found { + // logrus.Warnf("Unable to match volume %s from container %s for volumes-from", volDest, ctr.ID()) + // } + // } + //} + // + //return finalMounts, finalNamedVolumes, nil + return nil, nil, nil +} + +// getMounts takes user-provided input from the --mount flag and creates OCI +// spec mounts and Libpod named volumes. +// podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ... +// podman run --mount type=tmpfs,target=/dev/shm ... +// podman run --mount type=volume,source=test-volume, ... +func getMounts(mounts []string) (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint + finalMounts := make(map[string]spec.Mount) + finalNamedVolumes := make(map[string]*libpod.ContainerNamedVolume) + + errInvalidSyntax := errors.Errorf("incorrect mount format: should be --mount type=,[src=,]target=[,options]") + + // TODO(vrothberg): the manual parsing can be replaced with a regular expression + // to allow a more robust parsing of the mount format and to give + // precise errors regarding supported format versus supported options. + for _, mount := range mounts { + arr := strings.SplitN(mount, ",", 2) + if len(arr) < 2 { + return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount) + } + kv := strings.Split(arr[0], "=") + // TODO: type is not explicitly required in Docker. + // If not specified, it defaults to "volume". + if len(kv) != 2 || kv[0] != "type" { + return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount) + } + + tokens := strings.Split(arr[1], ",") + switch kv[1] { + case TypeBind: + mount, err := getBindMount(tokens) + if err != nil { + return nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case TypeTmpfs: + mount, err := getTmpfsMount(tokens) + if err != nil { + return nil, nil, err + } + if _, ok := finalMounts[mount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination) + } + finalMounts[mount.Destination] = mount + case "volume": + volume, err := getNamedVolume(tokens) + if err != nil { + return nil, nil, err + } + if _, ok := finalNamedVolumes[volume.Dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, volume.Dest) + } + finalNamedVolumes[volume.Dest] = volume + default: + return nil, nil, errors.Errorf("invalid filesystem type %q", kv[1]) + } + } + + return finalMounts, finalNamedVolumes, nil +} + +// Parse a single bind mount entry from the --mount flag. +func getBindMount(args []string) (spec.Mount, error) { //nolint + newMount := spec.Mount{ + Type: TypeBind, + } + + var setSource, setDest, setRORW, setSuid, setDev, setExec, setRelabel bool + + for _, val := range args { + kv := strings.Split(val, "=") + switch kv[0] { + case "bind-nonrecursive": + newMount.Options = append(newMount.Options, "bind") + case "ro", "rw": + if setRORW { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' or 'rw' options more than once") + } + setRORW = true + // Can be formatted as one of: + // ro + // ro=[true|false] + // rw + // rw=[true|false] + switch len(kv) { + case 1: + newMount.Options = append(newMount.Options, kv[0]) + case 2: + switch strings.ToLower(kv[1]) { + case "true": + newMount.Options = append(newMount.Options, kv[0]) + case "false": + // Set the opposite only for rw + // ro's opposite is the default + if kv[0] == "rw" { + newMount.Options = append(newMount.Options, "ro") + } + default: + return newMount, errors.Wrapf(optionArgError, "%s must be set to true or false, instead received %q", kv[0], kv[1]) + } + default: + return newMount, errors.Wrapf(optionArgError, "badly formatted option %q", val) + } + case "nosuid", "suid": + if setSuid { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newMount.Options = append(newMount.Options, kv[0]) + case "nodev", "dev": + if setDev { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newMount.Options = append(newMount.Options, kv[0]) + case "noexec", "exec": + if setExec { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newMount.Options = append(newMount.Options, kv[0]) + case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z": + newMount.Options = append(newMount.Options, kv[0]) + case "bind-propagation": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, kv[1]) + case "src", "source": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeHostDir(kv[1]); err != nil { + return newMount, err + } + newMount.Source = kv[1] + setSource = true + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + case "relabel": + if setRelabel { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'relabel' option more than once") + } + setRelabel = true + if len(kv) != 2 { + return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) + } + switch kv[1] { + case "private": + newMount.Options = append(newMount.Options, "z") + case "shared": + newMount.Options = append(newMount.Options, "Z") + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0]) + } + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + if !setSource { + newMount.Source = newMount.Destination + } + + options, err := parse.ValidateVolumeOpts(newMount.Options) + if err != nil { + return newMount, err + } + newMount.Options = options + return newMount, nil +} + +// Parse a single tmpfs mount entry from the --mount flag +func getTmpfsMount(args []string) (spec.Mount, error) { //nolint + newMount := spec.Mount{ + Type: TypeTmpfs, + Source: TypeTmpfs, + } + + var setDest, setRORW, setSuid, setDev, setExec, setTmpcopyup bool + + for _, val := range args { + kv := strings.Split(val, "=") + switch kv[0] { + case "tmpcopyup", "notmpcopyup": + if setTmpcopyup { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'tmpcopyup' and 'notmpcopyup' options more than once") + } + setTmpcopyup = true + newMount.Options = append(newMount.Options, kv[0]) + case "ro", "rw": + if setRORW { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") + } + setRORW = true + newMount.Options = append(newMount.Options, kv[0]) + case "nosuid", "suid": + if setSuid { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newMount.Options = append(newMount.Options, kv[0]) + case "nodev", "dev": + if setDev { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newMount.Options = append(newMount.Options, kv[0]) + case "noexec", "exec": + if setExec { + return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newMount.Options = append(newMount.Options, kv[0]) + case "tmpfs-mode": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1])) + case "tmpfs-size": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1])) + case "src", "source": + return newMount, errors.Errorf("source is not supported with tmpfs mounts") + case "target", "dst", "destination": + if len(kv) == 1 { + return newMount, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return newMount, err + } + newMount.Destination = filepath.Clean(kv[1]) + setDest = true + default: + return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setDest { + return newMount, noDestError + } + + return newMount, nil +} + +// Parse a single volume mount entry from the --mount flag. +// Note that the volume-label option for named volumes is currently NOT supported. +// TODO: add support for --volume-label +func getNamedVolume(args []string) (*libpod.ContainerNamedVolume, error) { //nolint + newVolume := new(libpod.ContainerNamedVolume) + + var setSource, setDest, setRORW, setSuid, setDev, setExec bool + + for _, val := range args { + kv := strings.Split(val, "=") + switch kv[0] { + case "ro", "rw": + if setRORW { + return nil, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once") + } + setRORW = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "nosuid", "suid": + if setSuid { + return nil, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once") + } + setSuid = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "nodev", "dev": + if setDev { + return nil, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once") + } + setDev = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "noexec", "exec": + if setExec { + return nil, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once") + } + setExec = true + newVolume.Options = append(newVolume.Options, kv[0]) + case "volume-label": + return nil, errors.Errorf("the --volume-label option is not presently implemented") + case "src", "source": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + newVolume.Name = kv[1] + setSource = true + case "target", "dst", "destination": + if len(kv) == 1 { + return nil, errors.Wrapf(optionArgError, kv[0]) + } + if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { + return nil, err + } + newVolume.Dest = filepath.Clean(kv[1]) + setDest = true + default: + return nil, errors.Wrapf(util.ErrBadMntOption, kv[0]) + } + } + + if !setSource { + return nil, errors.Errorf("must set source volume") + } + if !setDest { + return nil, noDestError + } + + return newVolume, nil +} + +func getVolumeMounts(vols []string) (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint + mounts := make(map[string]spec.Mount) + volumes := make(map[string]*libpod.ContainerNamedVolume) + + volumeFormatErr := errors.Errorf("incorrect volume format, should be [host-dir:]ctr-dir[:option]") + + for _, vol := range vols { + var ( + options []string + src string + dest string + err error + ) + + splitVol := strings.Split(vol, ":") + if len(splitVol) > 3 { + return nil, nil, errors.Wrapf(volumeFormatErr, vol) + } + + src = splitVol[0] + if len(splitVol) == 1 { + // This is an anonymous named volume. Only thing given + // is destination. + // Name/source will be blank, and populated by libpod. + src = "" + dest = splitVol[0] + } else if len(splitVol) > 1 { + dest = splitVol[1] + } + if len(splitVol) > 2 { + if options, err = parse.ValidateVolumeOpts(strings.Split(splitVol[2], ",")); err != nil { + return nil, nil, err + } + } + + // Do not check source dir for anonymous volumes + if len(splitVol) > 1 { + if err := parse.ValidateVolumeHostDir(src); err != nil { + return nil, nil, err + } + } + if err := parse.ValidateVolumeCtrDir(dest); err != nil { + return nil, nil, err + } + + cleanDest := filepath.Clean(dest) + + if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") { + // This is not a named volume + newMount := spec.Mount{ + Destination: cleanDest, + Type: string(TypeBind), + Source: src, + Options: options, + } + if _, ok := mounts[newMount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, newMount.Destination) + } + mounts[newMount.Destination] = newMount + } else { + // This is a named volume + newNamedVol := new(libpod.ContainerNamedVolume) + newNamedVol.Name = src + newNamedVol.Dest = cleanDest + newNamedVol.Options = options + + if _, ok := volumes[newNamedVol.Dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, newNamedVol.Dest) + } + volumes[newNamedVol.Dest] = newNamedVol + } + + logrus.Debugf("User mount %s:%s options %v", src, dest, options) + } + + return mounts, volumes, nil +} + +// Get mounts for container's image volumes +// TODO deferred baude +func getImageVolumes() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) { //nolint + //mounts := make(map[string]spec.Mount) + //volumes := make(map[string]*define.ContainerNamedVolume) + // + //if config.ImageVolumeType == "ignore" { + // return mounts, volumes, nil + //} + // + //for vol := range config.BuiltinImgVolumes { + // cleanDest := filepath.Clean(vol) + // logrus.Debugf("Adding image volume at %s", cleanDest) + // if config.ImageVolumeType == "tmpfs" { + // // Tmpfs image volumes are handled as mounts + // mount := spec.Mount{ + // Destination: cleanDest, + // Source: TypeTmpfs, + // Type: TypeTmpfs, + // Options: []string{"rprivate", "rw", "nodev", "exec"}, + // } + // mounts[cleanDest] = mount + // } else { + // // Anonymous volumes have no name. + // namedVolume := new(define.ContainerNamedVolume) + // namedVolume.Options = []string{"rprivate", "rw", "nodev", "exec"} + // namedVolume.Dest = cleanDest + // volumes[cleanDest] = namedVolume + // } + //} + // + //return mounts, volumes, nil + return nil, nil, nil +} + +// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts +func getTmpfsMounts(mounts []string) (map[string]spec.Mount, error) { //nolint + m := make(map[string]spec.Mount) + for _, i := range mounts { + // Default options if nothing passed + var options []string + spliti := strings.Split(i, ":") + destPath := spliti[0] + if err := parse.ValidateVolumeCtrDir(spliti[0]); err != nil { + return nil, err + } + if len(spliti) > 1 { + options = strings.Split(spliti[1], ",") + } + + if _, ok := m[destPath]; ok { + return nil, errors.Wrapf(errDuplicateDest, destPath) + } + + mount := spec.Mount{ + Destination: filepath.Clean(destPath), + Type: string(TypeTmpfs), + Options: options, + Source: string(TypeTmpfs), + } + m[destPath] = mount + } + return m, nil +} + +// AddContainerInitBinary adds the init binary specified by path iff the +// container will run in a private PID namespace that is not shared with the +// host or another pre-existing container, where an init-like process is +// already running. +// +// Note that AddContainerInitBinary prepends "/dev/init" "--" to the command +// to execute the bind-mounted binary as PID 1. +// TODO this needs to be worked on to work in new env +func addContainerInitBinary(path string) (spec.Mount, error) { //nolint + mount := spec.Mount{ + Destination: "/dev/init", + Type: TypeBind, + Source: path, + Options: []string{TypeBind, "ro"}, + } + + //if path == "" { + // return mount, fmt.Errorf("please specify a path to the container-init binary") + //} + //if !config.Pid.PidMode.IsPrivate() { + // return mount, fmt.Errorf("cannot add init binary as PID 1 (PID namespace isn't private)") + //} + //if config.Systemd { + // return mount, fmt.Errorf("cannot use container-init binary with systemd") + //} + //if _, err := os.Stat(path); os.IsNotExist(err) { + // return mount, errors.Wrap(err, "container-init binary not found on the host") + //} + //config.Command = append([]string{"/dev/init", "--"}, config.Command...) + return mount, nil +} + +// Supersede existing mounts in the spec with new, user-specified mounts. +// TODO: Should we unmount subtree mounts? E.g., if /tmp/ is mounted by +// one mount, and we already have /tmp/a and /tmp/b, should we remove +// the /tmp/a and /tmp/b mounts in favor of the more general /tmp? +func SupercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.Mount { + if len(mounts) > 0 { + // If we have overlappings mounts, remove them from the spec in favor of + // the user-added volume mounts + destinations := make(map[string]bool) + for _, mount := range mounts { + destinations[path.Clean(mount.Destination)] = true + } + // Copy all mounts from spec to defaultMounts, except for + // - mounts overridden by a user supplied mount; + // - all mounts under /dev if a user supplied /dev is present; + mountDev := destinations["/dev"] + for _, mount := range configMount { + if _, ok := destinations[path.Clean(mount.Destination)]; !ok { + if mountDev && strings.HasPrefix(mount.Destination, "/dev/") { + // filter out everything under /dev if /dev is user-mounted + continue + } + + logrus.Debugf("Adding mount %s", mount.Destination) + mounts = append(mounts, mount) + } + } + return mounts + } + return configMount +} + +func InitFSMounts(mounts []spec.Mount) error { + for i, m := range mounts { + switch { + case m.Type == TypeBind: + opts, err := util.ProcessOptions(m.Options, false, m.Source) + if err != nil { + return err + } + mounts[i].Options = opts + case m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev": + opts, err := util.ProcessOptions(m.Options, true, "") + if err != nil { + return err + } + mounts[i].Options = opts + } + } + return nil +} -- cgit v1.2.3-54-g00ecf