diff options
51 files changed, 4103 insertions, 172 deletions
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: `<number>[<unit>]`, where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))" +) + +var ( + defaultContainerConfig = getDefaultContainerConfig() +) + +func getDefaultContainerConfig() *config.Config { + defaultContainerConfig, err := config.Default() + if err != nil { + logrus.Error(err) + os.Exit(1) + } + return defaultContainerConfig +} + +func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { + //createFlags := c.Flags() + createFlags := pflag.FlagSet{} + createFlags.StringSliceVar( + &cf.Annotation, + "annotation", []string{}, + "Add annotations to container (key:value)", + ) + createFlags.StringSliceVarP( + &cf.Attach, + "attach", "a", []string{}, + "Attach to STDIN, STDOUT or STDERR", + ) + createFlags.StringVar( + &cf.Authfile, + "authfile", buildahcli.GetDefaultAuthFile(), + "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override", + ) + createFlags.StringVar( + &cf.BlkIOWeight, + "blkio-weight", "", + "Block IO weight (relative weight) accepts a weight value between 10 and 1000.", + ) + createFlags.StringSliceVar( + &cf.BlkIOWeightDevice, + "blkio-weight-device", []string{}, + "Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`)", + ) + createFlags.StringSliceVar( + &cf.CapAdd, + "cap-add", []string{}, + "Add capabilities to the container", + ) + createFlags.StringSliceVar( + &cf.CapDrop, + "cap-drop", []string{}, + "Drop capabilities from the container", + ) + createFlags.StringVar( + &cf.CGroupsNS, + "cgroupns", getDefaultCgroupNS(), + "cgroup namespace to use", + ) + createFlags.StringVar( + &cf.CGroups, + "cgroups", "enabled", + `control container cgroup configuration ("enabled"|"disabled"|"no-conmon")`, + ) + createFlags.StringVar( + &cf.CGroupParent, + "cgroup-parent", "", + "Optional parent cgroup for the container", + ) + createFlags.StringVar( + &cf.CIDFile, + "cidfile", "", + "Write the container ID to the file", + ) + createFlags.StringVar( + &cf.ConmonPIDFile, + "conmon-pidfile", "", + "Path to the file that will receive the PID of conmon", + ) + createFlags.Uint64Var( + &cf.CPUPeriod, + "cpu-period", 0, + "Limit the CPU CFS (Completely Fair Scheduler) period", + ) + createFlags.Int64Var( + &cf.CPUQuota, + "cpu-quota", 0, + "Limit the CPU CFS (Completely Fair Scheduler) quota", + ) + createFlags.Uint64Var( + &cf.CPURTPeriod, + "cpu-rt-period", 0, + "Limit the CPU real-time period in microseconds", + ) + createFlags.Int64Var( + &cf.CPURTRuntime, + "cpu-rt-runtime", 0, + "Limit the CPU real-time runtime in microseconds", + ) + createFlags.Uint64Var( + &cf.CPUShares, + "cpu-shares", 0, + "CPU shares (relative weight)", + ) + createFlags.Float64Var( + &cf.CPUS, + "cpus", 0, + "Number of CPUs. The default is 0.000 which means no limit", + ) + createFlags.StringVar( + &cf.CPUSetCPUs, + "cpuset-cpus", "", + "CPUs in which to allow execution (0-3, 0,1)", + ) + createFlags.StringVar( + &cf.CPUSetMems, + "cpuset-mems", "", + "Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.", + ) + createFlags.BoolVarP( + &cf.Detach, + "detach", "d", false, + "Run container in background and print container ID", + ) + createFlags.StringVar( + &cf.DetachKeys, + "detach-keys", getDefaultDetachKeys(), + "Override the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-cf`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`", + ) + createFlags.StringSliceVar( + &cf.Device, + "device", getDefaultDevices(), + fmt.Sprintf("Add a host device to the container"), + ) + createFlags.StringSliceVar( + &cf.DeviceCGroupRule, + "device-cgroup-rule", []string{}, + "Add a rule to the cgroup allowed devices list", + ) + createFlags.StringSliceVar( + &cf.DeviceReadBPs, + "device-read-bps", []string{}, + "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)", + ) + createFlags.StringSliceVar( + &cf.DeviceReadIOPs, + "device-read-iops", []string{}, + "Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)", + ) + createFlags.StringSliceVar( + &cf.DeviceWriteBPs, + "device-write-bps", []string{}, + "Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb)", + ) + createFlags.StringSliceVar( + &cf.DeviceWriteIOPs, + "device-write-iops", []string{}, + "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)", + ) + createFlags.StringVar( + &cf.Entrypoint, + "entrypoint", "", + "Overwrite the default ENTRYPOINT of the image", + ) + createFlags.StringArrayVarP( + &cf.env, + "env", "e", getDefaultEnv(), + "Set environment variables in container", + ) + createFlags.BoolVar( + &cf.EnvHost, + "env-host", false, "Use all current host environment variables in container", + ) + createFlags.StringSliceVar( + &cf.EnvFile, + "env-file", []string{}, + "Read in a file of environment variables", + ) + createFlags.StringSliceVar( + &cf.Expose, + "expose", []string{}, + "Expose a port or a range of ports", + ) + createFlags.StringSliceVar( + &cf.GIDMap, + "gidmap", []string{}, + "GID map to use for the user namespace", + ) + createFlags.StringSliceVar( + &cf.GroupAdd, + "group-add", []string{}, + "Add additional groups to join", + ) + createFlags.Bool( + "help", false, "", + ) + createFlags.StringVar( + &cf.HealthCmd, + "health-cmd", "", + "set a healthcheck command for the container ('none' disables the existing healthcheck)", + ) + createFlags.StringVar( + &cf.HealthInterval, + "health-interval", cliconfig.DefaultHealthCheckInterval, + "set an interval for the healthchecks (a value of disable results in no automatic timer setup)", + ) + createFlags.UintVar( + &cf.HealthRetries, + "health-retries", cliconfig.DefaultHealthCheckRetries, + "the number of retries allowed before a healthcheck is considered to be unhealthy", + ) + createFlags.StringVar( + &cf.HealthStartPeriod, + "health-start-period", cliconfig.DefaultHealthCheckStartPeriod, + "the initialization time needed for a container to bootstrap", + ) + createFlags.StringVar( + &cf.HealthTimeout, + "health-timeout", cliconfig.DefaultHealthCheckTimeout, + "the maximum time allowed to complete the healthcheck before an interval is considered failed", + ) + createFlags.StringVarP( + &cf.Hostname, + "hostname", "h", "", + "Set container hostname", + ) + createFlags.BoolVar( + &cf.HTTPProxy, + "http-proxy", true, + "Set proxy environment variables in the container based on the host proxy vars", + ) + createFlags.StringVar( + &cf.ImageVolume, + "image-volume", cliconfig.DefaultImageVolume, + `Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore")`, + ) + createFlags.BoolVar( + &cf.Init, + "init", false, + "Run an init binary inside the container that forwards signals and reaps processes", + ) + createFlags.StringVar( + &cf.InitPath, + "init-path", getDefaultInitPath(), + // Do not use the Value field for setting the default value to determine user input (i.e., non-empty string) + fmt.Sprintf("Path to the container-init binary"), + ) + createFlags.BoolVarP( + &cf.Interactive, + "interactive", "i", false, + "Keep STDIN open even if not attached", + ) + createFlags.StringVar( + &cf.IPC, + "ipc", getDefaultIPCNS(), + "IPC namespace to use", + ) + createFlags.StringVar( + &cf.KernelMemory, + "kernel-memory", "", + "Kernel memory limit "+sizeWithUnitFormat, + ) + createFlags.StringArrayVarP( + &cf.Label, + "label", "l", []string{}, + "Set metadata on container", + ) + createFlags.StringSliceVar( + &cf.LabelFile, + "label-file", []string{}, + "Read in a line delimited file of labels", + ) + createFlags.StringVar( + &cf.LogDriver, + "log-driver", "", + "Logging driver for the container", + ) + createFlags.StringSliceVar( + &cf.LogOptions, + "log-opt", []string{}, + "Logging driver options", + ) + createFlags.StringVarP( + &cf.Memory, + "memory", "m", "", + "Memory limit "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.MemoryReservation, + "memory-reservation", "", + "Memory soft limit "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.MemorySwap, + "memory-swap", "", + "Swap limit equal to memory plus swap: '-1' to enable unlimited swap", + ) + createFlags.Int64Var( + &cf.MemorySwappiness, + "memory-swappiness", -1, + "Tune container memory swappiness (0 to 100, or -1 for system default)", + ) + createFlags.StringVar( + &cf.Name, + "name", "", + "Assign a name to the container", + ) + createFlags.BoolVar( + &cf.NoHealthCheck, + "no-healthcheck", false, + "Disable healthchecks on container", + ) + createFlags.BoolVar( + &cf.OOMKillDisable, + "oom-kill-disable", false, + "Disable OOM Killer", + ) + createFlags.IntVar( + &cf.OOMScoreAdj, + "oom-score-adj", 0, + "Tune the host's OOM preferences (-1000 to 1000)", + ) + createFlags.StringVar( + &cf.OverrideArch, + "override-arch", "", + "use `ARCH` instead of the architecture of the machine for choosing images", + ) + //markFlagHidden(createFlags, "override-arch") + createFlags.StringVar( + &cf.OverrideOS, + "override-os", "", + "use `OS` instead of the running OS for choosing images", + ) + //markFlagHidden(createFlags, "override-os") + createFlags.StringVar( + &cf.PID, + "pid", getDefaultPidNS(), + "PID namespace to use", + ) + createFlags.Int64Var( + &cf.PIDsLimit, + "pids-limit", getDefaultPidsLimit(), + getDefaultPidsDescription(), + ) + createFlags.StringVar( + &cf.Pod, + "pod", "", + "Run container in an existing pod", + ) + createFlags.BoolVar( + &cf.Privileged, + "privileged", false, + "Give extended privileges to container", + ) + createFlags.BoolVarP( + &cf.PublishAll, + "publish-all", "P", false, + "Publish all exposed ports to random ports on the host interface", + ) + createFlags.StringVar( + &cf.Pull, + "pull", "missing", + `Pull image before creating ("always"|"missing"|"never")`, + ) + createFlags.BoolVarP( + &cf.Quiet, + "quiet", "q", false, + "Suppress output information when pulling images", + ) + createFlags.BoolVar( + &cf.ReadOnly, + "read-only", false, + "Make containers root filesystem read-only", + ) + createFlags.BoolVar( + &cf.ReadOnlyTmpFS, + "read-only-tmpfs", true, + "When running containers in read-only mode mount a read-write tmpfs on /run, /tmp and /var/tmp", + ) + createFlags.StringVar( + &cf.Restart, + "restart", "", + `Restart policy to apply when a container exits ("always"|"no"|"on-failure")`, + ) + createFlags.BoolVar( + &cf.Rm, + "rm", false, + "Remove container (and pod if created) after exit", + ) + createFlags.BoolVar( + &cf.RootFS, + "rootfs", false, + "The first argument is not an image but the rootfs to the exploded container", + ) + createFlags.StringArrayVar( + &cf.SecurityOpt, + "security-opt", getDefaultSecurityOptions(), + fmt.Sprintf("Security Options"), + ) + createFlags.StringVar( + &cf.ShmSize, + "shm-size", getDefaultShmSize(), + "Size of /dev/shm "+sizeWithUnitFormat, + ) + createFlags.StringVar( + &cf.StopSignal, + "stop-signal", "", + "Signal to stop a container. Default is SIGTERM", + ) + createFlags.UintVar( + &cf.StopTimeout, + "stop-timeout", defaultContainerConfig.Engine.StopTimeout, + "Timeout (in seconds) to stop a container. Default is 10", + ) + createFlags.StringSliceVar( + &cf.StoreageOpt, + "storage-opt", []string{}, + "Storage driver options per container", + ) + createFlags.StringVar( + &cf.SubUIDName, + "subgidname", "", + "Name of range listed in /etc/subgid for use in user namespace", + ) + createFlags.StringVar( + &cf.SubGIDName, + "subuidname", "", + "Name of range listed in /etc/subuid for use in user namespace", + ) + + createFlags.StringSliceVar( + &cf.Sysctl, + "sysctl", getDefaultSysctls(), + "Sysctl options", + ) + createFlags.StringVar( + &cf.SystemdD, + "systemd", "true", + `Run container in systemd mode ("true"|"false"|"always")`, + ) + createFlags.StringArrayVar( + &cf.TmpFS, + "tmpfs", []string{}, + "Mount a temporary filesystem (`tmpfs`) into a container", + ) + createFlags.BoolVarP( + &cf.TTY, + "tty", "t", false, + "Allocate a pseudo-TTY for container", + ) + createFlags.StringSliceVar( + &cf.UIDMap, + "uidmap", []string{}, + "UID map to use for the user namespace", + ) + createFlags.StringSliceVar( + &cf.Ulimit, + "ulimit", getDefaultUlimits(), + "Ulimit options", + ) + createFlags.StringVarP( + &cf.User, + "user", "u", "", + "Username or UID (format: <name|uid>[:<group|gid>])", + ) + createFlags.StringVar( + &cf.UserNS, + "userns", getDefaultUserNS(), + "User namespace to use", + ) + createFlags.StringVar( + &cf.UTS, + "uts", getDefaultUTSNS(), + "UTS namespace to use", + ) + createFlags.StringArrayVar( + &cf.Mount, + "mount", []string{}, + "Attach a filesystem mount to the container", + ) + createFlags.StringArrayVarP( + &cf.Volume, + "volume", "v", getDefaultVolumes(), + "Bind mount a volume into the container", + ) + createFlags.StringSliceVar( + &cf.VolumesFrom, + "volumes-from", []string{}, + "Mount volumes from the specified container(s)", + ) + createFlags.StringVarP( + &cf.Workdir, + "workdir", "w", "", + "Working directory inside the container", + ) + createFlags.StringVar( + &cf.SeccompPolicy, + "seccomp-policy", "default", + "Policy for selecting a seccomp profile (experimental)", + ) + return &createFlags +} + +func AliasFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { + switch name { + case "healthcheck-command": + name = "health-cmd" + case "healthcheck-interval": + name = "health-interval" + case "healthcheck-retries": + name = "health-retries" + case "healthcheck-start-period": + name = "health-start-period" + case "healthcheck-timeout": + name = "health-timeout" + case "net": + name = "network" + } + return pflag.NormalizedName(name) +} diff --git a/cmd/podmanV2/common/create_opts.go b/cmd/podmanV2/common/create_opts.go new file mode 100644 index 000000000..9d12e4b26 --- /dev/null +++ b/cmd/podmanV2/common/create_opts.go @@ -0,0 +1,103 @@ +package common + +import "github.com/containers/libpod/pkg/domain/entities" + +type ContainerCLIOpts struct { + Annotation []string + Attach []string + Authfile string + BlkIOWeight string + BlkIOWeightDevice []string + CapAdd []string + CapDrop []string + CGroupsNS string + CGroups string + CGroupParent string + CIDFile string + ConmonPIDFile string + CPUPeriod uint64 + CPUQuota int64 + CPURTPeriod uint64 + CPURTRuntime int64 + CPUShares uint64 + CPUS float64 + CPUSetCPUs string + CPUSetMems string + Detach bool + DetachKeys string + Device []string + DeviceCGroupRule []string + DeviceReadBPs []string + DeviceReadIOPs []string + DeviceWriteBPs []string + DeviceWriteIOPs []string + Entrypoint string + env []string + EnvHost bool + EnvFile []string + Expose []string + GIDMap []string + GroupAdd []string + HealthCmd string + HealthInterval string + HealthRetries uint + HealthStartPeriod string + HealthTimeout string + Hostname string + HTTPProxy bool + ImageVolume string + Init bool + InitPath string + Interactive bool + IPC string + KernelMemory string + Label []string + LabelFile []string + LogDriver string + LogOptions []string + Memory string + MemoryReservation string + MemorySwap string + MemorySwappiness int64 + Name string + NoHealthCheck bool + OOMKillDisable bool + OOMScoreAdj int + OverrideArch string + OverrideOS string + PID string + PIDsLimit int64 + Pod string + Privileged bool + PublishAll bool + Pull string + Quiet bool + ReadOnly bool + ReadOnlyTmpFS bool + Restart string + Rm bool + RootFS bool + SecurityOpt []string + ShmSize string + StopSignal string + StopTimeout uint + StoreageOpt []string + SubUIDName string + SubGIDName string + Sysctl []string + SystemdD string + TmpFS []string + TTY bool + UIDMap []string + Ulimit []string + User string + UserNS string + UTS string + Mount []string + Volume []string + VolumesFrom []string + Workdir string + SeccompPolicy string + + Net *entities.NetOptions +} diff --git a/cmd/podmanV2/common/createparse.go b/cmd/podmanV2/common/createparse.go new file mode 100644 index 000000000..89524a04b --- /dev/null +++ b/cmd/podmanV2/common/createparse.go @@ -0,0 +1,51 @@ +package common + +import ( + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +// validate determines if the flags and values given by the user are valid. things checked +// by validate must not need any state information on the flag (i.e. changed) +func (c *ContainerCLIOpts) validate() error { + var () + if c.Rm && c.Restart != "" && c.Restart != "no" { + return errors.Errorf("the --rm option conflicts with --restart") + } + + if _, err := util.ValidatePullType(c.Pull); err != nil { + return err + } + // Verify the additional hosts are in correct format + for _, host := range c.Net.AddHosts { + if _, err := parse.ValidateExtraHost(host); err != nil { + return err + } + } + + if dnsSearches := c.Net.DNSSearch; len(dnsSearches) > 0 { + // Validate domains are good + for _, dom := range dnsSearches { + if dom == "." { + if len(dnsSearches) > 1 { + return errors.Errorf("cannot pass additional search domains when also specifying '.'") + } + continue + } + if _, err := parse.ValidateDomain(dom); err != nil { + return err + } + } + } + var imageVolType = map[string]string{ + "bind": "", + "tmpfs": "", + "ignore": "", + } + if _, ok := imageVolType[c.ImageVolume]; !ok { + return errors.Errorf("invalid image-volume type %q. Pick one of bind, tmpfs, or ignore", c.ImageVolume) + } + return nil + +} diff --git a/cmd/podmanV2/common/default.go b/cmd/podmanV2/common/default.go new file mode 100644 index 000000000..fea161edf --- /dev/null +++ b/cmd/podmanV2/common/default.go @@ -0,0 +1,121 @@ +package common + +import ( + "fmt" + "os" + + "github.com/containers/buildah/pkg/parse" + "github.com/containers/libpod/pkg/apparmor" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/sysinfo" + "github.com/opencontainers/selinux/go-selinux" +) + +// TODO these options are directly embedded into many of the CLI cobra values, as such +// this approach will not work in a remote client. so we will need to likely do something like a +// supported and unsupported approach here and backload these options into the specgen +// once we are "on" the host system. +func getDefaultSecurityOptions() []string { + securityOpts := []string{} + if defaultContainerConfig.Containers.SeccompProfile != "" && defaultContainerConfig.Containers.SeccompProfile != parse.SeccompDefaultPath { + securityOpts = append(securityOpts, fmt.Sprintf("seccomp=%s", defaultContainerConfig.Containers.SeccompProfile)) + } + if apparmor.IsEnabled() && defaultContainerConfig.Containers.ApparmorProfile != "" { + securityOpts = append(securityOpts, fmt.Sprintf("apparmor=%s", defaultContainerConfig.Containers.ApparmorProfile)) + } + if selinux.GetEnabled() && !defaultContainerConfig.Containers.EnableLabeling { + securityOpts = append(securityOpts, fmt.Sprintf("label=%s", selinux.DisableSecOpt()[0])) + } + return securityOpts +} + +// getDefaultSysctls +func getDefaultSysctls() []string { + return defaultContainerConfig.Containers.DefaultSysctls +} + +func getDefaultVolumes() []string { + return defaultContainerConfig.Containers.Volumes +} + +func getDefaultDevices() []string { + return defaultContainerConfig.Containers.Devices +} + +func getDefaultDNSServers() []string { //nolint + return defaultContainerConfig.Containers.DNSServers +} + +func getDefaultDNSSearches() []string { //nolint + return defaultContainerConfig.Containers.DNSSearches +} + +func getDefaultDNSOptions() []string { //nolint + return defaultContainerConfig.Containers.DNSOptions +} + +func getDefaultEnv() []string { + return defaultContainerConfig.Containers.Env +} + +func getDefaultInitPath() string { + return defaultContainerConfig.Containers.InitPath +} + +func getDefaultIPCNS() string { + return defaultContainerConfig.Containers.IPCNS +} + +func getDefaultPidNS() string { + return defaultContainerConfig.Containers.PidNS +} + +func getDefaultNetNS() string { //nolint + if defaultContainerConfig.Containers.NetNS == "private" && rootless.IsRootless() { + return "slirp4netns" + } + return defaultContainerConfig.Containers.NetNS +} + +func getDefaultCgroupNS() string { + return defaultContainerConfig.Containers.CgroupNS +} + +func getDefaultUTSNS() string { + return defaultContainerConfig.Containers.UTSNS +} + +func getDefaultShmSize() string { + return defaultContainerConfig.Containers.ShmSize +} + +func getDefaultUlimits() []string { + return defaultContainerConfig.Containers.DefaultUlimits +} + +func getDefaultUserNS() string { + userns := os.Getenv("PODMAN_USERNS") + if userns != "" { + return userns + } + return defaultContainerConfig.Containers.UserNS +} + +func getDefaultPidsLimit() int64 { + if rootless.IsRootless() { + cgroup2, _ := cgroups.IsCgroup2UnifiedMode() + if cgroup2 { + return defaultContainerConfig.Containers.PidsLimit + } + } + return sysinfo.GetDefaultPidsLimit() +} + +func getDefaultPidsDescription() string { + return "Tune container pids limit (set 0 for unlimited)" +} + +func getDefaultDetachKeys() string { + return defaultContainerConfig.Engine.DetachKeys +} diff --git a/cmd/podmanV2/common/ports.go b/cmd/podmanV2/common/ports.go new file mode 100644 index 000000000..7e2b1e79d --- /dev/null +++ b/cmd/podmanV2/common/ports.go @@ -0,0 +1,126 @@ +package common + +import ( + "fmt" + "net" + "strconv" + + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ExposedPorts parses user and image ports and returns binding information +func ExposedPorts(expose []string, publish []ocicni.PortMapping, publishAll bool, imageExposedPorts map[string]struct{}) ([]ocicni.PortMapping, error) { + containerPorts := make(map[string]string) + + // TODO this needs to be added into a something that + // has access to an imageengine + // add expose ports from the image itself + //for expose := range imageExposedPorts { + // _, port := nat.SplitProtoPort(expose) + // containerPorts[port] = "" + //} + + // add the expose ports from the user (--expose) + // can be single or a range + for _, expose := range expose { + //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + _, port := nat.SplitProtoPort(expose) + //parse the start and end port and create a sequence of ports to expose + //if expose a port, the start and end port are the same + start, end, err := nat.ParsePortRange(port) + if err != nil { + return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", expose, err) + } + for i := start; i <= end; i++ { + containerPorts[strconv.Itoa(int(i))] = "" + } + } + + // TODO/FIXME this is hell reencarnated + // parse user inputted port bindings + pbPorts, portBindings, err := nat.ParsePortSpecs([]string{}) + if err != nil { + return nil, err + } + + // delete exposed container ports if being used by -p + for i := range pbPorts { + delete(containerPorts, i.Port()) + } + + // iterate container ports and make port bindings from them + if publishAll { + for e := range containerPorts { + //support two formats for expose, original format <portnum>/[<proto>] or <startport-endport>/[<proto>] + //proto, port := nat.SplitProtoPort(e) + p, err := nat.NewPort("tcp", e) + if err != nil { + return nil, err + } + rp, err := getRandomPort() + if err != nil { + return nil, err + } + logrus.Debug(fmt.Sprintf("Using random host port %d with container port %d", rp, p.Int())) + portBindings[p] = CreatePortBinding(rp, "") + } + } + + // We need to see if any host ports are not populated and if so, we need to assign a + // random port to them. + for k, pb := range portBindings { + if pb[0].HostPort == "" { + hostPort, err := getRandomPort() + if err != nil { + return nil, err + } + logrus.Debug(fmt.Sprintf("Using random host port %d with container port %s", hostPort, k.Port())) + pb[0].HostPort = strconv.Itoa(hostPort) + } + } + var pms []ocicni.PortMapping + for k, v := range portBindings { + for _, pb := range v { + hp, err := strconv.Atoi(pb.HostPort) + if err != nil { + return nil, err + } + pms = append(pms, ocicni.PortMapping{ + HostPort: int32(hp), + ContainerPort: int32(k.Int()), + //Protocol: "", + HostIP: pb.HostIP, + }) + } + } + return pms, nil +} + +func getRandomPort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, errors.Wrapf(err, "unable to get free port") + } + defer l.Close() + _, randomPort, err := net.SplitHostPort(l.Addr().String()) + if err != nil { + return 0, errors.Wrapf(err, "unable to determine free port") + } + rp, err := strconv.Atoi(randomPort) + if err != nil { + return 0, errors.Wrapf(err, "unable to convert random port to int") + } + return rp, nil +} + +//CreatePortBinding takes port (int) and IP (string) and creates an array of portbinding structs +func CreatePortBinding(hostPort int, hostIP string) []nat.PortBinding { + pb := nat.PortBinding{ + HostPort: strconv.Itoa(hostPort), + } + pb.HostIP = hostIP + return []nat.PortBinding{pb} +} diff --git a/cmd/podmanV2/common/specgen.go b/cmd/podmanV2/common/specgen.go new file mode 100644 index 000000000..5245e206e --- /dev/null +++ b/cmd/podmanV2/common/specgen.go @@ -0,0 +1,647 @@ +package common + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/containers/image/v5/manifest" + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/libpod" + ann "github.com/containers/libpod/pkg/annotations" + envLib "github.com/containers/libpod/pkg/env" + ns "github.com/containers/libpod/pkg/namespaces" + "github.com/containers/libpod/pkg/specgen" + systemdGen "github.com/containers/libpod/pkg/systemd/generate" + "github.com/containers/libpod/pkg/util" + "github.com/docker/go-units" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string) error { + var ( + err error + //namespaces map[string]string + ) + + // validate flags as needed + if err := c.validate(); err != nil { + return nil + } + + inputCommand := args[1:] + if len(c.HealthCmd) > 0 { + s.HealthConfig, err = makeHealthCheckFromCli(c.HealthCmd, c.HealthInterval, c.HealthRetries, c.HealthTimeout, c.HealthStartPeriod) + if err != nil { + return err + } + } + + s.IDMappings, err = util.ParseIDMapping(ns.UsernsMode(c.UserNS), c.UIDMap, c.GIDMap, c.SubUIDName, c.SubGIDName) + if err != nil { + return err + } + if m := c.Memory; len(m) > 0 { + ml, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + s.ResourceLimits.Memory.Limit = &ml + } + if m := c.MemoryReservation; len(m) > 0 { + mr, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + s.ResourceLimits.Memory.Reservation = &mr + } + if m := c.MemorySwap; len(m) > 0 { + var ms int64 + if m == "-1" { + ms = int64(-1) + s.ResourceLimits.Memory.Swap = &ms + } else { + ms, err = units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for memory") + } + } + s.ResourceLimits.Memory.Swap = &ms + } + if m := c.KernelMemory; len(m) > 0 { + mk, err := units.RAMInBytes(m) + if err != nil { + return errors.Wrapf(err, "invalid value for kernel-memory") + } + s.ResourceLimits.Memory.Kernel = &mk + } + if b := c.BlkIOWeight; len(b) > 0 { + u, err := strconv.ParseUint(b, 10, 16) + if err != nil { + return errors.Wrapf(err, "invalid value for blkio-weight") + } + nu := uint16(u) + s.ResourceLimits.BlockIO.Weight = &nu + } + + s.Terminal = c.TTY + ep, err := ExposedPorts(c.Expose, c.Net.PublishPorts, c.PublishAll, nil) + if err != nil { + return err + } + s.PortMappings = ep + s.Pod = c.Pod + + //s.CgroupNS = specgen.Namespace{ + // NSMode: , + // Value: "", + //} + + //s.UserNS = specgen.Namespace{} + + // Kernel Namespaces + // TODO Fix handling of namespace from pod + // Instead of integrating here, should be done in libpod + // However, that also involves setting up security opts + // when the pod's namespace is integrated + //namespaces = map[string]string{ + // "cgroup": c.CGroupsNS, + // "pid": c.PID, + // //"net": c.Net.Network.Value, // TODO need help here + // "ipc": c.IPC, + // "user": c.User, + // "uts": c.UTS, + //} + // + //if len(c.PID) > 0 { + // split := strings.SplitN(c.PID, ":", 2) + // // need a way to do thsi + // specgen.Namespace{ + // NSMode: split[0], + // } + // //Value: split1 if len allows + //} + // TODO this is going to have be done after things like pod creation are done because + // pod creation changes these values. + //pidMode := ns.PidMode(namespaces["pid"]) + //usernsMode := ns.UsernsMode(namespaces["user"]) + //utsMode := ns.UTSMode(namespaces["uts"]) + //cgroupMode := ns.CgroupMode(namespaces["cgroup"]) + //ipcMode := ns.IpcMode(namespaces["ipc"]) + //// Make sure if network is set to container namespace, port binding is not also being asked for + //netMode := ns.NetworkMode(namespaces["net"]) + //if netMode.IsContainer() { + // if len(portBindings) > 0 { + // return nil, errors.Errorf("cannot set port bindings on an existing container network namespace") + // } + //} + + // TODO Remove when done with namespaces for realz + // Setting a default for IPC to get this working + s.IpcNS = specgen.Namespace{ + NSMode: specgen.Private, + Value: "", + } + + // TODO this is going to have to be done the libpod/server end of things + // USER + //user := c.String("user") + //if user == "" { + // switch { + // case usernsMode.IsKeepID(): + // user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID()) + // case data == nil: + // user = "0" + // default: + // user = data.Config.User + // } + //} + + // STOP SIGNAL + signalString := "TERM" + if sig := c.StopSignal; len(sig) > 0 { + signalString = sig + } + stopSignal, err := util.ParseSignal(signalString) + if err != nil { + return err + } + s.StopSignal = &stopSignal + + // ENVIRONMENT VARIABLES + // + // Precedence order (higher index wins): + // 1) env-host, 2) image data, 3) env-file, 4) env + env := map[string]string{ + "container": "podman", + } + + // First transform the os env into a map. We need it for the labels later in + // any case. + osEnv, err := envLib.ParseSlice(os.Environ()) + if err != nil { + return errors.Wrap(err, "error parsing host environment variables") + } + + if c.EnvHost { + env = envLib.Join(env, osEnv) + } + // env-file overrides any previous variables + for _, f := range c.EnvFile { + fileEnv, err := envLib.ParseFile(f) + if err != nil { + return err + } + // File env is overridden by env. + env = envLib.Join(env, fileEnv) + } + + // env overrides any previous variables + if cmdLineEnv := c.env; len(cmdLineEnv) > 0 { + parsedEnv, err := envLib.ParseSlice(cmdLineEnv) + if err != nil { + return err + } + env = envLib.Join(env, parsedEnv) + } + s.Env = env + + // LABEL VARIABLES + labels, err := parse.GetAllLabels(c.LabelFile, c.Label) + if err != nil { + return errors.Wrapf(err, "unable to process labels") + } + + if systemdUnit, exists := osEnv[systemdGen.EnvVariable]; exists { + labels[systemdGen.EnvVariable] = systemdUnit + } + + s.Labels = labels + + // ANNOTATIONS + annotations := make(map[string]string) + + // First, add our default annotations + annotations[ann.TTY] = "false" + if c.TTY { + annotations[ann.TTY] = "true" + } + + // Last, add user annotations + for _, annotation := range c.Annotation { + splitAnnotation := strings.SplitN(annotation, "=", 2) + if len(splitAnnotation) < 2 { + return errors.Errorf("Annotations must be formatted KEY=VALUE") + } + annotations[splitAnnotation[0]] = splitAnnotation[1] + } + s.Annotations = annotations + + workDir := "/" + if wd := c.Workdir; len(wd) > 0 { + workDir = wd + } + s.WorkDir = workDir + entrypoint := []string{} + userCommand := []string{} + if ep := c.Entrypoint; len(ep) > 0 { + // Check if entrypoint specified is json + if err := json.Unmarshal([]byte(c.Entrypoint), &entrypoint); err != nil { + entrypoint = append(entrypoint, ep) + } + } + + var command []string + + // Build the command + // If we have an entry point, it goes first + if len(entrypoint) > 0 { + command = entrypoint + } + if len(inputCommand) > 0 { + // User command overrides data CMD + command = append(command, inputCommand...) + userCommand = append(userCommand, inputCommand...) + } + + if len(inputCommand) > 0 { + s.Command = userCommand + } else { + s.Command = command + } + + // SHM Size + shmSize, err := units.FromHumanSize(c.ShmSize) + if err != nil { + return errors.Wrapf(err, "unable to translate --shm-size") + } + s.ShmSize = &shmSize + s.HostAdd = c.Net.AddHosts + s.DNSServer = c.Net.DNSServers + s.DNSSearch = c.Net.DNSSearch + s.DNSOption = c.Net.DNSOptions + + // deferred, must be added on libpod side + //var ImageVolumes map[string]struct{} + //if data != nil && c.String("image-volume") != "ignore" { + // ImageVolumes = data.Config.Volumes + //} + + s.ImageVolumeMode = c.ImageVolume + systemd := c.SystemdD == "always" + if !systemd && command != nil { + x, err := strconv.ParseBool(c.SystemdD) + if err != nil { + return errors.Wrapf(err, "cannot parse bool %s", c.SystemdD) + } + if x && (command[0] == "/usr/sbin/init" || command[0] == "/sbin/init" || (filepath.Base(command[0]) == "systemd")) { + systemd = true + } + } + if systemd { + if s.StopSignal == nil { + stopSignal, err = util.ParseSignal("RTMIN+3") + if err != nil { + return errors.Wrapf(err, "error parsing systemd signal") + } + s.StopSignal = &stopSignal + } + } + swappiness := uint64(c.MemorySwappiness) + if s.ResourceLimits == nil { + s.ResourceLimits = &specs.LinuxResources{} + } + if s.ResourceLimits.Memory == nil { + s.ResourceLimits.Memory = &specs.LinuxMemory{} + } + s.ResourceLimits.Memory.Swappiness = &swappiness + + if s.LogConfiguration == nil { + s.LogConfiguration = &specgen.LogConfig{} + } + s.LogConfiguration.Driver = libpod.KubernetesLogging + if ld := c.LogDriver; len(ld) > 0 { + s.LogConfiguration.Driver = ld + } + if s.ResourceLimits.Pids == nil { + s.ResourceLimits.Pids = &specs.LinuxPids{} + } + s.ResourceLimits.Pids.Limit = c.PIDsLimit + if c.CGroups == "disabled" && c.PIDsLimit > 0 { + s.ResourceLimits.Pids.Limit = -1 + } + // TODO WTF + //cgroup := &cc.CgroupConfig{ + // Cgroups: c.String("cgroups"), + // Cgroupns: c.String("cgroupns"), + // CgroupParent: c.String("cgroup-parent"), + // CgroupMode: cgroupMode, + //} + // + //userns := &cc.UserConfig{ + // GroupAdd: c.StringSlice("group-add"), + // IDMappings: idmappings, + // UsernsMode: usernsMode, + // User: user, + //} + // + //uts := &cc.UtsConfig{ + // UtsMode: utsMode, + // NoHosts: c.Bool("no-hosts"), + // HostAdd: c.StringSlice("add-host"), + // Hostname: c.String("hostname"), + //} + + sysctl := map[string]string{} + if ctl := c.Sysctl; len(ctl) > 0 { + sysctl, err = util.ValidateSysctls(ctl) + if err != nil { + return err + } + } + s.Sysctl = sysctl + + s.CapAdd = c.CapAdd + s.CapDrop = c.CapDrop + s.Privileged = c.Privileged + s.ReadOnlyFilesystem = c.ReadOnly + + // TODO + // ouitside of specgen and oci though + // defaults to true, check spec/storage + //s.readon = c.ReadOnlyTmpFS + // TODO convert to map? + // check if key=value and convert + sysmap := make(map[string]string) + for _, ctl := range c.Sysctl { + splitCtl := strings.SplitN(ctl, "=", 2) + if len(splitCtl) < 2 { + return errors.Errorf("invalid sysctl value %q", ctl) + } + sysmap[splitCtl[0]] = splitCtl[1] + } + s.Sysctl = sysmap + + for _, opt := range c.SecurityOpt { + if opt == "no-new-privileges" { + s.ContainerSecurityConfig.NoNewPrivileges = true + } else { + con := strings.SplitN(opt, "=", 2) + if len(con) != 2 { + return fmt.Errorf("invalid --security-opt 1: %q", opt) + } + + switch con[0] { + case "label": + // TODO selinux opts and label opts are the same thing + s.ContainerSecurityConfig.SelinuxOpts = append(s.ContainerSecurityConfig.SelinuxOpts, con[1]) + case "apparmor": + s.ContainerSecurityConfig.ApparmorProfile = con[1] + case "seccomp": + s.SeccompProfilePath = con[1] + default: + return fmt.Errorf("invalid --security-opt 2: %q", opt) + } + } + } + + // TODO any idea why this was done + // storage.go from spec/ + // grab it + //volumes := rtc.Containers.Volumes + // TODO conflict on populate? + //if v := c.Volume; len(v)> 0 { + // s.Volumes = append(volumes, c.StringSlice("volume")...) + //} + //s.volu + + //s.Mounts = c.Mount + s.VolumesFrom = c.VolumesFrom + + // TODO any idea why this was done + //devices := rtc.Containers.Devices + // TODO conflict on populate? + // + //if c.Changed("device") { + // devices = append(devices, c.StringSlice("device")...) + //} + + // TODO things i cannot find in spec + // we dont think these are in the spec + // init - initbinary + // initpath + s.Stdin = c.Interactive + // quiet + //DeviceCgroupRules: c.StringSlice("device-cgroup-rule"), + + if bps := c.DeviceReadBPs; len(bps) > 0 { + if s.ThrottleReadBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return err + } + } + + if bps := c.DeviceWriteBPs; len(bps) > 0 { + if s.ThrottleWriteBpsDevice, err = parseThrottleBPSDevices(bps); err != nil { + return err + } + } + + if iops := c.DeviceReadIOPs; len(iops) > 0 { + if s.ThrottleReadIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return err + } + } + + if iops := c.DeviceWriteIOPs; len(iops) > 0 { + if s.ThrottleWriteIOPSDevice, err = parseThrottleIOPsDevices(iops); err != nil { + return err + } + } + + s.ResourceLimits.Memory.DisableOOMKiller = &c.OOMKillDisable + + // Rlimits/Ulimits + for _, u := range c.Ulimit { + if u == "host" { + s.Rlimits = nil + break + } + ul, err := units.ParseUlimit(u) + if err != nil { + return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u) + } + rl := specs.POSIXRlimit{ + Type: ul.Name, + Hard: uint64(ul.Hard), + Soft: uint64(ul.Soft), + } + s.Rlimits = append(s.Rlimits, rl) + } + + //Tmpfs: c.StringArray("tmpfs"), + + // TODO how to handle this? + //Syslog: c.Bool("syslog"), + + logOpts := make(map[string]string) + for _, o := range c.LogOptions { + split := strings.SplitN(o, "=", 2) + if len(split) < 2 { + return errors.Errorf("invalid log option %q", o) + } + logOpts[split[0]] = split[1] + } + s.LogConfiguration.Options = logOpts + s.Name = c.Name + + if err := parseWeightDevices(c.BlkIOWeightDevice, s); err != nil { + return err + } + + if s.ResourceLimits.CPU == nil { + s.ResourceLimits.CPU = &specs.LinuxCPU{} + } + s.ResourceLimits.CPU.Shares = &c.CPUShares + s.ResourceLimits.CPU.Period = &c.CPUPeriod + + // TODO research these + //s.ResourceLimits.CPU.Cpus = c.CPUS + //s.ResourceLimits.CPU.Cpus = c.CPUSetCPUs + + //s.ResourceLimits.CPU. = c.CPUSetCPUs + s.ResourceLimits.CPU.Mems = c.CPUSetMems + s.ResourceLimits.CPU.Quota = &c.CPUQuota + s.ResourceLimits.CPU.RealtimePeriod = &c.CPURTPeriod + s.ResourceLimits.CPU.RealtimeRuntime = &c.CPURTRuntime + s.OOMScoreAdj = &c.OOMScoreAdj + s.RestartPolicy = c.Restart + s.Remove = c.Rm + s.StopTimeout = &c.StopTimeout + + // TODO where should we do this? + //func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) { + return nil +} + +func makeHealthCheckFromCli(inCmd, interval string, retries uint, timeout, startPeriod string) (*manifest.Schema2HealthConfig, error) { + // Every healthcheck requires a command + if len(inCmd) == 0 { + return nil, errors.New("Must define a healthcheck command for all healthchecks") + } + + // first try to parse option value as JSON array of strings... + cmd := []string{} + err := json.Unmarshal([]byte(inCmd), &cmd) + if err != nil { + // ...otherwise pass it to "/bin/sh -c" inside the container + cmd = []string{"CMD-SHELL", inCmd} + } + hc := manifest.Schema2HealthConfig{ + Test: cmd, + } + + if interval == "disable" { + interval = "0" + } + intervalDuration, err := time.ParseDuration(interval) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-interval %s ", interval) + } + + hc.Interval = intervalDuration + + if retries < 1 { + return nil, errors.New("healthcheck-retries must be greater than 0.") + } + hc.Retries = int(retries) + timeoutDuration, err := time.ParseDuration(timeout) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-timeout %s", timeout) + } + if timeoutDuration < time.Duration(1) { + return nil, errors.New("healthcheck-timeout must be at least 1 second") + } + hc.Timeout = timeoutDuration + + startPeriodDuration, err := time.ParseDuration(startPeriod) + if err != nil { + return nil, errors.Wrapf(err, "invalid healthcheck-start-period %s", startPeriod) + } + if startPeriodDuration < time.Duration(0) { + return nil, errors.New("healthcheck-start-period must be 0 seconds or greater") + } + hc.StartPeriod = startPeriodDuration + + return &hc, nil +} + +func parseWeightDevices(weightDevs []string, s *specgen.SpecGenerator) error { + for _, val := range weightDevs { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return fmt.Errorf("bad format for device path: %s", val) + } + weight, err := strconv.ParseUint(split[1], 10, 0) + if err != nil { + return fmt.Errorf("invalid weight for device: %s", val) + } + if weight > 0 && (weight < 10 || weight > 1000) { + return fmt.Errorf("invalid weight for device: %s", val) + } + w := uint16(weight) + s.WeightDevice[split[0]] = specs.LinuxWeightDevice{ + Weight: &w, + LeafWeight: nil, + } + } + return nil +} + +func parseThrottleBPSDevices(bpsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range bpsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := units.RAMInBytes(split[1]) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + if rate < 0 { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>[<unit>]. Number must be a positive integer. Unit is optional and can be kb, mb, or gb", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: uint64(rate)} + } + return td, nil +} + +func parseThrottleIOPsDevices(iopsDevices []string) (map[string]specs.LinuxThrottleDevice, error) { + td := make(map[string]specs.LinuxThrottleDevice) + for _, val := range iopsDevices { + split := strings.SplitN(val, ":", 2) + if len(split) != 2 { + return nil, fmt.Errorf("bad format: %s", val) + } + if !strings.HasPrefix(split[0], "/dev/") { + return nil, fmt.Errorf("bad format for device path: %s", val) + } + rate, err := strconv.ParseUint(split[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val) + } + td[split[0]] = specs.LinuxThrottleDevice{Rate: rate} + } + return td, nil +} diff --git a/cmd/podmanV2/containers/checkpoint.go b/cmd/podmanV2/containers/checkpoint.go new file mode 100644 index 000000000..7c3e551bc --- /dev/null +++ b/cmd/podmanV2/containers/checkpoint.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + checkpointDescription = ` + podman container checkpoint + + Checkpoints one or more running containers. The container name or ID can be used. +` + checkpointCommand = &cobra.Command{ + Use: "checkpoint [flags] CONTAINER [CONTAINER...]", + Short: "Checkpoints one or more containers", + Long: checkpointDescription, + RunE: checkpoint, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman container checkpoint --keep ctrID + podman container checkpoint --all + podman container checkpoint --leave-running --latest`, + } +) + +var ( + checkpointOptions entities.CheckpointOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: checkpointCommand, + Parent: containerCmd, + }) + flags := checkpointCommand.Flags() + flags.BoolVarP(&checkpointOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files") + flags.BoolVarP(&checkpointOptions.LeaveRuninng, "leave-running", "R", false, "Leave the container running after writing checkpoint to disk") + flags.BoolVar(&checkpointOptions.TCPEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections") + flags.BoolVarP(&checkpointOptions.All, "all", "a", false, "Checkpoint all running containers") + flags.BoolVarP(&checkpointOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.StringVarP(&checkpointOptions.Export, "export", "e", "", "Export the checkpoint image to a tar.gz") + flags.BoolVar(&checkpointOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not include root file-system changes when exporting") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func checkpoint(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if rootless.IsRootless() { + return errors.New("checkpointing a container requires root") + } + if checkpointOptions.Export == "" && checkpointOptions.IgnoreRootFS { + return errors.Errorf("--ignore-rootfs can only be used with --export") + } + responses, err := registry.ContainerEngine().ContainerCheckpoint(context.Background(), args, checkpointOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} 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 <container_id> 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/containers/restore.go b/cmd/podmanV2/containers/restore.go new file mode 100644 index 000000000..6cab6ab50 --- /dev/null +++ b/cmd/podmanV2/containers/restore.go @@ -0,0 +1,104 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + restoreDescription = ` + podman container restore + + Restores a container from a checkpoint. The container name or ID can be used. +` + restoreCommand = &cobra.Command{ + Use: "restore [flags] CONTAINER [CONTAINER...]", + Short: "Restores one or more containers from a checkpoint", + Long: restoreDescription, + RunE: restore, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, true, false) + }, + Example: `podman container restore ctrID + podman container restore --latest + podman container restore --all`, + } +) + +var ( + restoreOptions entities.RestoreOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: restoreCommand, + Parent: containerCmd, + }) + flags := restoreCommand.Flags() + flags.BoolVarP(&restoreOptions.All, "all", "a", false, "Restore all checkpointed containers") + flags.BoolVarP(&restoreOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files") + flags.BoolVarP(&restoreOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&restoreOptions.TCPEstablished, "tcp-established", false, "Restore a container with established TCP connections") + flags.StringVarP(&restoreOptions.Import, "import", "i", "", "Restore from exported checkpoint archive (tar.gz)") + flags.StringVarP(&restoreOptions.Name, "name", "n", "", "Specify new name for container restored from exported checkpoint (only works with --import)") + flags.BoolVar(&restoreOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not apply root file-system changes when importing from exported checkpoint") + flags.BoolVar(&restoreOptions.IgnoreStaticIP, "ignore-static-ip", false, "Ignore IP address set via --static-ip") + flags.BoolVar(&restoreOptions.IgnoreStaticMAC, "ignore-static-mac", false, "Ignore MAC address set via --mac-address") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func restore(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if rootless.IsRootless() { + return errors.New("restoring a container requires root") + } + if restoreOptions.Import == "" && restoreOptions.IgnoreRootFS { + return errors.Errorf("--ignore-rootfs can only be used with --import") + } + if restoreOptions.Import == "" && restoreOptions.Name != "" { + return errors.Errorf("--name can only be used with --import") + } + if restoreOptions.Name != "" && restoreOptions.TCPEstablished { + return errors.Errorf("--tcp-established cannot be used with --name") + } + + argLen := len(args) + if restoreOptions.Import != "" { + if restoreOptions.All || restoreOptions.Latest { + return errors.Errorf("Cannot use --import with --all or --latest") + } + if argLen > 0 { + return errors.Errorf("Cannot use --import with positional arguments") + } + } + if (restoreOptions.All || restoreOptions.Latest) && argLen > 0 { + return errors.Errorf("no arguments are needed with --all or --latest") + } + if argLen < 1 && !restoreOptions.All && !restoreOptions.Latest && restoreOptions.Import == "" { + return errors.Errorf("you must provide at least one name or id") + } + responses, err := registry.ContainerEngine().ContainerRestore(context.Background(), args, restoreOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() + +} diff --git a/cmd/podmanV2/images/save.go b/cmd/podmanV2/images/save.go new file mode 100644 index 000000000..ae39b7bce --- /dev/null +++ b/cmd/podmanV2/images/save.go @@ -0,0 +1,87 @@ +package images + +import ( + "context" + "os" + "strings" + + "github.com/containers/libpod/libpod/define" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" +) + +var validFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive} + +var ( + saveDescription = `Save an image to docker-archive or oci-archive on the local machine. Default is docker-archive.` + + saveCommand = &cobra.Command{ + Use: "save [flags] IMAGE", + Short: "Save image to an archive", + Long: saveDescription, + PersistentPreRunE: preRunE, + RunE: save, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.Errorf("need at least 1 argument") + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if !util.StringInSlice(format, validFormats) { + return errors.Errorf("format value must be one of %s", strings.Join(validFormats, " ")) + } + return nil + }, + Example: `podman save --quiet -o myimage.tar imageID + podman save --format docker-dir -o ubuntu-dir ubuntu + podman save > alpine-all.tar alpine:latest`, + } +) + +var ( + saveOpts entities.ImageSaveOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: saveCommand, + }) + flags := saveCommand.Flags() + flags.BoolVar(&saveOpts.Compress, "compress", false, "Compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)") + flags.StringVar(&saveOpts.Format, "format", define.V2s2Archive, "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-archive, docker-dir (directory with v2s2 manifest type)") + flags.StringVarP(&saveOpts.Output, "output", "o", "", "Write to a specified file (default: stdout, which must be redirected)") + flags.BoolVarP(&saveOpts.Quiet, "quiet", "q", false, "Suppress the output") + +} + +func save(cmd *cobra.Command, args []string) error { + var ( + tags []string + ) + if cmd.Flag("compress").Changed && (saveOpts.Format != define.OCIManifestDir && saveOpts.Format != define.V2s2ManifestDir && saveOpts.Format == "") { + return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'") + } + if len(saveOpts.Output) == 0 { + fi := os.Stdout + if terminal.IsTerminal(int(fi.Fd())) { + return errors.Errorf("refusing to save to terminal. Use -o flag or redirect") + } + saveOpts.Output = "/dev/stdout" + } + if err := parse.ValidateFileName(saveOpts.Output); err != nil { + return err + } + if len(args) > 1 { + tags = args[1:] + } + return registry.ImageEngine().Save(context.Background(), args[0], tags, saveOpts) +} 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/parse.go b/cmd/podmanV2/parse/net.go index 10d2146fa..03cda268c 100644 --- a/cmd/podmanV2/parse/parse.go +++ b/cmd/podmanV2/parse/net.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/pkg/errors" - "github.com/spf13/cobra" ) const ( @@ -187,47 +186,3 @@ func ValidURL(urlStr string) error { } 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/net_test.go index a6ddc2be9..a6ddc2be9 100644 --- a/cmd/podmanV2/parse/parse_test.go +++ b/cmd/podmanV2/parse/net_test.go diff --git a/libpod/define/config.go b/libpod/define/config.go index 5598f97a3..7b967f17d 100644 --- a/libpod/define/config.go +++ b/libpod/define/config.go @@ -26,3 +26,10 @@ type InfoData struct { // VolumeDriverLocal is the "local" volume driver. It is managed by libpod // itself. const VolumeDriverLocal = "local" + +const ( + OCIManifestDir = "oci-dir" + OCIArchive = "oci-archive" + V2s2ManifestDir = "docker-dir" + V2s2Archive = "docker-archive" +) diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index 6a0097b8e..2e96dbe57 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -353,6 +353,9 @@ func (r *ConmonOCIRuntime) StartContainer(ctr *Container) error { if notify, ok := os.LookupEnv("NOTIFY_SOCKET"); ok { env = append(env, fmt.Sprintf("NOTIFY_SOCKET=%s", notify)) } + if path, ok := os.LookupEnv("PATH"); ok { + env = append(env, fmt.Sprintf("PATH=%s", path)) + } if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "start", ctr.ID()); err != nil { return err } @@ -930,6 +933,13 @@ func (r *ConmonOCIRuntime) CheckpointContainer(ctr *Container, options Container if options.TCPEstablished { args = append(args, "--tcp-established") } + runtimeDir, err := util.GetRuntimeDir() + if err != nil { + return err + } + if err = os.Setenv("XDG_RUNTIME_DIR", runtimeDir); err != nil { + return errors.Wrapf(err, "cannot set XDG_RUNTIME_DIR") + } args = append(args, ctr.ID()) return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...) } @@ -939,7 +949,7 @@ func (r *ConmonOCIRuntime) CheckpointContainer(ctr *Container, options Container func (r *ConmonOCIRuntime) SupportsCheckpoint() bool { // Check if the runtime implements checkpointing. Currently only // runc's checkpoint/restore implementation is supported. - cmd := exec.Command(r.path, "checkpoint", "-h") + cmd := exec.Command(r.path, "checkpoint", "--help") if err := cmd.Start(); err != nil { return false } diff --git a/libpod/options.go b/libpod/options.go index dfbec364a..65a089131 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1334,7 +1334,7 @@ func WithNamedVolumes(volumes []*ContainerNamedVolume) CtrCreateOption { } destinations[vol.Dest] = true - mountOpts, err := util.ProcessOptions(vol.Options, false, nil) + mountOpts, err := util.ProcessOptions(vol.Options, false, "") if err != nil { return errors.Wrapf(err, "error processing options for named volume %q mounted at %q", vol.Name, vol.Dest) } diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index c395ffc7f..b4ebeb944 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -26,6 +26,7 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/checkpoint" envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/systemd/generate" "github.com/containers/storage" @@ -625,7 +626,7 @@ func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues) switch { case c.Import != "": - containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) + containers, err = checkpoint.CRImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) case c.All: containers, err = r.GetContainers(filterFuncs...) default: diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index cdc34004f..fde72552b 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,16 +1,21 @@ package libpod import ( + "io/ioutil" "net/http" + "os" "path/filepath" "sort" "strconv" "time" + "github.com/containers/libpod/pkg/api/handlers/compat" + "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -325,3 +330,129 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts shared.P } return ps, nil } + +func Checkpoint(w http.ResponseWriter, r *http.Request) { + var targetFile string + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Keep bool `schema:"keep"` + LeaveRunning bool `schema:"leaveRunning"` + TCPEstablished bool `schema:"tcpEstablished"` + Export bool `schema:"export"` + IgnoreRootFS bool `schema:"ignoreRootFS"` + }{ + // override any golang type defaults + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if query.Export { + tmpFile, err := ioutil.TempFile("", "checkpoint") + if err != nil { + utils.InternalServerError(w, err) + return + } + defer os.Remove(tmpFile.Name()) + if err := tmpFile.Close(); err != nil { + utils.InternalServerError(w, err) + return + } + targetFile = tmpFile.Name() + } + options := libpod.ContainerCheckpointOptions{ + Keep: query.Keep, + KeepRunning: query.LeaveRunning, + TCPEstablished: query.TCPEstablished, + IgnoreRootfs: query.IgnoreRootFS, + } + if query.Export { + options.TargetFile = targetFile + } + err = ctr.Checkpoint(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + return + } + if query.Export { + f, err := os.Open(targetFile) + if err != nil { + utils.InternalServerError(w, err) + return + } + defer f.Close() + utils.WriteResponse(w, http.StatusOK, f) + return + } + utils.WriteResponse(w, http.StatusOK, entities.CheckpointReport{Id: ctr.ID()}) +} + +func Restore(w http.ResponseWriter, r *http.Request) { + var ( + targetFile string + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Keep bool `schema:"keep"` + TCPEstablished bool `schema:"tcpEstablished"` + Import bool `schema:"import"` + Name string `schema:"name"` + IgnoreRootFS bool `schema:"ignoreRootFS"` + IgnoreStaticIP bool `schema:"ignoreStaticIP"` + IgnoreStaticMAC bool `schema:"ignoreStaticMAC"` + }{ + // override any golang type defaults + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if query.Import { + t, err := ioutil.TempFile("", "restore") + if err != nil { + utils.InternalServerError(w, err) + return + } + defer t.Close() + if err := compat.SaveFromBody(t, r); err != nil { + utils.InternalServerError(w, err) + return + } + targetFile = t.Name() + } + + options := libpod.ContainerCheckpointOptions{ + Keep: query.Keep, + TCPEstablished: query.TCPEstablished, + IgnoreRootfs: query.IgnoreRootFS, + IgnoreStaticIP: query.IgnoreStaticIP, + IgnoreStaticMAC: query.IgnoreStaticMAC, + } + if query.Import { + options.TargetFile = targetFile + options.Name = query.Name + } + err = ctr.Restore(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, entities.RestoreReport{Id: ctr.ID()}) +} 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/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index e7f20854c..850de4598 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -16,12 +16,14 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" image2 "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/util" + utils2 "github.com/containers/libpod/utils" "github.com/gorilla/schema" "github.com/pkg/errors" ) @@ -161,13 +163,16 @@ func PruneImages(w http.ResponseWriter, r *http.Request) { } func ExportImage(w http.ResponseWriter, r *http.Request) { + var ( + output string + ) runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { Compress bool `schema:"compress"` Format string `schema:"format"` }{ - Format: "docker-archive", + Format: define.OCIArchive, } if err := decoder.Decode(&query, r.URL.Query()); err != nil { @@ -175,14 +180,27 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) return } - - tmpfile, err := ioutil.TempFile("", "api.tar") - if err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) - return - } - if err := tmpfile.Close(); err != nil { - utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) + switch query.Format { + case define.OCIArchive, define.V2s2Archive: + tmpfile, err := ioutil.TempFile("", "api.tar") + if err != nil { + utils.Error(w, "unable to create tmpfile", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempfile")) + return + } + output = tmpfile.Name() + if err := tmpfile.Close(); err != nil { + utils.Error(w, "unable to close tmpfile", http.StatusInternalServerError, errors.Wrap(err, "unable to close tempfile")) + return + } + case define.OCIManifestDir, define.V2s2ManifestDir: + tmpdir, err := ioutil.TempDir("", "save") + if err != nil { + utils.Error(w, "unable to create tmpdir", http.StatusInternalServerError, errors.Wrap(err, "unable to create tempdir")) + return + } + output = tmpdir + default: + utils.Error(w, "unknown format", http.StatusInternalServerError, errors.Errorf("unknown format %q", query.Format)) return } name := utils.GetName(r) @@ -192,17 +210,28 @@ func ExportImage(w http.ResponseWriter, r *http.Request) { return } - if err := newImage.Save(r.Context(), name, query.Format, tmpfile.Name(), []string{}, false, query.Compress); err != nil { + if err := newImage.Save(r.Context(), name, query.Format, output, []string{}, false, query.Compress); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, err) return } - rdr, err := os.Open(tmpfile.Name()) + defer os.RemoveAll(output) + // if dir format, we need to tar it + if query.Format == "oci-dir" || query.Format == "docker-dir" { + rdr, err := utils2.Tar(output) + if err != nil { + utils.InternalServerError(w, err) + return + } + defer rdr.Close() + utils.WriteResponse(w, http.StatusOK, rdr) + return + } + rdr, err := os.Open(output) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to read the exported tarfile")) return } defer rdr.Close() - defer os.Remove(tmpfile.Name()) utils.WriteResponse(w, http.StatusOK, rdr) } diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 32b8c5b0a..b5bd488fb 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -46,6 +46,13 @@ func WriteResponse(w http.ResponseWriter, code int, value interface{}) { if _, err := io.Copy(w, v); err != nil { logrus.Errorf("unable to copy to response: %q", err) } + case io.Reader: + w.Header().Set("Content-Type", "application/x-tar") + w.WriteHeader(code) + + if _, err := io.Copy(w, v); err != nil { + logrus.Errorf("unable to copy to response: %q", err) + } default: WriteJSON(w, code, value) } diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 145c054c0..f126112d0 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -1282,5 +1282,100 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/export"), s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) + // swagger:operation GET /libpod/containers/{name}/checkout libpod libpodCheckpointContainer + // --- + // tags: + // - containers + // summary: Checkpoint a container + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // - in: query + // name: keep + // type: boolean + // description: keep all temporary checkpoint files + // - in: query + // name: leaveRunning + // type: boolean + // description: leave the container running after writing checkpoint to disk + // - in: query + // name: tcpEstablished + // type: boolean + // description: checkpoint a container with established TCP connections + // - in: query + // name: export + // type: boolean + // description: export the checkpoint image to a tar.gz + // - in: query + // name: ignoreRootFS + // type: boolean + // description: do not include root file-system changes when exporting + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body if exported + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/checkpoint"), s.APIHandler(libpod.Checkpoint)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/{name} restore libpod libpodRestoreContainer + // --- + // tags: + // - containers + // summary: Restore a container + // description: Restore a container from a checkpoint. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or id of the container + // - in: query + // name: name + // type: string + // description: the name of the container when restored from a tar. can only be used with import + // - in: query + // name: keep + // type: boolean + // description: keep all temporary checkpoint files + // - in: query + // name: leaveRunning + // type: boolean + // description: leave the container running after writing checkpoint to disk + // - in: query + // name: tcpEstablished + // type: boolean + // description: checkpoint a container with established TCP connections + // - in: query + // name: import + // type: boolean + // description: import the restore from a checkpoint tar.gz + // - in: query + // name: ignoreRootFS + // type: boolean + // description: do not include root file-system changes when exporting + // - in: query + // name: ignoreStaticIP + // type: boolean + // description: ignore IP address if set statically + // - in: query + // name: ignoreStaticMAC + // type: boolean + // description: ignore MAC address if set statically + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body if exported + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/restore"), s.APIHandler(libpod.Restore)).Methods(http.MethodPost) return nil } diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index e4e46025b..d45423096 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -955,7 +955,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // tags: // - images // summary: Export an image - // description: Export an image as a tarball + // description: Export an image // parameters: // - in: path // name: name:.* diff --git a/pkg/bindings/containers/checkpoint.go b/pkg/bindings/containers/checkpoint.go new file mode 100644 index 000000000..84924587b --- /dev/null +++ b/pkg/bindings/containers/checkpoint.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" +) + +// Checkpoint checkpoints the given container (identified by nameOrId). All additional +// options are options and allow for more fine grained control of the checkpoint process. +func Checkpoint(ctx context.Context, nameOrId string, keep, leaveRunning, tcpEstablished, ignoreRootFS *bool, export *string) (*entities.CheckpointReport, error) { + var report entities.CheckpointReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if keep != nil { + params.Set("keep", strconv.FormatBool(*keep)) + } + if leaveRunning != nil { + params.Set("leaveRunning", strconv.FormatBool(*leaveRunning)) + } + if tcpEstablished != nil { + params.Set("TCPestablished", strconv.FormatBool(*tcpEstablished)) + } + if ignoreRootFS != nil { + params.Set("ignoreRootFS", strconv.FormatBool(*ignoreRootFS)) + } + if export != nil { + params.Set("export", *export) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/checkpoint", params, nameOrId) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} + +// Restore restores a checkpointed container to running. The container is identified by the nameOrId option. All +// additional options are optional and allow finer control of the restore processs. +func Restore(ctx context.Context, nameOrId string, keep, tcpEstablished, ignoreRootFS, ignoreStaticIP, ignoreStaticMAC *bool, name, importArchive *string) (*entities.RestoreReport, error) { + var report entities.RestoreReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if keep != nil { + params.Set("keep", strconv.FormatBool(*keep)) + } + if tcpEstablished != nil { + params.Set("TCPestablished", strconv.FormatBool(*tcpEstablished)) + } + if ignoreRootFS != nil { + params.Set("ignoreRootFS", strconv.FormatBool(*ignoreRootFS)) + } + if ignoreStaticIP != nil { + params.Set("ignoreStaticIP", strconv.FormatBool(*ignoreStaticIP)) + } + if ignoreStaticMAC != nil { + params.Set("ignoreStaticMAC", strconv.FormatBool(*ignoreStaticMAC)) + } + if name != nil { + params.Set("name", *name) + } + if importArchive != nil { + params.Set("import", *importArchive) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restore", params, nameOrId) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index dcb568d6b..1b3df609b 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -146,11 +146,12 @@ func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, c if err != nil { return err } - if err := response.Process(nil); err != nil { + + if response.StatusCode/100 == 2 || response.StatusCode/100 == 3 { + _, err = io.Copy(w, response.Body) return err } - _, err = io.Copy(w, response.Body) - return err + return nil } // Prune removes unused images from local storage. The optional filters can be used to further diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index a5b74013b..78f592d32 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -1,6 +1,4 @@ -// +build !remoteclient - -package adapter +package checkpoint import ( "context" @@ -42,9 +40,9 @@ func crImportFromJSON(filePath string, v interface{}) error { return nil } -// crImportCheckpoint it the function which imports the information +// CRImportCheckpoint it the function which imports the information // from checkpoint tarball and re-creates the container from that information -func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { +func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { // First get the container definition from the // tarball to a temporary directory archiveFile, err := os.Open(input) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index d51124f55..74b23cd71 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -121,3 +121,39 @@ type CommitReport struct { type ContainerExportOptions struct { Output string } + +type CheckpointOptions struct { + All bool + Export string + IgnoreRootFS bool + Keep bool + Latest bool + LeaveRuninng bool + TCPEstablished bool +} + +type CheckpointReport struct { + Err error + Id string +} + +type RestoreOptions struct { + All bool + IgnoreRootFS bool + IgnoreStaticIP bool + IgnoreStaticMAC bool + Import string + Keep bool + Latest bool + Name string + TCPEstablished bool +} + +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 a122857cd..025da50f3 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -4,10 +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/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 04b9d34e6..a28bfc548 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -17,4 +17,5 @@ type ImageEngine interface { Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error) Import(ctx context.Context, opts ImageImportOptions) (*ImageImportReport, error) Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error + Save(ctx context.Context, nameOrId string, tags []string, options ImageSaveOptions) error } diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index d66de3c5e..bc8a34c13 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -234,3 +234,10 @@ type ImageImportOptions struct { type ImageImportReport struct { Id string } + +type ImageSaveOptions struct { + Compress bool + Format string + Output string + Quiet bool +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d4c5ac311..d3d51db82 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -13,12 +13,44 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/adapter/shortcuts" + "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" ) +// getContainersByContext gets pods whether all, latest, or a slice of names/ids +// is specified. +func getContainersByContext(all, latest bool, names []string, runtime *libpod.Runtime) (ctrs []*libpod.Container, err error) { + var ctr *libpod.Container + ctrs = []*libpod.Container{} + + switch { + case all: + ctrs, err = runtime.GetAllContainers() + case latest: + ctr, err = runtime.GetLatestContainer() + ctrs = append(ctrs, ctr) + default: + for _, n := range names { + ctr, e := runtime.LookupContainer(n) + if e != nil { + // Log all errors here, so callers don't need to. + logrus.Debugf("Error looking up container %q: %v", n, e) + if err == nil { + err = e + } + } else { + ctrs = append(ctrs, ctr) + } + } + } + return +} + // TODO: Should return *entities.ContainerExistsReport, error func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { _, err := ic.Libpod.LookupContainer(nameOrId) @@ -333,3 +365,93 @@ func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, } return ctr.Export(options.Output) } + +func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options entities.CheckpointOptions) ([]*entities.CheckpointReport, error) { + var ( + err error + cons []*libpod.Container + reports []*entities.CheckpointReport + ) + checkOpts := libpod.ContainerCheckpointOptions{ + Keep: options.Keep, + TCPEstablished: options.TCPEstablished, + TargetFile: options.Export, + IgnoreRootfs: options.IgnoreRootFS, + } + + if options.All { + running := func(c *libpod.Container) bool { + state, _ := c.State() + return state == define.ContainerStateRunning + } + cons, err = ic.Libpod.GetContainers(running) + } else { + cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, con := range cons { + err = con.Checkpoint(ctx, checkOpts) + reports = append(reports, &entities.CheckpointReport{ + Err: err, + Id: con.ID(), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) { + var ( + cons []*libpod.Container + err error + filterFuncs []libpod.ContainerFilter + reports []*entities.RestoreReport + ) + + restoreOptions := libpod.ContainerCheckpointOptions{ + Keep: options.Keep, + TCPEstablished: options.TCPEstablished, + TargetFile: options.Import, + Name: options.Name, + IgnoreRootfs: options.IgnoreRootFS, + IgnoreStaticIP: options.IgnoreStaticIP, + IgnoreStaticMAC: options.IgnoreStaticMAC, + } + + filterFuncs = append(filterFuncs, func(c *libpod.Container) bool { + state, _ := c.State() + return state == define.ContainerStateExited + }) + + switch { + case options.Import != "": + cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options.Import, options.Name) + case options.All: + cons, err = ic.Libpod.GetContainers(filterFuncs...) + default: + cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, con := range cons { + err := con.Restore(ctx, restoreOptions) + reports = append(reports, &entities.RestoreReport{ + Err: err, + Id: con.ID(), + }) + } + 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/abi/images.go b/pkg/domain/infra/abi/images.go index 94008f287..9d706a112 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -405,3 +405,11 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti } return &entities.ImageImportReport{Id: id}, nil } + +func (ir *ImageEngine) Save(ctx context.Context, nameOrId string, tags []string, options entities.ImageSaveOptions) error { + newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(nameOrId) + if err != nil { + return err + } + return newImage.Save(ctx, nameOrId, options.Format, options.Output, tags, options.Quiet, options.Compress) +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 8885ae7c7..ae8994cba 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -6,9 +6,11 @@ import ( "os" "github.com/containers/image/v5/docker/reference" - + "github.com/containers/libpod/libpod/define" + "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" ) @@ -226,3 +228,80 @@ func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, } return containers.Export(ic.ClientCxt, nameOrId, w) } + +func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options entities.CheckpointOptions) ([]*entities.CheckpointReport, error) { + var ( + reports []*entities.CheckpointReport + err error + ctrs []libpod.ListContainer + ) + + if options.All { + allCtrs, err := getContainersByContext(ic.ClientCxt, true, []string{}) + if err != nil { + return nil, err + } + // narrow the list to running only + for _, c := range allCtrs { + if c.State == define.ContainerStateRunning.String() { + ctrs = append(ctrs, c) + } + } + + } else { + ctrs, err = getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + } + for _, c := range ctrs { + report, err := containers.Checkpoint(ic.ClientCxt, c.ID, &options.Keep, &options.LeaveRuninng, &options.TCPEstablished, &options.IgnoreRootFS, &options.Export) + if err != nil { + reports = append(reports, &entities.CheckpointReport{Id: c.ID, Err: err}) + } + reports = append(reports, report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) { + var ( + reports []*entities.RestoreReport + err error + ctrs []libpod.ListContainer + ) + if options.All { + allCtrs, err := getContainersByContext(ic.ClientCxt, true, []string{}) + if err != nil { + return nil, err + } + // narrow the list to exited only + for _, c := range allCtrs { + if c.State == define.ContainerStateExited.String() { + ctrs = append(ctrs, c) + } + } + + } else { + ctrs, err = getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + } + for _, c := range ctrs { + report, err := containers.Restore(ic.ClientCxt, c.ID, &options.Keep, &options.TCPEstablished, &options.IgnoreRootFS, &options.IgnoreStaticIP, &options.IgnoreStaticMAC, &options.Name, &options.Import) + if err != nil { + reports = append(reports, &entities.RestoreReport{Id: c.ID, Err: err}) + } + reports = append(reports, report) + } + 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/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 028603d98..516914a68 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -2,12 +2,14 @@ package tunnel import ( "context" + "io/ioutil" "os" "github.com/containers/image/v5/docker/reference" images "github.com/containers/libpod/pkg/bindings/images" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/utils" + utils2 "github.com/containers/libpod/utils" "github.com/pkg/errors" ) @@ -188,3 +190,54 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error { return images.Push(ir.ClientCxt, source, destination, options) } + +func (ir *ImageEngine) Save(ctx context.Context, nameOrId string, tags []string, options entities.ImageSaveOptions) error { + var ( + f *os.File + err error + ) + + switch options.Format { + case "oci-dir", "docker-dir": + f, err = ioutil.TempFile("", "podman_save") + if err == nil { + defer func() { _ = os.Remove(f.Name()) }() + } + default: + f, err = os.Create(options.Output) + } + if err != nil { + return err + } + + exErr := images.Export(ir.ClientCxt, nameOrId, f, &options.Format, &options.Compress) + if err := f.Close(); err != nil { + return err + } + if exErr != nil { + return exErr + } + + if options.Format != "oci-dir" && options.Format != "docker-dir" { + return nil + } + + f, err = os.Open(f.Name()) + if err != nil { + return err + } + info, err := os.Stat(options.Output) + switch { + case err == nil: + if info.Mode().IsRegular() { + return errors.Errorf("%q already exists as a regular file", options.Output) + } + case os.IsNotExist(err): + if err := os.Mkdir(options.Output, 0755); err != nil { + return err + } + default: + return err + } + return utils2.UntarToFileSystem(options.Output, f, nil) +} diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 4732af757..5de07fc28 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -381,11 +381,9 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM // BIND MOUNTS configSpec.Mounts = SupercedeUserMounts(userMounts, configSpec.Mounts) // Process mounts to ensure correct options - finalMounts, err := InitFSMounts(configSpec.Mounts) - if err != nil { + if err := InitFSMounts(configSpec.Mounts); err != nil { return nil, err } - configSpec.Mounts = finalMounts // BLOCK IO blkio, err := config.CreateBlockIO() diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index b0687b4c2..68a84d638 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -10,7 +10,6 @@ import ( "github.com/containers/buildah/pkg/parse" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/util" - pmount "github.com/containers/storage/pkg/mount" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -855,75 +854,22 @@ func SupercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.M } // Ensure mount options on all mounts are correct -func InitFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) { - // We need to look up mounts so we can figure out the proper mount flags - // to apply. - systemMounts, err := pmount.GetMounts() - if err != nil { - return nil, errors.Wrapf(err, "error retrieving system mounts to look up mount options") - } - - // TODO: We probably don't need to re-build the mounts array - var mounts []spec.Mount - for _, m := range inputMounts { - if m.Type == TypeBind { - baseMnt, err := findMount(m.Destination, systemMounts) +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 nil, errors.Wrapf(err, "error looking up mountpoint for mount %s", m.Destination) - } - var noexec, nosuid, nodev bool - for _, baseOpt := range strings.Split(baseMnt.Opts, ",") { - switch baseOpt { - case "noexec": - noexec = true - case "nosuid": - nosuid = true - case "nodev": - nodev = true - } + return err } - - defaultMountOpts := new(util.DefaultMountOptions) - defaultMountOpts.Noexec = noexec - defaultMountOpts.Nosuid = nosuid - defaultMountOpts.Nodev = nodev - - opts, err := util.ProcessOptions(m.Options, false, defaultMountOpts) + mounts[i].Options = opts + case m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev": + opts, err := util.ProcessOptions(m.Options, true, "") if err != nil { - return nil, err + return err } - m.Options = opts - } - if m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev" { - opts, err := util.ProcessOptions(m.Options, true, nil) - if err != nil { - return nil, err - } - m.Options = opts - } - - mounts = append(mounts, m) - } - return mounts, nil -} - -// TODO: We could make this a bit faster by building a tree of the mountpoints -// and traversing it to identify the correct mount. -func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) { - var err error - target, err = filepath.Abs(target) - if err != nil { - return nil, errors.Wrapf(err, "cannot resolve %s", target) - } - var bestSoFar *pmount.Info - for _, i := range mounts { - if bestSoFar != nil && len(bestSoFar.Mountpoint) > len(i.Mountpoint) { - // Won't be better than what we have already found - continue - } - if strings.HasPrefix(target, i.Mountpoint) { - bestSoFar = i + mounts[i].Options = opts } } - return bestSoFar, nil + return 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_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/container_create.go b/pkg/specgen/generate/container_create.go index b4039bb91..aad59a861 100644 --- a/pkg/specgen/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -1,4 +1,4 @@ -package specgen +package generate import ( "context" @@ -7,14 +7,15 @@ import ( "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 (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, error) { - if err := s.validate(); err != nil { +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() @@ -22,7 +23,7 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er return nil, err } - options, err := s.createContainerOptions(rt) + options, err := createContainerOptions(rt, s) if err != nil { return nil, err } @@ -31,7 +32,7 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er if err != nil { return nil, err } - options = append(options, s.createExitCommandOption(rt.StorageConfig(), rtc, podmanPath)) + options = append(options, createExitCommandOption(s, rt.StorageConfig(), rtc, podmanPath)) newImage, err := rt.ImageRuntime().NewFromLocal(s.Image) if err != nil { return nil, err @@ -39,14 +40,14 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) - runtimeSpec, err := s.toOCISpec(rt, newImage) + 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) { +func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -114,7 +115,7 @@ func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime) ([]libpod.Ctr options = append(options, libpod.WithPrivileged(s.Privileged)) // Get namespace related options - namespaceOptions, err := s.generateNamespaceContainerOpts(rt) + namespaceOptions, err := s.GenerateNamespaceContainerOpts(rt) if err != nil { return nil, err } @@ -149,7 +150,7 @@ func (s *SpecGenerator) createContainerOptions(rt *libpod.Runtime) ([]libpod.Ctr return options, nil } -func (s *SpecGenerator) createExitCommandOption(storageConfig storage.StoreOptions, config *config.Config, podmanPath string) libpod.CtrCreateOption { +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. 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 2523f21b3..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 ) @@ -215,11 +215,9 @@ func (s *SpecGenerator) toOCISpec(rt *libpod.Runtime, newImage *image.Image) (*s // BIND MOUNTS configSpec.Mounts = createconfig.SupercedeUserMounts(s.Mounts, configSpec.Mounts) // Process mounts to ensure correct options - finalMounts, err := createconfig.InitFSMounts(configSpec.Mounts) - if err != nil { + if err := createconfig.InitFSMounts(configSpec.Mounts); err != nil { return nil, err } - configSpec.Mounts = finalMounts // Add annotations if configSpec.Annotations == nil { 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=<bind|tmpfs|volume>,[src=<host-dir|volume-name>,]target=<ctr-dir>[,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 +} diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index d21800bc3..329a7c913 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -13,19 +13,17 @@ var ( ErrDupeMntOption = errors.Errorf("duplicate mount option passed") ) -// DefaultMountOptions sets default mount options for ProcessOptions. -type DefaultMountOptions struct { - Noexec bool - Nosuid bool - Nodev bool +type defaultMountOptions struct { + noexec bool + nosuid bool + nodev bool } // ProcessOptions parses the options for a bind or tmpfs mount and ensures that // they are sensible and follow convention. The isTmpfs variable controls // whether extra, tmpfs-specific options will be allowed. -// The defaults variable controls default mount options that will be set. If it -// is not included, they will be set unconditionally. -func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOptions) ([]string, error) { +// The sourcePath variable, if not empty, contains a bind mount source. +func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, error) { var ( foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ bool ) @@ -122,13 +120,17 @@ func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOption if !foundProp { newOptions = append(newOptions, "rprivate") } - if !foundExec && (defaults == nil || defaults.Noexec) { + defaults, err := getDefaultMountOptions(sourcePath) + if err != nil { + return nil, err + } + if !foundExec && defaults.noexec { newOptions = append(newOptions, "noexec") } - if !foundSuid && (defaults == nil || defaults.Nosuid) { + if !foundSuid && defaults.nosuid { newOptions = append(newOptions, "nosuid") } - if !foundDev && (defaults == nil || defaults.Nodev) { + if !foundDev && defaults.nodev { newOptions = append(newOptions, "nodev") } if isTmpfs && !foundCopyUp { diff --git a/pkg/util/mountOpts_linux.go b/pkg/util/mountOpts_linux.go new file mode 100644 index 000000000..3eac4dd25 --- /dev/null +++ b/pkg/util/mountOpts_linux.go @@ -0,0 +1,23 @@ +package util + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func getDefaultMountOptions(path string) (defaultMountOptions, error) { + opts := defaultMountOptions{true, true, true} + if path == "" { + return opts, nil + } + var statfs unix.Statfs_t + if e := unix.Statfs(path, &statfs); e != nil { + return opts, &os.PathError{Op: "statfs", Path: path, Err: e} + } + opts.nodev = (statfs.Flags&unix.MS_NODEV == unix.MS_NODEV) + opts.noexec = (statfs.Flags&unix.MS_NOEXEC == unix.MS_NOEXEC) + opts.nosuid = (statfs.Flags&unix.MS_NOSUID == unix.MS_NOSUID) + + return opts, nil +} diff --git a/pkg/util/mountOpts_other.go b/pkg/util/mountOpts_other.go new file mode 100644 index 000000000..6a34942e5 --- /dev/null +++ b/pkg/util/mountOpts_other.go @@ -0,0 +1,7 @@ +// +build !linux + +package util + +func getDefaultMountOptions(path string) (opts defaultMountOptions, err error) { + return +} diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index 237223283..e6a3d2f7a 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -37,7 +37,7 @@ var _ = Describe("Podman checkpoint", func() { podmanTest.SeedImages() // Check if the runtime implements checkpointing. Currently only // runc's checkpoint/restore implementation is supported. - cmd := exec.Command(podmanTest.OCIRuntime, "checkpoint", "-h") + cmd := exec.Command(podmanTest.OCIRuntime, "checkpoint", "--help") if err := cmd.Start(); err != nil { Skip("OCI runtime does not support checkpoint/restore") } |