aboutsummaryrefslogtreecommitdiff
path: root/cmd/podman
diff options
context:
space:
mode:
authorDaniel J Walsh <dwalsh@redhat.com>2017-12-15 16:58:36 -0500
committerAtomic Bot <atomic-devel@projectatomic.io>2017-12-18 16:46:05 +0000
commit5770dc2640c216525ab84031e3712fcc46b3b087 (patch)
tree8a1c5c4e4a6ce6a35a3767247623a62bfd698f77 /cmd/podman
parentde3468e120d489d046c08dad72ba2262e222ccb1 (diff)
downloadpodman-5770dc2640c216525ab84031e3712fcc46b3b087.tar.gz
podman-5770dc2640c216525ab84031e3712fcc46b3b087.tar.bz2
podman-5770dc2640c216525ab84031e3712fcc46b3b087.zip
Rename all references to kpod to podman
The decision is in, kpod is going to be named podman. Signed-off-by: Daniel J Walsh <dwalsh@redhat.com> Closes: #145 Approved by: umohnani8
Diffstat (limited to 'cmd/podman')
-rw-r--r--cmd/podman/README.md16
-rw-r--r--cmd/podman/attach.go86
-rw-r--r--cmd/podman/common.go438
-rw-r--r--cmd/podman/common_test.go41
-rw-r--r--cmd/podman/create.go507
-rw-r--r--cmd/podman/create_cli.go242
-rw-r--r--cmd/podman/create_cli_test.go70
-rw-r--r--cmd/podman/diff.go128
-rw-r--r--cmd/podman/docker/types.go271
-rw-r--r--cmd/podman/exec.go86
-rw-r--r--cmd/podman/export.go65
-rw-r--r--cmd/podman/formats/formats.go143
-rw-r--r--cmd/podman/formats/templates.go78
-rw-r--r--cmd/podman/history.go246
-rw-r--r--cmd/podman/images.go337
-rw-r--r--cmd/podman/import.go190
-rw-r--r--cmd/podman/info.go84
-rw-r--r--cmd/podman/inspect.go364
-rw-r--r--cmd/podman/kill.go80
-rw-r--r--cmd/podman/load.go123
-rw-r--r--cmd/podman/login.go110
-rw-r--r--cmd/podman/logout.go69
-rw-r--r--cmd/podman/logs.go153
-rw-r--r--cmd/podman/main.go161
-rw-r--r--cmd/podman/mount.go125
-rw-r--r--cmd/podman/parse.go863
-rw-r--r--cmd/podman/pause.go58
-rw-r--r--cmd/podman/ps.go606
-rw-r--r--cmd/podman/pull.go120
-rw-r--r--cmd/podman/push.go167
-rw-r--r--cmd/podman/rm.go90
-rw-r--r--cmd/podman/rmi.go75
-rw-r--r--cmd/podman/run.go146
-rw-r--r--cmd/podman/save.go129
-rw-r--r--cmd/podman/spec.go561
-rw-r--r--cmd/podman/spec_test.go39
-rw-r--r--cmd/podman/start.go131
-rw-r--r--cmd/podman/stats.go226
-rw-r--r--cmd/podman/stop.go104
-rw-r--r--cmd/podman/tag.go77
-rw-r--r--cmd/podman/top.go258
-rw-r--r--cmd/podman/umount.go40
-rw-r--r--cmd/podman/unpause.go58
-rw-r--r--cmd/podman/user.go121
-rw-r--r--cmd/podman/version.go48
-rw-r--r--cmd/podman/wait.go61
46 files changed, 8191 insertions, 0 deletions
diff --git a/cmd/podman/README.md b/cmd/podman/README.md
new file mode 100644
index 000000000..6978b056a
--- /dev/null
+++ b/cmd/podman/README.md
@@ -0,0 +1,16 @@
+# podman - Simple debugging tool for pods and images
+podman is a simple client only tool to help with debugging issues when daemons such as CRI runtime and the kubelet are not responding or
+failing. A shared API layer could be created to share code between the daemon and podman. podman does not require any daemon running. podman
+utilizes the same underlying components that crio uses i.e. containers/image, container/storage, oci-runtime-tool/generate, runc or
+any other OCI compatible runtime. podman shares state with crio and so has the capability to debug pods/images created by crio.
+
+## Use cases
+1. List pods.
+2. Launch simple pods (that require no daemon support).
+3. Exec commands in a container in a pod.
+4. Launch additional containers in a pod.
+5. List images.
+6. Remove images not in use.
+7. Pull images.
+8. Check image size.
+9. Report pod disk resource usage.
diff --git a/cmd/podman/attach.go b/cmd/podman/attach.go
new file mode 100644
index 000000000..8c2c99fd5
--- /dev/null
+++ b/cmd/podman/attach.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "sync"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var (
+ attachFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "detach-keys",
+ Usage: "Override the key sequence for detaching a container. Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _.",
+ },
+ cli.BoolFlag{
+ Name: "no-stdin",
+ Usage: "Do not attach STDIN. The default is false.",
+ },
+ }
+ attachDescription = "The podman attach command allows you to attach to a running container using the container's ID or name, either to view its ongoing output or to control it interactively."
+ attachCommand = cli.Command{
+ Name: "attach",
+ Usage: "Attach to a running container",
+ Description: attachDescription,
+ Flags: attachFlags,
+ Action: attachCmd,
+ ArgsUsage: "",
+ }
+)
+
+func attachCmd(c *cli.Context) error {
+ args := c.Args()
+ if err := validateFlags(c, attachFlags); err != nil {
+ return err
+ }
+
+ if len(c.Args()) < 1 || len(c.Args()) > 1 {
+ return errors.Errorf("attach requires the name or id of one running container")
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ ctr, err := runtime.LookupContainer(args[0])
+
+ if err != nil {
+ return errors.Wrapf(err, "unable to exec into %s", args[0])
+ }
+
+ conState, err := ctr.State()
+ if err != nil {
+ return errors.Wrapf(err, "unable to determine state of %s", args[0])
+ }
+ if conState != libpod.ContainerStateRunning {
+ return errors.Errorf("you can only attach to running containers")
+ }
+ // Create a bool channel to track that the console socket attach
+ // is successful.
+ attached := make(chan bool)
+ // Create a waitgroup so we can sync and wait for all goroutines
+ // to finish before exiting main
+ var wg sync.WaitGroup
+
+ // We increment the wg counter because we need to do the attach
+ wg.Add(1)
+ // Attach to the running container
+ go func() {
+ logrus.Debugf("trying to attach to the container %s", ctr.ID())
+ defer wg.Done()
+ if err := ctr.Attach(c.Bool("no-stdin"), c.String("detach-keys"), attached); err != nil {
+ logrus.Errorf("unable to attach to container %s: %q", ctr.ID(), err)
+ }
+ }()
+ if !<-attached {
+ return errors.Errorf("unable to attach to container %s", ctr.ID())
+ }
+ wg.Wait()
+
+ return nil
+}
diff --git a/cmd/podman/common.go b/cmd/podman/common.go
new file mode 100644
index 000000000..99685107b
--- /dev/null
+++ b/cmd/podman/common.go
@@ -0,0 +1,438 @@
+package main
+
+import (
+ "os"
+ "reflect"
+ "regexp"
+ "strings"
+
+ "github.com/containers/storage"
+ "github.com/fatih/camelcase"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libkpod"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ stores = make(map[storage.Store]struct{})
+)
+
+const crioConfigPath = "/etc/crio/crio.conf"
+
+func getRuntime(c *cli.Context) (*libpod.Runtime, error) {
+
+ config, err := getConfig(c)
+ if err != nil {
+ return nil, errors.Wrapf(err, "could not get config")
+ }
+
+ options := storage.DefaultStoreOptions
+ options.GraphRoot = config.Root
+ options.RunRoot = config.RunRoot
+ options.GraphDriverName = config.Storage
+ options.GraphDriverOptions = config.StorageOptions
+
+ return libpod.NewRuntime(libpod.WithStorageConfig(options), libpod.WithConmonPath(config.Conmon), libpod.WithOCIRuntime(config.Runtime), libpod.WithCNIConfigDir(config.NetworkDir))
+}
+
+func shutdownStores() {
+ for store := range stores {
+ if _, err := store.Shutdown(false); err != nil {
+ break
+ }
+ }
+}
+
+func getConfig(c *cli.Context) (*libkpod.Config, error) {
+ config := libkpod.DefaultConfig()
+ var configFile string
+ if c.GlobalIsSet("config") {
+ configFile = c.GlobalString("config")
+ } else if _, err := os.Stat(crioConfigPath); err == nil {
+ configFile = crioConfigPath
+ }
+ // load and merge the configfile from the commandline or use
+ // the default crio config file
+ if configFile != "" {
+ err := config.UpdateFromFile(configFile)
+ if err != nil {
+ return config, err
+ }
+ }
+ if c.GlobalIsSet("root") {
+ config.Root = c.GlobalString("root")
+ }
+ if c.GlobalIsSet("runroot") {
+ config.RunRoot = c.GlobalString("runroot")
+ }
+ if c.GlobalIsSet("conmon") {
+ config.Conmon = c.GlobalString("conmon")
+ }
+ if c.GlobalIsSet("storage-driver") {
+ config.Storage = c.GlobalString("storage-driver")
+ }
+ if c.GlobalIsSet("storage-opt") {
+ opts := c.GlobalStringSlice("storage-opt")
+ if len(opts) > 0 {
+ config.StorageOptions = opts
+ }
+ }
+ if c.GlobalIsSet("runtime") {
+ config.Runtime = c.GlobalString("runtime")
+ }
+ if c.GlobalIsSet("cni-config-dir") {
+ config.NetworkDir = c.GlobalString("cni-config-dir")
+ }
+ return config, nil
+}
+
+func splitCamelCase(src string) string {
+ entries := camelcase.Split(src)
+ return strings.Join(entries, " ")
+}
+
+// validateFlags searches for StringFlags or StringSlice flags that never had
+// a value set. This commonly occurs when the CLI mistakenly takes the next
+// option and uses it as a value.
+func validateFlags(c *cli.Context, flags []cli.Flag) error {
+ for _, flag := range flags {
+ switch reflect.TypeOf(flag).String() {
+ case "cli.StringSliceFlag":
+ {
+ f := flag.(cli.StringSliceFlag)
+ name := strings.Split(f.Name, ",")
+ val := c.StringSlice(name[0])
+ for _, v := range val {
+ if ok, _ := regexp.MatchString("^-.+", v); ok {
+ return errors.Errorf("option --%s requires a value", name[0])
+ }
+ }
+ }
+ case "cli.StringFlag":
+ {
+ f := flag.(cli.StringFlag)
+ name := strings.Split(f.Name, ",")
+ val := c.String(name[0])
+ if ok, _ := regexp.MatchString("^-.+", val); ok {
+ return errors.Errorf("option --%s requires a value", name[0])
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// Common flags shared between commands
+var createFlags = []cli.Flag{
+ cli.StringSliceFlag{
+ Name: "add-host",
+ Usage: "Add a custom host-to-IP mapping (host:ip) (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "attach, a",
+ Usage: "Attach to STDIN, STDOUT or STDERR (default [])",
+ },
+ cli.StringFlag{
+ Name: "blkio-weight",
+ Usage: "Block IO weight (relative weight) accepts a weight value between 10 and 1000.",
+ },
+ cli.StringSliceFlag{
+ Name: "blkio-weight-device",
+ Usage: "Block IO weight (relative device weight, format: `DEVICE_NAME:WEIGHT`)",
+ },
+ cli.StringSliceFlag{
+ Name: "cap-add",
+ Usage: "Add capabilities to the container",
+ },
+ cli.StringSliceFlag{
+ Name: "cap-drop",
+ Usage: "Drop capabilities from the container",
+ },
+ cli.StringFlag{
+ Name: "cgroup-parent",
+ Usage: "Optional parent cgroup for the container",
+ },
+ cli.StringFlag{
+ Name: "cidfile",
+ Usage: "Write the container ID to the file",
+ },
+ cli.Uint64Flag{
+ Name: "cpu-period",
+ Usage: "Limit the CPU CFS (Completely Fair Scheduler) period",
+ },
+ cli.Int64Flag{
+ Name: "cpu-quota",
+ Usage: "Limit the CPU CFS (Completely Fair Scheduler) quota",
+ },
+ cli.Uint64Flag{
+ Name: "cpu-rt-period",
+ Usage: "Limit the CPU real-time period in microseconds",
+ },
+ cli.Int64Flag{
+ Name: "cpu-rt-runtime",
+ Usage: "Limit the CPU real-time runtime in microseconds",
+ },
+ cli.Uint64Flag{
+ Name: "cpu-shares",
+ Usage: "CPU shares (relative weight)",
+ },
+ cli.StringFlag{
+ Name: "cpus",
+ Usage: "Number of CPUs. The default is 0.000 which means no limit",
+ },
+ cli.StringFlag{
+ Name: "cpuset-cpus",
+ Usage: "CPUs in which to allow execution (0-3, 0,1)",
+ },
+ cli.StringFlag{
+ Name: "cpuset-mems",
+ Usage: "Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.",
+ },
+ cli.BoolFlag{
+ Name: "detach, d",
+ Usage: "Run container in background and print container ID",
+ },
+ cli.StringFlag{
+ Name: "detach-keys",
+ Usage: "Override the key sequence for detaching a container. Format is a single character `[a-Z]` or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`",
+ },
+ cli.StringSliceFlag{
+ Name: "device",
+ Usage: "Add a host device to the container (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "device-read-bps",
+ Usage: "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)",
+ },
+ cli.StringSliceFlag{
+ Name: "device-read-iops",
+ Usage: "Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)",
+ },
+ cli.StringSliceFlag{
+ Name: "device-write-bps",
+ Usage: "Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb)",
+ },
+ cli.StringSliceFlag{
+ Name: "device-write-iops",
+ Usage: "Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)",
+ },
+ cli.StringSliceFlag{
+ Name: "dns",
+ Usage: "Set custom DNS servers",
+ },
+ cli.StringSliceFlag{
+ Name: "dns-opt",
+ Usage: "Set custom DNS options",
+ },
+ cli.StringSliceFlag{
+ Name: "dns-search",
+ Usage: "Set custom DNS search domains",
+ },
+ cli.StringFlag{
+ Name: "entrypoint",
+ Usage: "Overwrite the default ENTRYPOINT of the image",
+ },
+ cli.StringSliceFlag{
+ Name: "env, e",
+ Usage: "Set environment variables in container",
+ },
+ cli.StringSliceFlag{
+ Name: "env-file",
+ Usage: "Read in a file of environment variables",
+ },
+ cli.StringSliceFlag{
+ Name: "expose",
+ Usage: "Expose a port or a range of ports (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "group-add",
+ Usage: "Add additional groups to join (default [])",
+ },
+ cli.StringFlag{
+ Name: "hostname",
+ Usage: "Set container hostname",
+ },
+ cli.BoolFlag{
+ Name: "interactive, i",
+ Usage: "Keep STDIN open even if not attached",
+ },
+ cli.StringFlag{
+ Name: "ip",
+ Usage: "Container IPv4 address (e.g. 172.23.0.9)",
+ },
+ cli.StringFlag{
+ Name: "ip6",
+ Usage: "Container IPv6 address (e.g. 2001:db8::1b99)",
+ },
+ cli.StringFlag{
+ Name: "ipc",
+ Usage: "IPC Namespace to use",
+ },
+ cli.StringFlag{
+ Name: "kernel-memory",
+ Usage: "Kernel memory limit (format: `<number>[<unit>]`, where unit = b, k, m or g)",
+ },
+ cli.StringSliceFlag{
+ Name: "label",
+ Usage: "Set metadata on container (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "label-file",
+ Usage: "Read in a line delimited file of labels (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "link-local-ip",
+ Usage: "Container IPv4/IPv6 link-local addresses (default [])",
+ },
+ cli.StringFlag{
+ Name: "log-driver",
+ Usage: "Logging driver for the container",
+ },
+ cli.StringSliceFlag{
+ Name: "log-opt",
+ Usage: "Logging driver options (default [])",
+ },
+ cli.StringFlag{
+ Name: "mac-address",
+ Usage: "Container MAC address (e.g. 92:d0:c6:0a:29:33)",
+ },
+ cli.StringFlag{
+ Name: "memory, m",
+ Usage: "Memory limit (format: <number>[<unit>], where unit = b, k, m or g)",
+ },
+ cli.StringFlag{
+ Name: "memory-reservation",
+ Usage: "Memory soft limit (format: <number>[<unit>], where unit = b, k, m or g)",
+ },
+ cli.StringFlag{
+ Name: "memory-swap",
+ Usage: "Swap limit equal to memory plus swap: '-1' to enable unlimited swap",
+ },
+ cli.Int64Flag{
+ Name: "memory-swappiness",
+ Usage: "Tune container memory swappiness (0 to 100) (default -1)",
+ Value: -1,
+ },
+ cli.StringFlag{
+ Name: "name",
+ Usage: "Assign a name to the container",
+ },
+ cli.StringFlag{
+ Name: "net",
+ Usage: "Setup the network namespace",
+ },
+ cli.StringFlag{
+ Name: "network",
+ Usage: "Connect a container to a network (default 'default')",
+ },
+ cli.StringSliceFlag{
+ Name: "network-alias",
+ Usage: "Add network-scoped alias for the container (default [])",
+ },
+ cli.BoolFlag{
+ Name: "oom-kill-disable",
+ Usage: "Disable OOM Killer",
+ },
+ cli.StringFlag{
+ Name: "oom-score-adj",
+ Usage: "Tune the host's OOM preferences (-1000 to 1000)",
+ },
+ cli.StringFlag{
+ Name: "pid",
+ Usage: "PID Namespace to use",
+ },
+ cli.Int64Flag{
+ Name: "pids-limit",
+ Usage: "Tune container pids limit (set -1 for unlimited)",
+ },
+ cli.StringFlag{
+ Name: "pod",
+ Usage: "Run container in an existing pod",
+ },
+ cli.BoolFlag{
+ Name: "privileged",
+ Usage: "Give extended privileges to container",
+ },
+ cli.StringSliceFlag{
+ Name: "publish, p",
+ Usage: "Publish a container's port, or a range of ports, to the host (default [])",
+ },
+ cli.BoolFlag{
+ Name: "publish-all, P",
+ Usage: "Publish all exposed ports to random ports on the host interface",
+ },
+ cli.BoolFlag{
+ Name: "read-only",
+ Usage: "Make containers root filesystem read-only",
+ },
+ cli.BoolFlag{
+ Name: "rm",
+ Usage: "Remove container (and pod if created) after exit",
+ },
+ cli.StringSliceFlag{
+ Name: "security-opt",
+ Usage: "Security Options (default [])",
+ },
+ cli.StringFlag{
+ Name: "shm-size",
+ Usage: "Size of `/dev/shm`. The format is `<number><unit>`. default is 64 MB",
+ },
+ cli.BoolFlag{
+ Name: "sig-proxy",
+ Usage: "Proxy received signals to the process (default true)",
+ },
+ cli.StringFlag{
+ Name: "stop-signal",
+ Usage: "Signal to stop a container. Default is SIGTERM",
+ },
+ cli.IntFlag{
+ Name: "stop-timeout",
+ Usage: "Timeout (in seconds) to stop a container. Default is 10",
+ },
+ cli.StringSliceFlag{
+ Name: "storage-opt",
+ Usage: "Storage driver options per container (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "sysctl",
+ Usage: "Sysctl options (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "tmpfs",
+ Usage: "Mount a temporary filesystem (`tmpfs`) into a container (default [])",
+ },
+ cli.BoolFlag{
+ Name: "tty, t",
+ Usage: "Allocate a pseudo-TTY for container",
+ },
+ cli.StringSliceFlag{
+ Name: "ulimit",
+ Usage: "Ulimit options (default [])",
+ },
+ cli.StringFlag{
+ Name: "user, u",
+ Usage: "Username or UID (format: <name|uid>[:<group|gid>])",
+ },
+ cli.StringFlag{
+ Name: "userns",
+ Usage: "User namespace to use",
+ },
+ cli.StringFlag{
+ Name: "uts",
+ Usage: "UTS namespace to use",
+ },
+ cli.StringSliceFlag{
+ Name: "volume, v",
+ Usage: "Bind mount a volume into the container (default [])",
+ },
+ cli.StringSliceFlag{
+ Name: "volumes-from",
+ Usage: "Mount volumes from the specified container(s) (default [])",
+ },
+ cli.StringFlag{
+ Name: "workdir, w",
+ Usage: "Working `directory inside the container",
+ Value: "/",
+ },
+}
diff --git a/cmd/podman/common_test.go b/cmd/podman/common_test.go
new file mode 100644
index 000000000..042568d7e
--- /dev/null
+++ b/cmd/podman/common_test.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "os/user"
+ "testing"
+
+ "flag"
+
+ "github.com/urfave/cli"
+)
+
+func TestGetStore(t *testing.T) {
+ t.Skip("FIX THIS!")
+
+ //cmd/podman/common_test.go:27: cannot use c (type *cli.Context) as type *libkpod.Config in argument to getStore
+
+ // Make sure the tests are running as root
+ skipTestIfNotRoot(t)
+
+ set := flag.NewFlagSet("test", 0)
+ globalSet := flag.NewFlagSet("test", 0)
+ globalSet.String("root", "", "path to the root directory in which data, including images, is stored")
+ globalCtx := cli.NewContext(nil, globalSet, nil)
+ command := cli.Command{Name: "imagesCommand"}
+ c := cli.NewContext(nil, set, globalCtx)
+ c.Command = command
+
+ //_, err := getStore(c)
+ //if err != nil {
+ //t.Error(err)
+ //}
+}
+
+func skipTestIfNotRoot(t *testing.T) {
+ u, err := user.Current()
+ if err != nil {
+ t.Skip("Could not determine user. Running without root may cause tests to fail")
+ } else if u.Uid != "0" {
+ t.Skip("tests will fail unless run as root")
+ }
+}
diff --git a/cmd/podman/create.go b/cmd/podman/create.go
new file mode 100644
index 000000000..f65bc49c6
--- /dev/null
+++ b/cmd/podman/create.go
@@ -0,0 +1,507 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/go-units"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+type mountType string
+
+// Type constants
+const (
+ // TypeBind is the type for mounting host dir
+ TypeBind mountType = "bind"
+ // TypeVolume is the type for remote storage volumes
+ // TypeVolume mountType = "volume" // re-enable upon use
+ // TypeTmpfs is the type for mounting tmpfs
+ TypeTmpfs mountType = "tmpfs"
+)
+
+var (
+ defaultEnvVariables = map[string]string{
+ "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "TERM": "xterm",
+ }
+)
+
+type createResourceConfig struct {
+ BlkioWeight uint16 // blkio-weight
+ BlkioWeightDevice []string // blkio-weight-device
+ CPUPeriod uint64 // cpu-period
+ CPUQuota int64 // cpu-quota
+ CPURtPeriod uint64 // cpu-rt-period
+ CPURtRuntime int64 // cpu-rt-runtime
+ CPUShares uint64 // cpu-shares
+ CPUs string // cpus
+ CPUsetCPUs string
+ CPUsetMems string // cpuset-mems
+ DeviceReadBps []string // device-read-bps
+ DeviceReadIOps []string // device-read-iops
+ DeviceWriteBps []string // device-write-bps
+ DeviceWriteIOps []string // device-write-iops
+ DisableOomKiller bool // oom-kill-disable
+ KernelMemory int64 // kernel-memory
+ Memory int64 //memory
+ MemoryReservation int64 // memory-reservation
+ MemorySwap int64 //memory-swap
+ MemorySwappiness int // memory-swappiness
+ OomScoreAdj int //oom-score-adj
+ PidsLimit int64 // pids-limit
+ ShmSize string
+ Ulimit []string //ulimit
+}
+
+type createConfig struct {
+ Runtime *libpod.Runtime
+ Args []string
+ CapAdd []string // cap-add
+ CapDrop []string // cap-drop
+ CidFile string
+ CgroupParent string // cgroup-parent
+ Command []string
+ Detach bool // detach
+ Devices []*pb.Device // device
+ DNSOpt []string //dns-opt
+ DNSSearch []string //dns-search
+ DNSServers []string //dns
+ Entrypoint string //entrypoint
+ Env map[string]string //env
+ Expose []string //expose
+ GroupAdd []uint32 // group-add
+ Hostname string //hostname
+ Image string
+ Interactive bool //interactive
+ IpcMode container.IpcMode //ipc
+ IP6Address string //ipv6
+ IPAddress string //ip
+ Labels map[string]string //label
+ LinkLocalIP []string // link-local-ip
+ LogDriver string // log-driver
+ LogDriverOpt []string // log-opt
+ MacAddress string //mac-address
+ Name string //name
+ NetMode container.NetworkMode //net
+ Network string //network
+ NetworkAlias []string //network-alias
+ PidMode container.PidMode //pid
+ NsUser string
+ Pod string //pod
+ Privileged bool //privileged
+ Publish []string //publish
+ PublishAll bool //publish-all
+ ReadOnlyRootfs bool //read-only
+ Resources createResourceConfig
+ Rm bool //rm
+ ShmDir string
+ SigProxy bool //sig-proxy
+ StopSignal string // stop-signal
+ StopTimeout int64 // stop-timeout
+ StorageOpts []string //storage-opt
+ Sysctl map[string]string //sysctl
+ Tmpfs []string // tmpfs
+ Tty bool //tty
+ User uint32 //user
+ Group uint32 // group
+ UtsMode container.UTSMode //uts
+ Volumes []string //volume
+ WorkDir string //workdir
+ MountLabel string //SecurityOpts
+ ProcessLabel string //SecurityOpts
+ NoNewPrivileges bool //SecurityOpts
+ ApparmorProfile string //SecurityOpts
+ SeccompProfilePath string //SecurityOpts
+ SecurityOpts []string
+}
+
+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'."
+
+var createCommand = cli.Command{
+ Name: "create",
+ Usage: "create but do not start a container",
+ Description: createDescription,
+ Flags: createFlags,
+ Action: createCmd,
+ ArgsUsage: "IMAGE [COMMAND [ARG...]]",
+ SkipArgReorder: true,
+ UseShortOptionHandling: true,
+}
+
+func createCmd(c *cli.Context) error {
+ // TODO should allow user to create based off a directory on the host not just image
+ // Need CLI support for this
+ var imageName string
+ if err := validateFlags(c, createFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ createConfig, err := parseCreateOpts(c, runtime)
+ if err != nil {
+ return err
+ }
+
+ // Deal with the image after all the args have been checked
+ createImage := runtime.NewImage(createConfig.Image)
+ createImage.LocalName, _ = createImage.GetLocalImageName()
+ if createImage.LocalName == "" {
+ // The image wasnt found by the user input'd name or its fqname
+ // Pull the image
+ fmt.Printf("Trying to pull %s...", createImage.PullName)
+ createImage.Pull()
+ }
+
+ runtimeSpec, err := createConfigToOCISpec(createConfig)
+ if err != nil {
+ return err
+ }
+ if createImage.LocalName != "" {
+ nameIsID, err := runtime.IsImageID(createImage.LocalName)
+ if err != nil {
+ return err
+ }
+ if nameIsID {
+ // If the input from the user is an ID, then we need to get the image
+ // name for cstorage
+ createImage.LocalName, err = createImage.GetNameByID()
+ if err != nil {
+ return err
+ }
+ }
+ imageName = createImage.LocalName
+ } else {
+ imageName, err = createImage.GetFQName()
+ }
+ if err != nil {
+ return err
+ }
+ imageID, err := createImage.GetImageID()
+ if err != nil {
+ return err
+ }
+ options, err := createConfig.GetContainerCreateOptions()
+ if err != nil {
+ return errors.Wrapf(err, "unable to parse new container options")
+ }
+ // Gather up the options for NewContainer which consist of With... funcs
+ options = append(options, libpod.WithRootFSFromImage(imageID, imageName, false))
+ options = append(options, libpod.WithSELinuxLabels(createConfig.ProcessLabel, createConfig.MountLabel))
+ options = append(options, libpod.WithShmDir(createConfig.ShmDir))
+ ctr, err := runtime.NewContainer(runtimeSpec, options...)
+ if err != nil {
+ return err
+ }
+
+ createConfigJSON, err := json.Marshal(createConfig)
+ if err != nil {
+ return err
+ }
+ if err := ctr.AddArtifact("create-config", createConfigJSON); err != nil {
+ return err
+ }
+
+ logrus.Debug("new container created ", ctr.ID())
+
+ if c.String("cidfile") != "" {
+ libpod.WriteFile(ctr.ID(), c.String("cidfile"))
+ } else {
+ fmt.Printf("%s\n", ctr.ID())
+ }
+
+ return nil
+}
+
+const seccompDefaultPath = "/etc/crio/seccomp.json"
+
+func parseSecurityOpt(config *createConfig, securityOpts []string) error {
+ var (
+ labelOpts []string
+ err error
+ )
+
+ if config.PidMode.IsHost() {
+ labelOpts = append(labelOpts, label.DisableSecOpt()...)
+ } else if config.PidMode.IsContainer() {
+ ctr, err := config.Runtime.LookupContainer(config.PidMode.Container())
+ if err != nil {
+ return errors.Wrapf(err, "container %q not found", config.PidMode.Container())
+ }
+ labelOpts = append(labelOpts, label.DupSecOpt(ctr.ProcessLabel())...)
+ }
+
+ if config.IpcMode.IsHost() {
+ labelOpts = append(labelOpts, label.DisableSecOpt()...)
+ } else if config.IpcMode.IsContainer() {
+ ctr, err := config.Runtime.LookupContainer(config.IpcMode.Container())
+ if err != nil {
+ return errors.Wrapf(err, "container %q not found", config.IpcMode.Container())
+ }
+ labelOpts = append(labelOpts, label.DupSecOpt(ctr.ProcessLabel())...)
+ }
+
+ for _, opt := range securityOpts {
+ if opt == "no-new-privileges" {
+ config.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":
+ labelOpts = append(labelOpts, con[1])
+ case "apparmor":
+ config.ApparmorProfile = con[1]
+ case "seccomp":
+ config.SeccompProfilePath = con[1]
+ default:
+ return fmt.Errorf("Invalid --security-opt 2: %q", opt)
+ }
+ }
+ }
+
+ if config.SeccompProfilePath == "" {
+ if _, err := os.Stat(seccompDefaultPath); err != nil {
+ if !os.IsNotExist(err) {
+ return errors.Wrapf(err, "can't check if %q exists", seccompDefaultPath)
+ }
+ } else {
+ config.SeccompProfilePath = seccompDefaultPath
+ }
+ }
+ config.ProcessLabel, config.MountLabel, err = label.InitLabels(labelOpts)
+ return err
+}
+
+// Parses CLI options related to container creation into a config which can be
+// parsed into an OCI runtime spec
+func parseCreateOpts(c *cli.Context, runtime *libpod.Runtime) (*createConfig, error) {
+ var command []string
+ var memoryLimit, memoryReservation, memorySwap, memoryKernel int64
+ var blkioWeight uint16
+ var uid, gid uint32
+
+ if len(c.Args()) < 1 {
+ return nil, errors.Errorf("image name or ID is required")
+ }
+ image := c.Args()[0]
+
+ if len(c.Args()) > 1 {
+ command = c.Args()[1:]
+ }
+
+ // LABEL VARIABLES
+ labels, err := getAllLabels(c.StringSlice("label-file"), c.StringSlice("labels"))
+ if err != nil {
+ return &createConfig{}, errors.Wrapf(err, "unable to process labels")
+ }
+ // ENVIRONMENT VARIABLES
+ env := defaultEnvVariables
+ if err := readKVStrings(env, c.StringSlice("env-file"), c.StringSlice("env")); err != nil {
+ return &createConfig{}, errors.Wrapf(err, "unable to process environment variables")
+ }
+
+ sysctl, err := convertStringSliceToMap(c.StringSlice("sysctl"), "=")
+ if err != nil {
+ return &createConfig{}, errors.Wrapf(err, "sysctl values must be in the form of KEY=VALUE")
+ }
+
+ groupAdd, err := stringSlicetoUint32Slice(c.StringSlice("group-add"))
+ if err != nil {
+ return &createConfig{}, errors.Wrapf(err, "invalid value for groups provided")
+ }
+
+ if c.String("user") != "" {
+ // TODO
+ // We need to mount the imagefs and get the uid/gid
+ // For now, user zeros
+ uid = 0
+ gid = 0
+ }
+
+ if c.String("memory") != "" {
+ memoryLimit, err = units.RAMInBytes(c.String("memory"))
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid value for memory")
+ }
+ }
+ if c.String("memory-reservation") != "" {
+ memoryReservation, err = units.RAMInBytes(c.String("memory-reservation"))
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid value for memory-reservation")
+ }
+ }
+ if c.String("memory-swap") != "" {
+ memorySwap, err = units.RAMInBytes(c.String("memory-swap"))
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid value for memory-swap")
+ }
+ }
+ if c.String("kernel-memory") != "" {
+ memoryKernel, err = units.RAMInBytes(c.String("kernel-memory"))
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid value for kernel-memory")
+ }
+ }
+ if c.String("blkio-weight") != "" {
+ u, err := strconv.ParseUint(c.String("blkio-weight"), 10, 16)
+ if err != nil {
+ return nil, errors.Wrapf(err, "invalid value for blkio-weight")
+ }
+ blkioWeight = uint16(u)
+ }
+
+ if err = parseVolumes(c.StringSlice("volume")); err != nil {
+ return nil, err
+ }
+
+ // Because we cannot do a non-terminal attach, we need to set tty to true
+ // if detach is not false
+ // TODO Allow non-terminal attach
+ tty := c.Bool("tty")
+ if !c.Bool("detach") && !tty {
+ tty = true
+ }
+
+ pidMode := container.PidMode(c.String("pid"))
+ if !pidMode.Valid() {
+ return nil, errors.Errorf("--pid %q is not valid", c.String("pid"))
+ }
+
+ if c.Bool("detach") && c.Bool("rm") {
+ return nil, errors.Errorf("--rm and --detach can not be specified together")
+ }
+
+ utsMode := container.UTSMode(c.String("uts"))
+ if !utsMode.Valid() {
+ return nil, errors.Errorf("--uts %q is not valid", c.String("uts"))
+ }
+ ipcMode := container.IpcMode(c.String("ipc"))
+ if !ipcMode.Valid() {
+ return nil, errors.Errorf("--ipc %q is not valid", ipcMode)
+ }
+ shmDir := ""
+ if ipcMode.IsHost() {
+ shmDir = "/dev/shm"
+ } else if ipcMode.IsContainer() {
+ ctr, err := runtime.LookupContainer(ipcMode.Container())
+ if err != nil {
+ return nil, errors.Wrapf(err, "container %q not found", ipcMode.Container())
+ }
+ shmDir = ctr.ShmDir()
+ }
+
+ config := &createConfig{
+ Runtime: runtime,
+ CapAdd: c.StringSlice("cap-add"),
+ CapDrop: c.StringSlice("cap-drop"),
+ CgroupParent: c.String("cgroup-parent"),
+ Command: command,
+ Detach: c.Bool("detach"),
+ DNSOpt: c.StringSlice("dns-opt"),
+ DNSSearch: c.StringSlice("dns-search"),
+ DNSServers: c.StringSlice("dns"),
+ Entrypoint: c.String("entrypoint"),
+ Env: env,
+ Expose: c.StringSlice("expose"),
+ GroupAdd: groupAdd,
+ Hostname: c.String("hostname"),
+ Image: image,
+ Interactive: c.Bool("interactive"),
+ IP6Address: c.String("ipv6"),
+ IPAddress: c.String("ip"),
+ Labels: labels,
+ LinkLocalIP: c.StringSlice("link-local-ip"),
+ LogDriver: c.String("log-driver"),
+ LogDriverOpt: c.StringSlice("log-opt"),
+ MacAddress: c.String("mac-address"),
+ Name: c.String("name"),
+ Network: c.String("network"),
+ NetworkAlias: c.StringSlice("network-alias"),
+ IpcMode: ipcMode,
+ NetMode: container.NetworkMode(c.String("network")),
+ UtsMode: utsMode,
+ PidMode: pidMode,
+ Pod: c.String("pod"),
+ Privileged: c.Bool("privileged"),
+ Publish: c.StringSlice("publish"),
+ PublishAll: c.Bool("publish-all"),
+ ReadOnlyRootfs: c.Bool("read-only"),
+ Resources: createResourceConfig{
+ BlkioWeight: blkioWeight,
+ BlkioWeightDevice: c.StringSlice("blkio-weight-device"),
+ CPUShares: c.Uint64("cpu-shares"),
+ CPUPeriod: c.Uint64("cpu-period"),
+ CPUsetCPUs: c.String("cpu-period"),
+ CPUsetMems: c.String("cpuset-mems"),
+ CPUQuota: c.Int64("cpu-quota"),
+ CPURtPeriod: c.Uint64("cpu-rt-period"),
+ CPURtRuntime: c.Int64("cpu-rt-runtime"),
+ CPUs: c.String("cpus"),
+ DeviceReadBps: c.StringSlice("device-read-bps"),
+ DeviceReadIOps: c.StringSlice("device-read-iops"),
+ DeviceWriteBps: c.StringSlice("device-write-bps"),
+ DeviceWriteIOps: c.StringSlice("device-write-iops"),
+ DisableOomKiller: c.Bool("oom-kill-disable"),
+ ShmSize: c.String("shm-size"),
+ Memory: memoryLimit,
+ MemoryReservation: memoryReservation,
+ MemorySwap: memorySwap,
+ MemorySwappiness: c.Int("memory-swappiness"),
+ KernelMemory: memoryKernel,
+ OomScoreAdj: c.Int("oom-score-adj"),
+
+ PidsLimit: c.Int64("pids-limit"),
+ Ulimit: c.StringSlice("ulimit"),
+ },
+ Rm: c.Bool("rm"),
+ ShmDir: shmDir,
+ SigProxy: c.Bool("sig-proxy"),
+ StopSignal: c.String("stop-signal"),
+ StopTimeout: c.Int64("stop-timeout"),
+ StorageOpts: c.StringSlice("storage-opt"),
+ Sysctl: sysctl,
+ Tmpfs: c.StringSlice("tmpfs"),
+ Tty: tty,
+ User: uid,
+ Group: gid,
+ Volumes: c.StringSlice("volume"),
+ WorkDir: c.String("workdir"),
+ }
+
+ if !config.Privileged {
+ if err := parseSecurityOpt(config, c.StringSlice("security-opt")); err != nil {
+ return nil, err
+ }
+ }
+ config.SecurityOpts = c.StringSlice("security-opt")
+ warnings, err := verifyContainerResources(config, false)
+ if err != nil {
+ return nil, err
+ }
+ for _, warning := range warnings {
+ fmt.Fprintln(os.Stderr, warning)
+ }
+ return config, nil
+}
diff --git a/cmd/podman/create_cli.go b/cmd/podman/create_cli.go
new file mode 100644
index 000000000..0cc265e92
--- /dev/null
+++ b/cmd/podman/create_cli.go
@@ -0,0 +1,242 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/docker/docker/pkg/sysinfo"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+const (
+ // It's not kernel limit, we want this 4M limit to supply a reasonable functional container
+ linuxMinMemory = 4194304
+)
+
+func getAllLabels(labelFile, inputLabels []string) (map[string]string, error) {
+ labels := make(map[string]string)
+ labelErr := readKVStrings(labels, labelFile, inputLabels)
+ if labelErr != nil {
+ return labels, errors.Wrapf(labelErr, "unable to process labels from --label and label-file")
+ }
+ return labels, nil
+}
+
+func convertStringSliceToMap(strSlice []string, delimiter string) (map[string]string, error) {
+ sysctl := make(map[string]string)
+ for _, inputSysctl := range strSlice {
+ values := strings.Split(inputSysctl, delimiter)
+ if len(values) < 2 {
+ return sysctl, errors.Errorf("%s in an invalid sysctl value", inputSysctl)
+ }
+ sysctl[values[0]] = values[1]
+ }
+ return sysctl, nil
+}
+
+func addWarning(warnings []string, msg string) []string {
+ logrus.Warn(msg)
+ return append(warnings, msg)
+}
+
+func parseVolumes(volumes []string) error {
+ if len(volumes) == 0 {
+ return nil
+ }
+ for _, volume := range volumes {
+ arr := strings.SplitN(volume, ":", 3)
+ if len(arr) < 2 {
+ return errors.Errorf("incorrect volume format %q, should be host-dir:ctr-dir:[option]", volume)
+ }
+ if err := validateVolumeHostDir(arr[0]); err != nil {
+ return err
+ }
+ if err := validateVolumeCtrDir(arr[1]); err != nil {
+ return err
+ }
+ if len(arr) > 2 {
+ if err := validateVolumeOpts(arr[2]); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func validateVolumeHostDir(hostDir string) error {
+ if _, err := os.Stat(hostDir); err != nil {
+ return errors.Wrapf(err, "error checking path %q", hostDir)
+ }
+ return nil
+}
+
+func validateVolumeCtrDir(ctrDir string) error {
+ if ctrDir[0] != '/' {
+ return errors.Errorf("invalid container directory path %q", ctrDir)
+ }
+ return nil
+}
+
+func validateVolumeOpts(option string) error {
+ var foundRootPropagation, foundRWRO, foundLabelChange int
+ options := strings.Split(option, ",")
+ for _, opt := range options {
+ switch opt {
+ case "rw", "ro":
+ if foundRWRO > 1 {
+ return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option)
+ }
+ foundRWRO++
+ case "z", "Z":
+ if foundLabelChange > 1 {
+ return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option)
+ }
+ foundLabelChange++
+ case "private", "rprivate", "shared", "rshared", "slave", "rslave":
+ if foundRootPropagation > 1 {
+ return errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private' or '[r]slave' option", option)
+ }
+ foundRootPropagation++
+ default:
+ return errors.Errorf("invalid option type %q", option)
+ }
+ }
+ return nil
+}
+
+func verifyContainerResources(config *createConfig, update bool) ([]string, error) {
+ warnings := []string{}
+ sysInfo := sysinfo.New(true)
+
+ // memory subsystem checks and adjustments
+ if config.Resources.Memory != 0 && config.Resources.Memory < linuxMinMemory {
+ return warnings, fmt.Errorf("minimum memory limit allowed is 4MB")
+ }
+ if config.Resources.Memory > 0 && !sysInfo.MemoryLimit {
+ warnings = addWarning(warnings, "Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded.")
+ config.Resources.Memory = 0
+ config.Resources.MemorySwap = -1
+ }
+ if config.Resources.Memory > 0 && config.Resources.MemorySwap != -1 && !sysInfo.SwapLimit {
+ warnings = addWarning(warnings, "Your kernel does not support swap limit capabilities,or the cgroup is not mounted. Memory limited without swap.")
+ config.Resources.MemorySwap = -1
+ }
+ if config.Resources.Memory > 0 && config.Resources.MemorySwap > 0 && config.Resources.MemorySwap < config.Resources.Memory {
+ return warnings, fmt.Errorf("minimum memoryswap limit should be larger than memory limit, see usage")
+ }
+ if config.Resources.Memory == 0 && config.Resources.MemorySwap > 0 && !update {
+ return warnings, fmt.Errorf("you should always set the memory limit when using memoryswap limit, see usage")
+ }
+ if config.Resources.MemorySwappiness != -1 {
+ if !sysInfo.MemorySwappiness {
+ msg := "Your kernel does not support memory swappiness capabilities, or the cgroup is not mounted. Memory swappiness discarded."
+ warnings = addWarning(warnings, msg)
+ config.Resources.MemorySwappiness = -1
+ } else {
+ swappiness := config.Resources.MemorySwappiness
+ if swappiness < -1 || swappiness > 100 {
+ return warnings, fmt.Errorf("invalid value: %v, valid memory swappiness range is 0-100", swappiness)
+ }
+ }
+ }
+ if config.Resources.MemoryReservation > 0 && !sysInfo.MemoryReservation {
+ warnings = addWarning(warnings, "Your kernel does not support memory soft limit capabilities or the cgroup is not mounted. Limitation discarded.")
+ config.Resources.MemoryReservation = 0
+ }
+ if config.Resources.MemoryReservation > 0 && config.Resources.MemoryReservation < linuxMinMemory {
+ return warnings, fmt.Errorf("minimum memory reservation allowed is 4MB")
+ }
+ if config.Resources.Memory > 0 && config.Resources.MemoryReservation > 0 && config.Resources.Memory < config.Resources.MemoryReservation {
+ return warnings, fmt.Errorf("minimum memory limit can not be less than memory reservation limit, see usage")
+ }
+ if config.Resources.KernelMemory > 0 && !sysInfo.KernelMemory {
+ warnings = addWarning(warnings, "Your kernel does not support kernel memory limit capabilities or the cgroup is not mounted. Limitation discarded.")
+ config.Resources.KernelMemory = 0
+ }
+ if config.Resources.KernelMemory > 0 && config.Resources.KernelMemory < linuxMinMemory {
+ return warnings, fmt.Errorf("minimum kernel memory limit allowed is 4MB")
+ }
+ if config.Resources.DisableOomKiller == true && !sysInfo.OomKillDisable {
+ // only produce warnings if the setting wasn't to *disable* the OOM Kill; no point
+ // warning the caller if they already wanted the feature to be off
+ warnings = addWarning(warnings, "Your kernel does not support OomKillDisable. OomKillDisable discarded.")
+ config.Resources.DisableOomKiller = false
+ }
+
+ if config.Resources.PidsLimit != 0 && !sysInfo.PidsLimit {
+ warnings = addWarning(warnings, "Your kernel does not support pids limit capabilities or the cgroup is not mounted. PIDs limit discarded.")
+ config.Resources.PidsLimit = 0
+ }
+
+ if config.Resources.CPUShares > 0 && !sysInfo.CPUShares {
+ warnings = addWarning(warnings, "Your kernel does not support CPU shares or the cgroup is not mounted. Shares discarded.")
+ config.Resources.CPUShares = 0
+ }
+ if config.Resources.CPUPeriod > 0 && !sysInfo.CPUCfsPeriod {
+ warnings = addWarning(warnings, "Your kernel does not support CPU cfs period or the cgroup is not mounted. Period discarded.")
+ config.Resources.CPUPeriod = 0
+ }
+ if config.Resources.CPUPeriod != 0 && (config.Resources.CPUPeriod < 1000 || config.Resources.CPUPeriod > 1000000) {
+ return warnings, fmt.Errorf("CPU cfs period can not be less than 1ms (i.e. 1000) or larger than 1s (i.e. 1000000)")
+ }
+ if config.Resources.CPUQuota > 0 && !sysInfo.CPUCfsQuota {
+ warnings = addWarning(warnings, "Your kernel does not support CPU cfs quota or the cgroup is not mounted. Quota discarded.")
+ config.Resources.CPUQuota = 0
+ }
+ if config.Resources.CPUQuota > 0 && config.Resources.CPUQuota < 1000 {
+ return warnings, fmt.Errorf("CPU cfs quota can not be less than 1ms (i.e. 1000)")
+ }
+ // cpuset subsystem checks and adjustments
+ if (config.Resources.CPUsetCPUs != "" || config.Resources.CPUsetMems != "") && !sysInfo.Cpuset {
+ warnings = addWarning(warnings, "Your kernel does not support cpuset or the cgroup is not mounted. CPUset discarded.")
+ config.Resources.CPUsetCPUs = ""
+ config.Resources.CPUsetMems = ""
+ }
+ cpusAvailable, err := sysInfo.IsCpusetCpusAvailable(config.Resources.CPUsetCPUs)
+ if err != nil {
+ return warnings, fmt.Errorf("invalid value %s for cpuset cpus", config.Resources.CPUsetCPUs)
+ }
+ if !cpusAvailable {
+ return warnings, fmt.Errorf("requested CPUs are not available - requested %s, available: %s", config.Resources.CPUsetCPUs, sysInfo.Cpus)
+ }
+ memsAvailable, err := sysInfo.IsCpusetMemsAvailable(config.Resources.CPUsetMems)
+ if err != nil {
+ return warnings, fmt.Errorf("invalid value %s for cpuset mems", config.Resources.CPUsetMems)
+ }
+ if !memsAvailable {
+ return warnings, fmt.Errorf("requested memory nodes are not available - requested %s, available: %s", config.Resources.CPUsetMems, sysInfo.Mems)
+ }
+
+ // blkio subsystem checks and adjustments
+ if config.Resources.BlkioWeight > 0 && !sysInfo.BlkioWeight {
+ warnings = addWarning(warnings, "Your kernel does not support Block I/O weight or the cgroup is not mounted. Weight discarded.")
+ config.Resources.BlkioWeight = 0
+ }
+ if config.Resources.BlkioWeight > 0 && (config.Resources.BlkioWeight < 10 || config.Resources.BlkioWeight > 1000) {
+ return warnings, fmt.Errorf("range of blkio weight is from 10 to 1000")
+ }
+ if len(config.Resources.BlkioWeightDevice) > 0 && !sysInfo.BlkioWeightDevice {
+ warnings = addWarning(warnings, "Your kernel does not support Block I/O weight_device or the cgroup is not mounted. Weight-device discarded.")
+ config.Resources.BlkioWeightDevice = []string{}
+ }
+ if len(config.Resources.DeviceReadBps) > 0 && !sysInfo.BlkioReadBpsDevice {
+ warnings = addWarning(warnings, "Your kernel does not support BPS Block I/O read limit or the cgroup is not mounted. Block I/O BPS read limit discarded")
+ config.Resources.DeviceReadBps = []string{}
+ }
+ if len(config.Resources.DeviceWriteBps) > 0 && !sysInfo.BlkioWriteBpsDevice {
+ warnings = addWarning(warnings, "Your kernel does not support BPS Block I/O write limit or the cgroup is not mounted. Block I/O BPS write limit discarded.")
+ config.Resources.DeviceWriteBps = []string{}
+ }
+ if len(config.Resources.DeviceReadIOps) > 0 && !sysInfo.BlkioReadIOpsDevice {
+ warnings = addWarning(warnings, "Your kernel does not support IOPS Block read limit or the cgroup is not mounted. Block I/O IOPS read limit discarded.")
+ config.Resources.DeviceReadIOps = []string{}
+ }
+ if len(config.Resources.DeviceWriteIOps) > 0 && !sysInfo.BlkioWriteIOpsDevice {
+ warnings = addWarning(warnings, "Your kernel does not support IOPS Block I/O write limit or the cgroup is not mounted. Block I/O IOPS write limit discarded.")
+ config.Resources.DeviceWriteIOps = []string{}
+ }
+
+ return warnings, nil
+}
diff --git a/cmd/podman/create_cli_test.go b/cmd/podman/create_cli_test.go
new file mode 100644
index 000000000..63a1e5dd3
--- /dev/null
+++ b/cmd/podman/create_cli_test.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ Var1 = []string{"ONE=1", "TWO=2"}
+)
+
+func createTmpFile(content []byte) (string, error) {
+ tmpfile, err := ioutil.TempFile(os.TempDir(), "unittest")
+ if err != nil {
+ return "", err
+ }
+
+ if _, err := tmpfile.Write(content); err != nil {
+ return "", err
+
+ }
+ if err := tmpfile.Close(); err != nil {
+ return "", err
+ }
+ return tmpfile.Name(), nil
+}
+
+func TestConvertStringSliceToMap(t *testing.T) {
+ strSlice := []string{"BLAU=BLUE", "GELB=YELLOW"}
+ result, _ := convertStringSliceToMap(strSlice, "=")
+ assert.Equal(t, result["BLAU"], "BLUE")
+}
+
+func TestConvertStringSliceToMapBadData(t *testing.T) {
+ strSlice := []string{"BLAU=BLUE", "GELB^YELLOW"}
+ _, err := convertStringSliceToMap(strSlice, "=")
+ assert.Error(t, err)
+}
+
+func TestGetAllLabels(t *testing.T) {
+ fileLabels := []string{}
+ labels, _ := getAllLabels(fileLabels, Var1)
+ assert.Equal(t, len(labels), 2)
+}
+
+func TestGetAllLabelsBadKeyValue(t *testing.T) {
+ inLabels := []string{"ONE1", "TWO=2"}
+ fileLabels := []string{}
+ _, err := getAllLabels(fileLabels, inLabels)
+ assert.Error(t, err, assert.AnError)
+}
+
+func TestGetAllLabelsBadLabelFile(t *testing.T) {
+ fileLabels := []string{"/foobar5001/be"}
+ _, err := getAllLabels(fileLabels, Var1)
+ assert.Error(t, err, assert.AnError)
+}
+
+func TestGetAllLabelsFile(t *testing.T) {
+ content := []byte("THREE=3")
+ tFile, err := createTmpFile(content)
+ defer os.Remove(tFile)
+ assert.NoError(t, err)
+ fileLabels := []string{tFile}
+ result, _ := getAllLabels(fileLabels, Var1)
+ assert.Equal(t, len(result), 3)
+}
diff --git a/cmd/podman/diff.go b/cmd/podman/diff.go
new file mode 100644
index 000000000..a3ca9ae50
--- /dev/null
+++ b/cmd/podman/diff.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/containers/storage/pkg/archive"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/urfave/cli"
+)
+
+type diffJSONOutput struct {
+ Changed []string `json:"changed,omitempty"`
+ Added []string `json:"added,omitempty"`
+ Deleted []string `json:"deleted,omitempty"`
+}
+
+type diffOutputParams struct {
+ Change archive.ChangeType
+ Path string
+}
+
+type stdoutStruct struct {
+ output []diffOutputParams
+}
+
+func (so stdoutStruct) Out() error {
+ for _, d := range so.output {
+ fmt.Printf("%s %s\n", d.Change, d.Path)
+ }
+ return nil
+}
+
+var (
+ diffFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "archive",
+ Usage: "Save the diff as a tar archive",
+ Hidden: true,
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Change the output format.",
+ },
+ }
+ diffDescription = fmt.Sprint(`Displays changes on a container or image's filesystem. The
+ container or image will be compared to its parent layer`)
+
+ diffCommand = cli.Command{
+ Name: "diff",
+ Usage: "Inspect changes on container's file systems",
+ Description: diffDescription,
+ Flags: diffFlags,
+ Action: diffCmd,
+ ArgsUsage: "ID-NAME",
+ }
+)
+
+func formatJSON(output []diffOutputParams) (diffJSONOutput, error) {
+ jsonStruct := diffJSONOutput{}
+ for _, output := range output {
+ switch output.Change {
+ case archive.ChangeModify:
+ jsonStruct.Changed = append(jsonStruct.Changed, output.Path)
+ case archive.ChangeAdd:
+ jsonStruct.Added = append(jsonStruct.Added, output.Path)
+ case archive.ChangeDelete:
+ jsonStruct.Deleted = append(jsonStruct.Deleted, output.Path)
+ default:
+ return jsonStruct, errors.Errorf("output kind %q not recognized", output.Change.String())
+ }
+ }
+ return jsonStruct, nil
+}
+
+func diffCmd(c *cli.Context) error {
+ if err := validateFlags(c, diffFlags); err != nil {
+ return err
+ }
+
+ if len(c.Args()) != 1 {
+ return errors.Errorf("container, image, or layer name must be specified: podman diff [options [...]] ID-NAME")
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ to := c.Args().Get(0)
+ changes, err := runtime.GetDiff("", to)
+ if err != nil {
+ return errors.Wrapf(err, "could not get changes for %q", to)
+ }
+
+ diffOutput := []diffOutputParams{}
+ outputFormat := c.String("format")
+
+ for _, change := range changes {
+
+ params := diffOutputParams{
+ Change: change.Kind,
+ Path: change.Path,
+ }
+ diffOutput = append(diffOutput, params)
+ }
+
+ var out formats.Writer
+
+ if outputFormat != "" {
+ switch outputFormat {
+ case formats.JSONString:
+ data, err := formatJSON(diffOutput)
+ if err != nil {
+ return err
+ }
+ out = formats.JSONStruct{Output: data}
+ default:
+ return errors.New("only valid format for diff is 'json'")
+ }
+ } else {
+ out = stdoutStruct{output: diffOutput}
+ }
+ formats.Writer(out).Out()
+
+ return nil
+}
diff --git a/cmd/podman/docker/types.go b/cmd/podman/docker/types.go
new file mode 100644
index 000000000..a7e456554
--- /dev/null
+++ b/cmd/podman/docker/types.go
@@ -0,0 +1,271 @@
+package docker
+
+//
+// Types extracted from Docker
+//
+
+import (
+ "time"
+
+ "github.com/containers/image/pkg/strslice"
+ "github.com/opencontainers/go-digest"
+)
+
+// TypeLayers github.com/docker/docker/image/rootfs.go
+const TypeLayers = "layers"
+
+// V2S2MediaTypeManifest github.com/docker/distribution/manifest/schema2/manifest.go
+const V2S2MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
+
+// V2S2MediaTypeImageConfig github.com/docker/distribution/manifest/schema2/manifest.go
+const V2S2MediaTypeImageConfig = "application/vnd.docker.container.image.v1+json"
+
+// V2S2MediaTypeLayer github.com/docker/distribution/manifest/schema2/manifest.go
+const V2S2MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
+
+// V2S2MediaTypeUncompressedLayer github.com/docker/distribution/manifest/schema2/manifest.go
+const V2S2MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar"
+
+// V2S2RootFS describes images root filesystem
+// This is currently a placeholder that only supports layers. In the future
+// this can be made into an interface that supports different implementations.
+// github.com/docker/docker/image/rootfs.go
+type V2S2RootFS struct {
+ Type string `json:"type"`
+ DiffIDs []digest.Digest `json:"diff_ids,omitempty"`
+}
+
+// V2S2History stores build commands that were used to create an image
+// github.com/docker/docker/image/image.go
+type V2S2History struct {
+ // Created is the timestamp at which the image was created
+ Created time.Time `json:"created"`
+ // Author is the name of the author that was specified when committing the image
+ Author string `json:"author,omitempty"`
+ // CreatedBy keeps the Dockerfile command used while building the image
+ CreatedBy string `json:"created_by,omitempty"`
+ // Comment is the commit message that was set when committing the image
+ Comment string `json:"comment,omitempty"`
+ // EmptyLayer is set to true if this history item did not generate a
+ // layer. Otherwise, the history item is associated with the next
+ // layer in the RootFS section.
+ EmptyLayer bool `json:"empty_layer,omitempty"`
+}
+
+// ID is the content-addressable ID of an image.
+// github.com/docker/docker/image/image.go
+type ID digest.Digest
+
+// HealthConfig holds configuration settings for the HEALTHCHECK feature.
+// github.com/docker/docker/api/types/container/config.go
+type HealthConfig struct {
+ // Test is the test to perform to check that the container is healthy.
+ // An empty slice means to inherit the default.
+ // The options are:
+ // {} : inherit healthcheck
+ // {"NONE"} : disable healthcheck
+ // {"CMD", args...} : exec arguments directly
+ // {"CMD-SHELL", command} : run command with system's default shell
+ Test []string `json:",omitempty"`
+
+ // Zero means to inherit. Durations are expressed as integer nanoseconds.
+ Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
+ Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
+
+ // Retries is the number of consecutive failures needed to consider a container as unhealthy.
+ // Zero means inherit.
+ Retries int `json:",omitempty"`
+}
+
+// PortSet is a collection of structs indexed by Port
+// github.com/docker/go-connections/nat/nat.go
+type PortSet map[Port]struct{}
+
+// Port is a string containing port number and protocol in the format "80/tcp"
+// github.com/docker/go-connections/nat/nat.go
+type Port string
+
+// Config contains the configuration data about a container.
+// It should hold only portable information about the container.
+// Here, "portable" means "independent from the host we are running on".
+// Non-portable information *should* appear in HostConfig.
+// All fields added to this struct must be marked `omitempty` to keep getting
+// predictable hashes from the old `v1Compatibility` configuration.
+// github.com/docker/docker/api/types/container/config.go
+type Config struct {
+ Hostname string // Hostname
+ Domainname string // Domainname
+ User string // User that will run the command(s) inside the container, also support user:group
+ AttachStdin bool // Attach the standard input, makes possible user interaction
+ AttachStdout bool // Attach the standard output
+ AttachStderr bool // Attach the standard error
+ ExposedPorts PortSet `json:",omitempty"` // List of exposed ports
+ Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
+ OpenStdin bool // Open stdin
+ StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
+ Env []string // List of environment variable to set in the container
+ Cmd strslice.StrSlice // Command to run when starting the container
+ Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy
+ ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific)
+ Image string // Name of the image as it was passed by the operator (e.g. could be symbolic)
+ Volumes map[string]struct{} // List of volumes (mounts) used for the container
+ WorkingDir string // Current directory (PWD) in the command will be launched
+ Entrypoint strslice.StrSlice // Entrypoint to run when starting the container
+ NetworkDisabled bool `json:",omitempty"` // Is network disabled
+ MacAddress string `json:",omitempty"` // Mac Address of the container
+ OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile
+ Labels map[string]string // List of labels set to this container
+ StopSignal string `json:",omitempty"` // Signal to stop a container
+ StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container
+ Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
+}
+
+// V1Compatibility - For non-top-level layers, create fake V1Compatibility
+// strings that fit the format and don't collide with anything else, but
+// don't result in runnable images on their own.
+// github.com/docker/distribution/manifest/schema1/config_builder.go
+type V1Compatibility struct {
+ ID string `json:"id"`
+ Parent string `json:"parent,omitempty"`
+ Comment string `json:"comment,omitempty"`
+ Created time.Time `json:"created"`
+ ContainerConfig struct {
+ Cmd []string
+ } `json:"container_config,omitempty"`
+ Author string `json:"author,omitempty"`
+ ThrowAway bool `json:"throwaway,omitempty"`
+}
+
+// V1Image stores the V1 image configuration.
+// github.com/docker/docker/image/image.go
+type V1Image struct {
+ // ID is a unique 64 character identifier of the image
+ ID string `json:"id,omitempty"`
+ // Parent is the ID of the parent image
+ Parent string `json:"parent,omitempty"`
+ // Comment is the commit message that was set when committing the image
+ Comment string `json:"comment,omitempty"`
+ // Created is the timestamp at which the image was created
+ Created time.Time `json:"created"`
+ // Container is the id of the container used to commit
+ Container string `json:"container,omitempty"`
+ // ContainerConfig is the configuration of the container that is committed into the image
+ ContainerConfig Config `json:"container_config,omitempty"`
+ // DockerVersion specifies the version of Docker that was used to build the image
+ DockerVersion string `json:"docker_version,omitempty"`
+ // Author is the name of the author that was specified when committing the image
+ Author string `json:"author,omitempty"`
+ // Config is the configuration of the container received from the client
+ Config *Config `json:"config,omitempty"`
+ // Architecture is the hardware that the image is build and runs on
+ Architecture string `json:"architecture,omitempty"`
+ // OS is the operating system used to build and run the image
+ OS string `json:"os,omitempty"`
+ // Size is the total size of the image including all layers it is composed of
+ Size int64 `json:",omitempty"`
+}
+
+// V2Image stores the image configuration
+// github.com/docker/docker/image/image.go
+type V2Image struct {
+ V1Image
+ Parent ID `json:"parent,omitempty"`
+ RootFS *V2S2RootFS `json:"rootfs,omitempty"`
+ History []V2S2History `json:"history,omitempty"`
+ OSVersion string `json:"os.version,omitempty"`
+ OSFeatures []string `json:"os.features,omitempty"`
+
+ // rawJSON caches the immutable JSON associated with this image.
+ //rawJSON []byte
+
+ // computedID is the ID computed from the hash of the image config.
+ // Not to be confused with the legacy V1 ID in V1Image.
+ //computedID ID
+}
+
+// V2Versioned provides a struct with the manifest schemaVersion and mediaType.
+// Incoming content with unknown schema version can be decoded against this
+// struct to check the version.
+// github.com/docker/distribution/manifest/versioned.go
+type V2Versioned struct {
+ // SchemaVersion is the image manifest schema that this image follows
+ SchemaVersion int `json:"schemaVersion"`
+
+ // MediaType is the media type of this schema.
+ MediaType string `json:"mediaType,omitempty"`
+}
+
+// V2S1FSLayer is a container struct for BlobSums defined in an image manifest
+// github.com/docker/distribution/manifest/schema1/manifest.go
+type V2S1FSLayer struct {
+ // BlobSum is the tarsum of the referenced filesystem image layer
+ BlobSum digest.Digest `json:"blobSum"`
+}
+
+// V2S1History stores unstructured v1 compatibility information
+// github.com/docker/distribution/manifest/schema1/manifest.go
+type V2S1History struct {
+ // V1Compatibility is the raw v1 compatibility information
+ V1Compatibility string `json:"v1Compatibility"`
+}
+
+// V2S1Manifest provides the base accessible fields for working with V2 image
+// format in the registry.
+// github.com/docker/distribution/manifest/schema1/manifest.go
+type V2S1Manifest struct {
+ V2Versioned
+
+ // Name is the name of the image's repository
+ Name string `json:"name"`
+
+ // Tag is the tag of the image specified by this manifest
+ Tag string `json:"tag"`
+
+ // Architecture is the host architecture on which this image is intended to
+ // run
+ Architecture string `json:"architecture"`
+
+ // FSLayers is a list of filesystem layer blobSums contained in this image
+ FSLayers []V2S1FSLayer `json:"fsLayers"`
+
+ // History is a list of unstructured historical data for v1 compatibility
+ History []V2S1History `json:"history"`
+}
+
+// V2S2Descriptor describes targeted content. Used in conjunction with a blob
+// store, a descriptor can be used to fetch, store and target any kind of
+// blob. The struct also describes the wire protocol format. Fields should
+// only be added but never changed.
+// github.com/docker/distribution/blobs.go
+type V2S2Descriptor struct {
+ // MediaType describe the type of the content. All text based formats are
+ // encoded as utf-8.
+ MediaType string `json:"mediaType,omitempty"`
+
+ // Size in bytes of content.
+ Size int64 `json:"size,omitempty"`
+
+ // Digest uniquely identifies the content. A byte stream can be verified
+ // against against this digest.
+ Digest digest.Digest `json:"digest,omitempty"`
+
+ // URLs contains the source URLs of this content.
+ URLs []string `json:"urls,omitempty"`
+
+ // NOTE: Before adding a field here, please ensure that all
+ // other options have been exhausted. Much of the type relationships
+ // depend on the simplicity of this type.
+}
+
+// V2S2Manifest defines a schema2 manifest.
+// github.com/docker/distribution/manifest/schema2/manifest.go
+type V2S2Manifest struct {
+ V2Versioned
+
+ // Config references the image configuration as a blob.
+ Config V2S2Descriptor `json:"config"`
+
+ // Layers lists descriptors for the layers referenced by the
+ // configuration.
+ Layers []V2S2Descriptor `json:"layers"`
+}
diff --git a/cmd/podman/exec.go b/cmd/podman/exec.go
new file mode 100644
index 000000000..0b3b9504d
--- /dev/null
+++ b/cmd/podman/exec.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ execFlags = []cli.Flag{
+ cli.StringSliceFlag{
+ Name: "env, e",
+ Usage: "Set environment variables",
+ },
+ cli.BoolFlag{
+ Name: "privileged",
+ Usage: "Give the process extended Linux capabilities inside the container. The default is false",
+ },
+ cli.BoolFlag{
+ Name: "tty, t",
+ Usage: "Allocate a pseudo-TTY. The default is false",
+ },
+ cli.StringFlag{
+ Name: "user, u",
+ Usage: "Sets the username or UID used and optionally the groupname or GID for the specified command",
+ },
+ }
+ execDescription = `
+ podman exec
+
+ Run a command in a running container
+`
+
+ execCommand = cli.Command{
+ Name: "exec",
+ Usage: "Run a process in a running container",
+ Description: execDescription,
+ Flags: execFlags,
+ Action: execCmd,
+ ArgsUsage: "CONTAINER-NAME",
+ }
+)
+
+func execCmd(c *cli.Context) error {
+ var envs []string
+ args := c.Args()
+ if len(args) < 1 {
+ return errors.Errorf("you must provide one container name or id")
+ }
+ if len(args) < 2 {
+ return errors.Errorf("you must provide a command to exec")
+ }
+ cmd := args[1:]
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ ctr, err := runtime.LookupContainer(args[0])
+ if err != nil {
+ return errors.Wrapf(err, "unable to exec into %s", args[0])
+ }
+ // Create a list of keys provided by the user
+ var userEnvKeys []string
+ for _, env := range c.StringSlice("env") {
+ splitEnv := strings.Split(env, "=")
+ userEnvKeys = append(userEnvKeys, splitEnv[0])
+ }
+
+ envs = append(envs, c.StringSlice("env")...)
+
+ // if the default key isnt in the user-provided list, add the default
+ // key and value to the environment variables. this is needed to set
+ // PATH for example.
+ for k, v := range defaultEnvVariables {
+ if !libpod.StringInSlice(k, userEnvKeys) {
+ envs = append(envs, fmt.Sprintf("%s=%s", k, v))
+ }
+ }
+
+ return ctr.Exec(c.Bool("tty"), c.Bool("privileged"), envs, cmd, c.String("user"))
+}
diff --git a/cmd/podman/export.go b/cmd/podman/export.go
new file mode 100644
index 000000000..9b498562e
--- /dev/null
+++ b/cmd/podman/export.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var (
+ exportFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "output, o",
+ Usage: "Write to a file, default is STDOUT",
+ Value: "/dev/stdout",
+ },
+ }
+ exportDescription = "Exports container's filesystem contents as a tar archive" +
+ " and saves it on the local machine."
+ exportCommand = cli.Command{
+ Name: "export",
+ Usage: "Export container's filesystem contents as a tar archive",
+ Description: exportDescription,
+ Flags: exportFlags,
+ Action: exportCmd,
+ ArgsUsage: "CONTAINER",
+ }
+)
+
+// exportCmd saves a container to a tarball on disk
+func exportCmd(c *cli.Context) error {
+ if err := validateFlags(c, exportFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) == 0 {
+ return errors.Errorf("container id must be specified")
+ }
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments given, need 1 at most.")
+ }
+
+ output := c.String("output")
+ if output == "/dev/stdout" {
+ file := os.Stdout
+ if logrus.IsTerminal(file) {
+ return errors.Errorf("refusing to export to terminal. Use -o flag or redirect")
+ }
+ }
+
+ ctr, err := runtime.LookupContainer(args[0])
+ if err != nil {
+ return errors.Wrapf(err, "error looking up container %q", args[0])
+ }
+
+ return ctr.Export(output)
+}
diff --git a/cmd/podman/formats/formats.go b/cmd/podman/formats/formats.go
new file mode 100644
index 000000000..4b6527b30
--- /dev/null
+++ b/cmd/podman/formats/formats.go
@@ -0,0 +1,143 @@
+package formats
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+ "text/tabwriter"
+ "text/template"
+
+ "github.com/ghodss/yaml"
+ "github.com/pkg/errors"
+)
+
+const (
+ // JSONString const to save on duplicate variable names
+ JSONString = "json"
+ // IDString const to save on duplicates for Go templates
+ IDString = "{{.ID}}"
+)
+
+// Writer interface for outputs
+type Writer interface {
+ Out() error
+}
+
+// JSONStructArray for JSON output
+type JSONStructArray struct {
+ Output []interface{}
+}
+
+// StdoutTemplateArray for Go template output
+type StdoutTemplateArray struct {
+ Output []interface{}
+ Template string
+ Fields map[string]string
+}
+
+// JSONStruct for JSON output
+type JSONStruct struct {
+ Output interface{}
+}
+
+// StdoutTemplate for Go template output
+type StdoutTemplate struct {
+ Output interface{}
+ Template string
+ Fields map[string]string
+}
+
+// YAMLStruct for YAML output
+type YAMLStruct struct {
+ Output interface{}
+}
+
+// Out method for JSON Arrays
+func (j JSONStructArray) Out() error {
+ data, err := json.MarshalIndent(j.Output, "", " ")
+ if err != nil {
+ return err
+ }
+
+ // JSON returns a byte array with a literal null [110 117 108 108] in it
+ // if it is passed empty data. We used bytes.Compare to see if that is
+ // the case.
+ if diff := bytes.Compare(data, []byte("null")); diff == 0 {
+ data = []byte("[]")
+ }
+
+ // If the we did get NULL back, we should spit out {} which is
+ // at least valid JSON for the consumer.
+ fmt.Printf("%s\n", data)
+ return nil
+}
+
+// Out method for Go templates
+func (t StdoutTemplateArray) Out() error {
+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
+ if strings.HasPrefix(t.Template, "table") {
+ // replace any spaces with tabs in template so that tabwriter can align it
+ t.Template = strings.Replace(strings.TrimSpace(t.Template[5:]), " ", "\t", -1)
+ headerTmpl, err := template.New("header").Funcs(headerFunctions).Parse(t.Template)
+ if err != nil {
+ return errors.Wrapf(err, "Template parsing error")
+ }
+ err = headerTmpl.Execute(w, t.Fields)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(w, "")
+ }
+ t.Template = strings.Replace(t.Template, " ", "\t", -1)
+ tmpl, err := template.New("image").Funcs(basicFunctions).Parse(t.Template)
+ if err != nil {
+ return errors.Wrapf(err, "Template parsing error")
+ }
+ for _, img := range t.Output {
+ basicTmpl := tmpl.Funcs(basicFunctions)
+ err = basicTmpl.Execute(w, img)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(w, "")
+ }
+ return w.Flush()
+}
+
+// Out method for JSON struct
+func (j JSONStruct) Out() error {
+ data, err := json.MarshalIndent(j.Output, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%s\n", data)
+ return nil
+}
+
+//Out method for Go templates
+func (t StdoutTemplate) Out() error {
+ tmpl, err := template.New("image").Parse(t.Template)
+ if err != nil {
+ return errors.Wrapf(err, "template parsing error")
+ }
+ err = tmpl.Execute(os.Stdout, t.Output)
+ if err != nil {
+ return err
+ }
+ fmt.Println()
+ return nil
+}
+
+// Out method for YAML
+func (y YAMLStruct) Out() error {
+ var buf []byte
+ var err error
+ buf, err = yaml.Marshal(y.Output)
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(buf))
+ return nil
+}
diff --git a/cmd/podman/formats/templates.go b/cmd/podman/formats/templates.go
new file mode 100644
index 000000000..c2582552a
--- /dev/null
+++ b/cmd/podman/formats/templates.go
@@ -0,0 +1,78 @@
+package formats
+
+import (
+ "bytes"
+ "encoding/json"
+ "strings"
+ "text/template"
+)
+
+// basicFunctions are the set of initial
+// functions provided to every template.
+var basicFunctions = template.FuncMap{
+ "json": func(v interface{}) string {
+ buf := &bytes.Buffer{}
+ enc := json.NewEncoder(buf)
+ enc.SetEscapeHTML(false)
+ _ = enc.Encode(v)
+ // Remove the trailing new line added by the encoder
+ return strings.TrimSpace(buf.String())
+ },
+ "split": strings.Split,
+ "join": strings.Join,
+ "title": strings.Title,
+ "lower": strings.ToLower,
+ "upper": strings.ToUpper,
+ "pad": padWithSpace,
+ "truncate": truncateWithLength,
+}
+
+// HeaderFunctions are used to created headers of a table.
+// This is a replacement of basicFunctions for header generation
+// because we want the header to remain intact.
+// Some functions like `split` are irrelevant so not added.
+var headerFunctions = template.FuncMap{
+ "json": func(v string) string {
+ return v
+ },
+ "title": func(v string) string {
+ return v
+ },
+ "lower": func(v string) string {
+ return v
+ },
+ "upper": func(v string) string {
+ return v
+ },
+ "truncate": func(v string, l int) string {
+ return v
+ },
+}
+
+// Parse creates a new anonymous template with the basic functions
+// and parses the given format.
+func Parse(format string) (*template.Template, error) {
+ return NewParse("", format)
+}
+
+// NewParse creates a new tagged template with the basic functions
+// and parses the given format.
+func NewParse(tag, format string) (*template.Template, error) {
+ return template.New(tag).Funcs(basicFunctions).Parse(format)
+}
+
+// padWithSpace adds whitespace to the input if the input is non-empty
+func padWithSpace(source string, prefix, suffix int) string {
+ if source == "" {
+ return source
+ }
+ return strings.Repeat(" ", prefix) + source + strings.Repeat(" ", suffix)
+}
+
+// truncateWithLength truncates the source string up to the length provided by the input
+func truncateWithLength(source string, length int) string {
+ if len(source) < length {
+ return source
+ }
+ return source[:length]
+}
diff --git a/cmd/podman/history.go b/cmd/podman/history.go
new file mode 100644
index 000000000..f142f5fd4
--- /dev/null
+++ b/cmd/podman/history.go
@@ -0,0 +1,246 @@
+package main
+
+import (
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/containers/image/types"
+ units "github.com/docker/go-units"
+ "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/urfave/cli"
+)
+
+const (
+ createdByTruncLength = 45
+ idTruncLength = 12
+)
+
+// historyTemplateParams stores info about each layer
+type historyTemplateParams struct {
+ ID string
+ Created string
+ CreatedBy string
+ Size string
+ Comment string
+}
+
+// historyJSONParams is only used when the JSON format is specified,
+// and is better for data processing from JSON.
+// historyJSONParams will be populated by data from v1.History and types.BlobInfo,
+// the members of the struct are the sama data types as their sources.
+type historyJSONParams struct {
+ ID string `json:"id"`
+ Created *time.Time `json:"created"`
+ CreatedBy string `json:"createdBy"`
+ Size int64 `json:"size"`
+ Comment string `json:"comment"`
+}
+
+// historyOptions stores cli flag values
+type historyOptions struct {
+ human bool
+ noTrunc bool
+ quiet bool
+ format string
+}
+
+var (
+ historyFlags = []cli.Flag{
+ cli.BoolTFlag{
+ Name: "human, H",
+ Usage: "Display sizes and dates in human readable format",
+ },
+ cli.BoolFlag{
+ Name: "no-trunc, notruncate",
+ Usage: "Do not truncate the output",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "Display the numeric IDs only",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Change the output to JSON or a Go template",
+ },
+ }
+
+ historyDescription = "Displays the history of an image. The information can be printed out in an easy to read, " +
+ "or user specified format, and can be truncated."
+ historyCommand = cli.Command{
+ Name: "history",
+ Usage: "Show history of a specified image",
+ Description: historyDescription,
+ Flags: historyFlags,
+ Action: historyCmd,
+ ArgsUsage: "",
+ UseShortOptionHandling: true,
+ }
+)
+
+func historyCmd(c *cli.Context) error {
+ if err := validateFlags(c, historyFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ format := genHistoryFormat(c.String("format"), c.Bool("quiet"))
+
+ args := c.Args()
+ if len(args) == 0 {
+ return errors.Errorf("an image name must be specified")
+ }
+ if len(args) > 1 {
+ return errors.Errorf("podman history takes at most 1 argument")
+ }
+ imgName := args[0]
+
+ opts := historyOptions{
+ human: c.BoolT("human"),
+ noTrunc: c.Bool("no-trunc"),
+ quiet: c.Bool("quiet"),
+ format: format,
+ }
+
+ history, layers, imageID, err := runtime.GetHistory(imgName)
+ if err != nil {
+ return errors.Wrapf(err, "error getting history of image %q", imgName)
+ }
+
+ return generateHistoryOutput(history, layers, imageID, opts)
+}
+
+func genHistoryFormat(format string, quiet bool) string {
+ if format != "" {
+ // "\t" from the command line is not being recognized as a tab
+ // replacing the string "\t" to a tab character if the user passes in "\t"
+ return strings.Replace(format, `\t`, "\t", -1)
+ }
+ if quiet {
+ return formats.IDString
+ }
+ return "table {{.ID}}\t{{.Created}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}\t"
+}
+
+// historyToGeneric makes an empty array of interfaces for output
+func historyToGeneric(templParams []historyTemplateParams, JSONParams []historyJSONParams) (genericParams []interface{}) {
+ if len(templParams) > 0 {
+ for _, v := range templParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+ }
+ for _, v := range JSONParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+}
+
+// generate the header based on the template provided
+func (h *historyTemplateParams) headerMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(h))
+ values := make(map[string]string)
+ for h := 0; h < v.NumField(); h++ {
+ key := v.Type().Field(h).Name
+ value := key
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+// getHistorytemplateOutput gets the modified history information to be printed in human readable format
+func getHistoryTemplateOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) (historyOutput []historyTemplateParams) {
+ var (
+ outputSize string
+ createdTime string
+ createdBy string
+ count = 1
+ )
+ for i := len(history) - 1; i >= 0; i-- {
+ if i != len(history)-1 {
+ imageID = "<missing>"
+ }
+ if !opts.noTrunc && i == len(history)-1 {
+ imageID = imageID[:idTruncLength]
+ }
+
+ var size int64
+ if !history[i].EmptyLayer {
+ size = layers[len(layers)-count].Size
+ count++
+ }
+
+ if opts.human {
+ createdTime = units.HumanDuration(time.Since((*history[i].Created))) + " ago"
+ outputSize = units.HumanSize(float64(size))
+ } else {
+ createdTime = (history[i].Created).Format(time.RFC3339)
+ outputSize = strconv.FormatInt(size, 10)
+ }
+
+ createdBy = strings.Join(strings.Fields(history[i].CreatedBy), " ")
+ if !opts.noTrunc && len(createdBy) > createdByTruncLength {
+ createdBy = createdBy[:createdByTruncLength-3] + "..."
+ }
+
+ params := historyTemplateParams{
+ ID: imageID,
+ Created: createdTime,
+ CreatedBy: createdBy,
+ Size: outputSize,
+ Comment: history[i].Comment,
+ }
+ historyOutput = append(historyOutput, params)
+ }
+ return
+}
+
+// getHistoryJSONOutput returns the history information in its raw form
+func getHistoryJSONOutput(history []v1.History, layers []types.BlobInfo, imageID string) (historyOutput []historyJSONParams) {
+ count := 1
+ for i := len(history) - 1; i >= 0; i-- {
+ var size int64
+ if !history[i].EmptyLayer {
+ size = layers[len(layers)-count].Size
+ count++
+ }
+
+ params := historyJSONParams{
+ ID: imageID,
+ Created: history[i].Created,
+ CreatedBy: history[i].CreatedBy,
+ Size: size,
+ Comment: history[i].Comment,
+ }
+ historyOutput = append(historyOutput, params)
+ }
+ return
+}
+
+// generateHistoryOutput generates the history based on the format given
+func generateHistoryOutput(history []v1.History, layers []types.BlobInfo, imageID string, opts historyOptions) error {
+ if len(history) == 0 {
+ return nil
+ }
+
+ var out formats.Writer
+
+ switch opts.format {
+ case formats.JSONString:
+ historyOutput := getHistoryJSONOutput(history, layers, imageID)
+ out = formats.JSONStructArray{Output: historyToGeneric([]historyTemplateParams{}, historyOutput)}
+ default:
+ historyOutput := getHistoryTemplateOutput(history, layers, imageID, opts)
+ out = formats.StdoutTemplateArray{Output: historyToGeneric(historyOutput, []historyJSONParams{}), Template: opts.format, Fields: historyOutput[0].headerMap()}
+ }
+
+ return formats.Writer(out).Out()
+}
diff --git a/cmd/podman/images.go b/cmd/podman/images.go
new file mode 100644
index 000000000..90dd8ae12
--- /dev/null
+++ b/cmd/podman/images.go
@@ -0,0 +1,337 @@
+package main
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/containers/storage"
+ "github.com/docker/go-units"
+ digest "github.com/opencontainers/go-digest"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/projectatomic/libpod/libpod/common"
+ "github.com/urfave/cli"
+)
+
+type imagesTemplateParams struct {
+ Repository string
+ Tag string
+ ID string
+ Digest digest.Digest
+ Created string
+ Size string
+}
+
+type imagesJSONParams struct {
+ ID string `json:"id"`
+ Name []string `json:"names"`
+ Digest digest.Digest `json:"digest"`
+ Created time.Time `json:"created"`
+ Size int64 `json:"size"`
+}
+
+type imagesOptions struct {
+ quiet bool
+ noHeading bool
+ noTrunc bool
+ digests bool
+ format string
+}
+
+var (
+ imagesFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "display only image IDs",
+ },
+ cli.BoolFlag{
+ Name: "noheading, n",
+ Usage: "do not print column headings",
+ },
+ cli.BoolFlag{
+ Name: "no-trunc, notruncate",
+ Usage: "do not truncate output",
+ },
+ cli.BoolFlag{
+ Name: "digests",
+ Usage: "show digests",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Change the output format to JSON or a Go template",
+ },
+ cli.StringFlag{
+ Name: "filter, f",
+ Usage: "filter output based on conditions provided (default [])",
+ },
+ }
+
+ imagesDescription = "lists locally stored images."
+ imagesCommand = cli.Command{
+ Name: "images",
+ Usage: "list images in local storage",
+ Description: imagesDescription,
+ Flags: imagesFlags,
+ Action: imagesCmd,
+ ArgsUsage: "",
+ UseShortOptionHandling: true,
+ }
+)
+
+func imagesCmd(c *cli.Context) error {
+ if err := validateFlags(c, imagesFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "Could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ format := genImagesFormat(c.String("format"), c.Bool("quiet"), c.Bool("noheading"), c.Bool("digests"))
+
+ opts := imagesOptions{
+ quiet: c.Bool("quiet"),
+ noHeading: c.Bool("noheading"),
+ noTrunc: c.Bool("no-trunc"),
+ digests: c.Bool("digests"),
+ format: format,
+ }
+
+ var imageInput string
+ if len(c.Args()) == 1 {
+ imageInput = c.Args().Get(0)
+ }
+ if len(c.Args()) > 1 {
+ return errors.New("'podman images' requires at most 1 argument")
+ }
+
+ params, err := runtime.ParseImageFilter(imageInput, c.String("filter"))
+ if err != nil {
+ return errors.Wrapf(err, "error parsing filter")
+ }
+
+ // generate the different filters
+ labelFilter := generateImagesFilter(params, "label")
+ beforeImageFilter := generateImagesFilter(params, "before-image")
+ sinceImageFilter := generateImagesFilter(params, "since-image")
+ danglingFilter := generateImagesFilter(params, "dangling")
+ referenceFilter := generateImagesFilter(params, "reference")
+ imageInputFilter := generateImagesFilter(params, "image-input")
+
+ images, err := runtime.GetImages(params, labelFilter, beforeImageFilter, sinceImageFilter, danglingFilter, referenceFilter, imageInputFilter)
+ if err != nil {
+ return errors.Wrapf(err, "could not get list of images matching filter")
+ }
+
+ return generateImagesOutput(runtime, images, opts)
+}
+
+func genImagesFormat(format string, quiet, noHeading, digests bool) string {
+ if format != "" {
+ // "\t" from the command line is not being recognized as a tab
+ // replacing the string "\t" to a tab character if the user passes in "\t"
+ return strings.Replace(format, `\t`, "\t", -1)
+ }
+ if quiet {
+ return formats.IDString
+ }
+ format = "table {{.Repository}}\t{{.Tag}}\t"
+ if noHeading {
+ format = "{{.Repository}}\t{{.Tag}}\t"
+ }
+ if digests {
+ format += "{{.Digest}}\t"
+ }
+ format += "{{.ID}}\t{{.Created}}\t{{.Size}}\t"
+ return format
+}
+
+// imagesToGeneric creates an empty array of interfaces for output
+func imagesToGeneric(templParams []imagesTemplateParams, JSONParams []imagesJSONParams) (genericParams []interface{}) {
+ if len(templParams) > 0 {
+ for _, v := range templParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+ }
+ for _, v := range JSONParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+}
+
+// generate the header based on the template provided
+func (i *imagesTemplateParams) headerMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(i))
+ values := make(map[string]string)
+
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ if value == "ID" {
+ value = "Image" + value
+ }
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+// getImagesTemplateOutput returns the images information to be printed in human readable format
+func getImagesTemplateOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) (imagesOutput []imagesTemplateParams) {
+ var (
+ lastID string
+ )
+ for _, img := range images {
+ if opts.quiet && lastID == img.ID {
+ continue // quiet should not show the same ID multiple times
+ }
+ createdTime := img.Created
+
+ imageID := "sha256:" + img.ID
+ if !opts.noTrunc {
+ imageID = img.ID[:idTruncLength]
+ }
+
+ repository := "<none>"
+ tag := "<none>"
+ if len(img.Names) > 0 {
+ arr := strings.Split(img.Names[0], ":")
+ repository = arr[0]
+ if len(arr) == 2 {
+ tag = arr[1]
+ }
+ }
+
+ imgData, _ := runtime.GetImageInspectInfo(*img)
+ if imgData != nil {
+ createdTime = *imgData.Created
+ }
+
+ params := imagesTemplateParams{
+ Repository: repository,
+ Tag: tag,
+ ID: imageID,
+ Digest: imgData.Digest,
+ Created: units.HumanDuration(time.Since((createdTime))) + " ago",
+ Size: units.HumanSizeWithPrecision(float64(imgData.Size), 3),
+ }
+ imagesOutput = append(imagesOutput, params)
+ }
+ return
+}
+
+// getImagesJSONOutput returns the images information in its raw form
+func getImagesJSONOutput(runtime *libpod.Runtime, images []*storage.Image) (imagesOutput []imagesJSONParams) {
+ for _, img := range images {
+ createdTime := img.Created
+
+ imgData, _ := runtime.GetImageInspectInfo(*img)
+ if imgData != nil {
+ createdTime = *imgData.Created
+ }
+
+ params := imagesJSONParams{
+ ID: img.ID,
+ Name: img.Names,
+ Digest: imgData.Digest,
+ Created: createdTime,
+ Size: imgData.Size,
+ }
+ imagesOutput = append(imagesOutput, params)
+ }
+ return
+}
+
+// generateImagesOutput generates the images based on the format provided
+func generateImagesOutput(runtime *libpod.Runtime, images []*storage.Image, opts imagesOptions) error {
+ if len(images) == 0 {
+ return nil
+ }
+
+ var out formats.Writer
+
+ switch opts.format {
+ case formats.JSONString:
+ imagesOutput := getImagesJSONOutput(runtime, images)
+ out = formats.JSONStructArray{Output: imagesToGeneric([]imagesTemplateParams{}, imagesOutput)}
+ default:
+ imagesOutput := getImagesTemplateOutput(runtime, images, opts)
+ out = formats.StdoutTemplateArray{Output: imagesToGeneric(imagesOutput, []imagesJSONParams{}), Template: opts.format, Fields: imagesOutput[0].headerMap()}
+
+ }
+
+ return formats.Writer(out).Out()
+}
+
+// generateImagesFilter returns an ImageFilter based on filterType
+// to add more filters, define a new case and write what the ImageFilter function should do
+func generateImagesFilter(params *libpod.ImageFilterParams, filterType string) libpod.ImageFilter {
+ switch filterType {
+ case "label":
+ return func(image *storage.Image, info *libpod.ImageData) bool {
+ if params == nil || params.Label == "" {
+ return true
+ }
+
+ pair := strings.SplitN(params.Label, "=", 2)
+ if val, ok := info.Labels[pair[0]]; ok {
+ if len(pair) == 2 && val == pair[1] {
+ return true
+ }
+ if len(pair) == 1 {
+ return true
+ }
+ }
+ return false
+ }
+ case "before-image":
+ return func(image *storage.Image, info *libpod.ImageData) bool {
+ if params == nil || params.BeforeImage.IsZero() {
+ return true
+ }
+ return info.Created.Before(params.BeforeImage)
+ }
+ case "since-image":
+ return func(image *storage.Image, info *libpod.ImageData) bool {
+ if params == nil || params.SinceImage.IsZero() {
+ return true
+ }
+ return info.Created.After(params.SinceImage)
+ }
+ case "dangling":
+ return func(image *storage.Image, info *libpod.ImageData) bool {
+ if params == nil || params.Dangling == "" {
+ return true
+ }
+ if common.IsFalse(params.Dangling) && params.ImageName != "<none>" {
+ return true
+ }
+ if common.IsTrue(params.Dangling) && params.ImageName == "<none>" {
+ return true
+ }
+ return false
+ }
+ case "reference":
+ return func(image *storage.Image, info *libpod.ImageData) bool {
+ if params == nil || params.ReferencePattern == "" {
+ return true
+ }
+ return libpod.MatchesReference(params.ImageName, params.ReferencePattern)
+ }
+ case "image-input":
+ return func(image *storage.Image, info *libpod.ImageData) bool {
+ if params == nil || params.ImageInput == "" {
+ return true
+ }
+ return libpod.MatchesReference(params.ImageName, params.ImageInput)
+ }
+ default:
+ fmt.Println("invalid filter type", filterType)
+ return nil
+ }
+}
diff --git a/cmd/podman/import.go b/cmd/podman/import.go
new file mode 100644
index 000000000..2e8702c3d
--- /dev/null
+++ b/cmd/podman/import.go
@@ -0,0 +1,190 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+
+ "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ importFlags = []cli.Flag{
+ cli.StringSliceFlag{
+ Name: "change, c",
+ Usage: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR",
+ },
+ cli.StringFlag{
+ Name: "message, m",
+ Usage: "Set commit message for imported image",
+ },
+ }
+ importDescription = `Create a container image from the contents of the specified tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz).
+ Note remote tar balls can be specified, via web address.
+ Optionally tag the image. You can specify the Dockerfile instructions using the --change option.
+ `
+ importCommand = cli.Command{
+ Name: "import",
+ Usage: "Import a tarball to create a filesystem image",
+ Description: importDescription,
+ Flags: importFlags,
+ Action: importCmd,
+ ArgsUsage: "TARBALL [REFERENCE]",
+ }
+)
+
+func importCmd(c *cli.Context) error {
+ if err := validateFlags(c, importFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ var opts libpod.CopyOptions
+ var source string
+ args := c.Args()
+ switch len(args) {
+ case 0:
+ return errors.Errorf("need to give the path to the tarball, or must specify a tarball of '-' for stdin")
+ case 1:
+ source = args[0]
+ case 2:
+ source = args[0]
+ opts.Reference = args[1]
+ default:
+ return errors.Errorf("too many arguments. Usage TARBALL [REFERENCE]")
+ }
+
+ changes := v1.ImageConfig{}
+ if c.IsSet("change") {
+ changes, err = getImageConfig(c.StringSlice("change"))
+ if err != nil {
+ return errors.Wrapf(err, "error adding config changes to image %q", source)
+ }
+ }
+
+ history := []v1.History{
+ {Comment: c.String("message")},
+ }
+
+ config := v1.Image{
+ Config: changes,
+ History: history,
+ }
+
+ opts.ImageConfig = config
+
+ // if source is a url, download it and save to a temp file
+ u, err := url.ParseRequestURI(source)
+ if err == nil && u.Scheme != "" {
+ file, err := downloadFromURL(source)
+ if err != nil {
+ return err
+ }
+ defer os.Remove(file)
+ source = file
+ }
+
+ return runtime.ImportImage(source, opts)
+}
+
+// donwloadFromURL downloads an image in the format "https:/example.com/myimage.tar"
+// and tempoarily saves in it /var/tmp/importxyz, which is deleted after the image is imported
+func downloadFromURL(source string) (string, error) {
+ fmt.Printf("Downloading from %q\n", source)
+
+ outFile, err := ioutil.TempFile("/var/tmp", "import")
+ if err != nil {
+ return "", errors.Wrap(err, "error creating file")
+ }
+ defer outFile.Close()
+
+ response, err := http.Get(source)
+ if err != nil {
+ return "", errors.Wrapf(err, "error downloading %q", source)
+ }
+ defer response.Body.Close()
+
+ _, err = io.Copy(outFile, response.Body)
+ if err != nil {
+ return "", errors.Wrapf(err, "error saving %q to %q", source, outFile)
+ }
+
+ return outFile.Name(), nil
+}
+
+// getImageConfig converts the --change flag values in the format "CMD=/bin/bash USER=example"
+// to a type v1.ImageConfig
+func getImageConfig(changes []string) (v1.ImageConfig, error) {
+ // USER=value | EXPOSE=value | ENV=value | ENTRYPOINT=value |
+ // CMD=value | VOLUME=value | WORKDIR=value | LABEL=key=value | STOPSIGNAL=value
+
+ var (
+ user string
+ env []string
+ entrypoint []string
+ cmd []string
+ workingDir string
+ stopSignal string
+ )
+
+ exposedPorts := make(map[string]struct{})
+ volumes := make(map[string]struct{})
+ labels := make(map[string]string)
+
+ for _, ch := range changes {
+ pair := strings.Split(ch, "=")
+ if len(pair) == 1 {
+ return v1.ImageConfig{}, errors.Errorf("no value given for instruction %q", ch)
+ }
+ switch pair[0] {
+ case "USER":
+ user = pair[1]
+ case "EXPOSE":
+ var st struct{}
+ exposedPorts[pair[1]] = st
+ case "ENV":
+ env = append(env, pair[1])
+ case "ENTRYPOINT":
+ entrypoint = append(entrypoint, pair[1])
+ case "CMD":
+ cmd = append(cmd, pair[1])
+ case "VOLUME":
+ var st struct{}
+ volumes[pair[1]] = st
+ case "WORKDIR":
+ workingDir = pair[1]
+ case "LABEL":
+ if len(pair) == 3 {
+ labels[pair[1]] = pair[2]
+ } else {
+ labels[pair[1]] = ""
+ }
+ case "STOPSIGNAL":
+ stopSignal = pair[1]
+ }
+ }
+
+ return v1.ImageConfig{
+ User: user,
+ ExposedPorts: exposedPorts,
+ Env: env,
+ Entrypoint: entrypoint,
+ Cmd: cmd,
+ Volumes: volumes,
+ WorkingDir: workingDir,
+ Labels: labels,
+ StopSignal: stopSignal,
+ }, nil
+}
diff --git a/cmd/podman/info.go b/cmd/podman/info.go
new file mode 100644
index 000000000..89f32a258
--- /dev/null
+++ b/cmd/podman/info.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+ "runtime"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ infoDescription = "display system information"
+ infoCommand = cli.Command{
+ Name: "info",
+ Usage: infoDescription,
+ Description: `Information display here pertain to the host, current storage stats, and build of podman. Useful for the user and when reporting issues.`,
+ Flags: infoFlags,
+ Action: infoCmd,
+ ArgsUsage: "",
+ }
+ infoFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "debug, D",
+ Usage: "display additional debug information",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Change the output format to JSON or a Go template",
+ },
+ }
+)
+
+func infoCmd(c *cli.Context) error {
+ if err := validateFlags(c, infoFlags); err != nil {
+ return err
+ }
+ info := map[string]interface{}{}
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ infoArr, err := runtime.Info()
+ if err != nil {
+ return errors.Wrapf(err, "error getting info")
+ }
+
+ if c.Bool("debug") {
+ debugInfo := debugInfo(c)
+ infoArr = append(infoArr, libpod.InfoData{Type: "debug", Data: debugInfo})
+ }
+
+ for _, currInfo := range infoArr {
+ info[currInfo.Type] = currInfo.Data
+ }
+
+ var out formats.Writer
+ infoOutputFormat := c.String("format")
+ switch infoOutputFormat {
+ case formats.JSONString:
+ out = formats.JSONStruct{Output: info}
+ case "":
+ out = formats.YAMLStruct{Output: info}
+ default:
+ out = formats.StdoutTemplate{Output: info, Template: infoOutputFormat}
+ }
+
+ formats.Writer(out).Out()
+
+ return nil
+}
+
+// top-level "debug" info
+func debugInfo(c *cli.Context) map[string]interface{} {
+ info := map[string]interface{}{}
+ info["compiler"] = runtime.Compiler
+ info["go version"] = runtime.Version()
+ info["podman version"] = c.App.Version
+ info["git commit"] = gitCommit
+ return info
+}
diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go
new file mode 100644
index 000000000..7fd039b75
--- /dev/null
+++ b/cmd/podman/inspect.go
@@ -0,0 +1,364 @@
+package main
+
+import (
+ "encoding/json"
+
+ specs "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+const (
+ inspectTypeContainer = "container"
+ inspectTypeImage = "image"
+ inspectAll = "all"
+)
+
+var (
+ inspectFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "type, t",
+ Value: inspectAll,
+ Usage: "Return JSON for specified type, (e.g image, container or task)",
+ },
+ cli.StringFlag{
+ Name: "format, f",
+ Usage: "Change the output format to a Go template",
+ },
+ cli.BoolFlag{
+ Name: "size",
+ Usage: "Display total file size if the type is container",
+ },
+ }
+ inspectDescription = "This displays the low-level information on containers and images identified by name or ID. By default, this will render all results in a JSON array. If the container and image have the same name, this will return container JSON for unspecified type."
+ inspectCommand = cli.Command{
+ Name: "inspect",
+ Usage: "Displays the configuration of a container or image",
+ Description: inspectDescription,
+ Flags: inspectFlags,
+ Action: inspectCmd,
+ ArgsUsage: "CONTAINER-OR-IMAGE",
+ }
+)
+
+func inspectCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) == 0 {
+ return errors.Errorf("container or image name must be specified: podman inspect [options [...]] name")
+ }
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments specified")
+ }
+ if err := validateFlags(c, inspectFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ if c.String("type") != inspectTypeContainer && c.String("type") != inspectTypeImage && c.String("type") != inspectAll {
+ return errors.Errorf("the only recognized types are %q, %q, and %q", inspectTypeContainer, inspectTypeImage, inspectAll)
+ }
+
+ name := args[0]
+
+ outputFormat := c.String("format")
+ var data interface{}
+ switch c.String("type") {
+ case inspectTypeContainer:
+ ctr, err := runtime.LookupContainer(name)
+ if err != nil {
+ return errors.Wrapf(err, "error looking up container %q", name)
+ }
+ libpodInspectData, err := ctr.Inspect(c.Bool("size"))
+ if err != nil {
+ return errors.Wrapf(err, "error getting libpod container inspect data %q", ctr.ID)
+ }
+ data, err = getCtrInspectInfo(ctr, libpodInspectData)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing container data %q", ctr.ID())
+ }
+ case inspectTypeImage:
+ image, err := runtime.GetImage(name)
+ if err != nil {
+ return errors.Wrapf(err, "error getting image %q", name)
+ }
+ data, err = runtime.GetImageInspectInfo(*image)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing image data %q", image.ID)
+ }
+ case inspectAll:
+ ctr, err := runtime.LookupContainer(name)
+ if err != nil {
+ image, err := runtime.GetImage(name)
+ if err != nil {
+ return errors.Wrapf(err, "error getting image %q", name)
+ }
+ data, err = runtime.GetImageInspectInfo(*image)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing image data %q", image.ID)
+ }
+ } else {
+ libpodInspectData, err := ctr.Inspect(c.Bool("size"))
+ if err != nil {
+ return errors.Wrapf(err, "error getting libpod container inspect data %q", ctr.ID)
+ }
+ data, err = getCtrInspectInfo(ctr, libpodInspectData)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing container data %q", ctr.ID)
+ }
+ }
+ }
+
+ var out formats.Writer
+ if outputFormat != "" && outputFormat != formats.JSONString {
+ //template
+ out = formats.StdoutTemplate{Output: data, Template: outputFormat}
+ } else {
+ // default is json output
+ out = formats.JSONStruct{Output: data}
+ }
+
+ formats.Writer(out).Out()
+ return nil
+}
+
+func getCtrInspectInfo(ctr *libpod.Container, ctrInspectData *libpod.ContainerInspectData) (*ContainerData, error) {
+ config := ctr.Config()
+ spec := config.Spec
+
+ cpus, mems, period, quota, realtimePeriod, realtimeRuntime, shares := getCPUInfo(spec)
+ blkioWeight, blkioWeightDevice, blkioReadBps, blkioWriteBps, blkioReadIOPS, blkioeWriteIOPS := getBLKIOInfo(spec)
+ memKernel, memReservation, memSwap, memSwappiness, memDisableOOMKiller := getMemoryInfo(spec)
+ pidsLimit := getPidsInfo(spec)
+ cgroup := getCgroup(spec)
+
+ var createArtifact createConfig
+ artifact, err := ctr.GetArtifact("create-config")
+ if err == nil {
+ if err := json.Unmarshal(artifact, &createArtifact); err != nil {
+ return nil, err
+ }
+ } else {
+ logrus.Errorf("couldn't get some inspect information, error getting artifact %q: %v", ctr.ID(), err)
+ }
+
+ data := &ContainerData{
+ CtrInspectData: ctrInspectData,
+ HostConfig: &HostConfig{
+ ConsoleSize: spec.Process.ConsoleSize,
+ OomScoreAdj: spec.Process.OOMScoreAdj,
+ CPUShares: shares,
+ BlkioWeight: blkioWeight,
+ BlkioWeightDevice: blkioWeightDevice,
+ BlkioDeviceReadBps: blkioReadBps,
+ BlkioDeviceWriteBps: blkioWriteBps,
+ BlkioDeviceReadIOps: blkioReadIOPS,
+ BlkioDeviceWriteIOps: blkioeWriteIOPS,
+ CPUPeriod: period,
+ CPUQuota: quota,
+ CPURealtimePeriod: realtimePeriod,
+ CPURealtimeRuntime: realtimeRuntime,
+ CPUSetCPUs: cpus,
+ CPUSetMems: mems,
+ Devices: spec.Linux.Devices,
+ KernelMemory: memKernel,
+ MemoryReservation: memReservation,
+ MemorySwap: memSwap,
+ MemorySwappiness: memSwappiness,
+ OomKillDisable: memDisableOOMKiller,
+ PidsLimit: pidsLimit,
+ Privileged: spec.Process.NoNewPrivileges,
+ ReadonlyRootfs: spec.Root.Readonly,
+ Runtime: ctr.RuntimeName(),
+ NetworkMode: string(createArtifact.NetMode),
+ IpcMode: string(createArtifact.IpcMode),
+ Cgroup: cgroup,
+ UTSMode: string(createArtifact.UtsMode),
+ UsernsMode: createArtifact.NsUser,
+ GroupAdd: spec.Process.User.AdditionalGids,
+ ContainerIDFile: createArtifact.CidFile,
+ AutoRemove: createArtifact.Rm,
+ CapAdd: createArtifact.CapAdd,
+ CapDrop: createArtifact.CapDrop,
+ DNS: createArtifact.DNSServers,
+ DNSOptions: createArtifact.DNSOpt,
+ DNSSearch: createArtifact.DNSSearch,
+ PidMode: string(createArtifact.PidMode),
+ CgroupParent: createArtifact.CgroupParent,
+ ShmSize: createArtifact.Resources.ShmSize,
+ Memory: createArtifact.Resources.Memory,
+ Ulimits: createArtifact.Resources.Ulimit,
+ SecurityOpt: createArtifact.SecurityOpts,
+ },
+ Config: &CtrConfig{
+ Hostname: spec.Hostname,
+ User: spec.Process.User,
+ Env: spec.Process.Env,
+ Image: config.RootfsImageName,
+ WorkingDir: spec.Process.Cwd,
+ Labels: config.Labels,
+ Annotations: spec.Annotations,
+ Tty: spec.Process.Terminal,
+ OpenStdin: config.Stdin,
+ StopSignal: config.StopSignal,
+ Cmd: config.Spec.Process.Args,
+ Entrypoint: createArtifact.Entrypoint,
+ },
+ }
+ return data, nil
+}
+
+func getCPUInfo(spec *specs.Spec) (string, string, *uint64, *int64, *uint64, *int64, *uint64) {
+ if spec.Linux.Resources == nil {
+ return "", "", nil, nil, nil, nil, nil
+ }
+ cpu := spec.Linux.Resources.CPU
+ if cpu == nil {
+ return "", "", nil, nil, nil, nil, nil
+ }
+ return cpu.Cpus, cpu.Mems, cpu.Period, cpu.Quota, cpu.RealtimePeriod, cpu.RealtimeRuntime, cpu.Shares
+}
+
+func getBLKIOInfo(spec *specs.Spec) (*uint16, []specs.LinuxWeightDevice, []specs.LinuxThrottleDevice, []specs.LinuxThrottleDevice, []specs.LinuxThrottleDevice, []specs.LinuxThrottleDevice) {
+ if spec.Linux.Resources == nil {
+ return nil, nil, nil, nil, nil, nil
+ }
+ blkio := spec.Linux.Resources.BlockIO
+ if blkio == nil {
+ return nil, nil, nil, nil, nil, nil
+ }
+ return blkio.Weight, blkio.WeightDevice, blkio.ThrottleReadBpsDevice, blkio.ThrottleWriteBpsDevice, blkio.ThrottleReadIOPSDevice, blkio.ThrottleWriteIOPSDevice
+}
+
+func getMemoryInfo(spec *specs.Spec) (*int64, *int64, *int64, *uint64, *bool) {
+ if spec.Linux.Resources == nil {
+ return nil, nil, nil, nil, nil
+ }
+ memory := spec.Linux.Resources.Memory
+ if memory == nil {
+ return nil, nil, nil, nil, nil
+ }
+ return memory.Kernel, memory.Reservation, memory.Swap, memory.Swappiness, memory.DisableOOMKiller
+}
+
+func getPidsInfo(spec *specs.Spec) *int64 {
+ if spec.Linux.Resources == nil {
+ return nil
+ }
+ pids := spec.Linux.Resources.Pids
+ if pids == nil {
+ return nil
+ }
+ return &pids.Limit
+}
+
+func getCgroup(spec *specs.Spec) string {
+ cgroup := "host"
+ for _, ns := range spec.Linux.Namespaces {
+ if ns.Type == specs.CgroupNamespace && ns.Path != "" {
+ cgroup = "container"
+ }
+ }
+ return cgroup
+}
+
+// ContainerData holds the podman inspect data for a container
+type ContainerData struct {
+ CtrInspectData *libpod.ContainerInspectData `json:"CtrInspectData"`
+ HostConfig *HostConfig `json:"HostConfig"`
+ Config *CtrConfig `json:"Config"`
+}
+
+// LogConfig holds the log information for a container
+type LogConfig struct {
+ Type string `json:"Type"` // TODO
+ Config map[string]string `json:"Config"` //idk type, TODO
+}
+
+// HostConfig represents the host configuration for the container
+type HostConfig struct {
+ ContainerIDFile string `json:"ContainerIDFile"`
+ LogConfig *LogConfig `json:"LogConfig"` //TODO
+ NetworkMode string `json:"NetworkMode"`
+ PortBindings map[string]struct{} `json:"PortBindings"` //TODO
+ AutoRemove bool `json:"AutoRemove"`
+ CapAdd []string `json:"CapAdd"`
+ CapDrop []string `json:"CapDrop"`
+ DNS []string `json:"DNS"`
+ DNSOptions []string `json:"DNSOptions"`
+ DNSSearch []string `json:"DNSSearch"`
+ ExtraHosts []string `json:"ExtraHosts"`
+ GroupAdd []uint32 `json:"GroupAdd"`
+ IpcMode string `json:"IpcMode"`
+ Cgroup string `json:"Cgroup"`
+ OomScoreAdj *int `json:"OomScoreAdj"`
+ PidMode string `json:"PidMode"`
+ Privileged bool `json:"Privileged"`
+ PublishAllPorts bool `json:"PublishAllPorts"` //TODO
+ ReadonlyRootfs bool `json:"ReadonlyRootfs"`
+ SecurityOpt []string `json:"SecurityOpt"`
+ UTSMode string `json:"UTSMode"`
+ UsernsMode string `json:"UsernsMode"`
+ ShmSize string `json:"ShmSize"`
+ Runtime string `json:"Runtime"`
+ ConsoleSize *specs.Box `json:"ConsoleSize"`
+ Isolation string `json:"Isolation"` //TODO
+ CPUShares *uint64 `json:"CPUSShares"`
+ Memory int64 `json:"Memory"`
+ NanoCPUs int `json:"NanoCPUs"` //check type, TODO
+ CgroupParent string `json:"CgroupParent"`
+ BlkioWeight *uint16 `json:"BlkioWeight"`
+ BlkioWeightDevice []specs.LinuxWeightDevice `json:"BlkioWeightDevice"`
+ BlkioDeviceReadBps []specs.LinuxThrottleDevice `json:"BlkioDeviceReadBps"`
+ BlkioDeviceWriteBps []specs.LinuxThrottleDevice `json:"BlkioDeviceWriteBps"`
+ BlkioDeviceReadIOps []specs.LinuxThrottleDevice `json:"BlkioDeviceReadIOps"`
+ BlkioDeviceWriteIOps []specs.LinuxThrottleDevice `json:"BlkioDeviceWriteIOps"`
+ CPUPeriod *uint64 `json:"CPUPeriod"`
+ CPUQuota *int64 `json:"CPUQuota"`
+ CPURealtimePeriod *uint64 `json:"CPURealtimePeriod"`
+ CPURealtimeRuntime *int64 `json:"CPURealtimeRuntime"`
+ CPUSetCPUs string `json:"CPUSetCPUs"`
+ CPUSetMems string `json:"CPUSetMems"`
+ Devices []specs.LinuxDevice `json:"Devices"`
+ DiskQuota int `json:"DiskQuota"` //check type, TODO
+ KernelMemory *int64 `json:"KernelMemory"`
+ MemoryReservation *int64 `json:"MemoryReservation"`
+ MemorySwap *int64 `json:"MemorySwap"`
+ MemorySwappiness *uint64 `json:"MemorySwappiness"`
+ OomKillDisable *bool `json:"OomKillDisable"`
+ PidsLimit *int64 `json:"PidsLimit"`
+ Ulimits []string `json:"Ulimits"`
+ CPUCount int `json:"CPUCount"` //check type, TODO
+ CPUPercent int `json:"CPUPercent"` //check type, TODO
+ IOMaximumIOps int `json:"IOMaximumIOps"` //check type, TODO
+ IOMaximumBandwidth int `json:"IOMaximumBandwidth"` //check type, TODO
+}
+
+// CtrConfig holds information about the container configuration
+type CtrConfig struct {
+ Hostname string `json:"Hostname"`
+ DomainName string `json:"Domainname"` //TODO
+ User specs.User `json:"User"`
+ AttachStdin bool `json:"AttachStdin"` //TODO
+ AttachStdout bool `json:"AttachStdout"` //TODO
+ AttachStderr bool `json:"AttachStderr"` //TODO
+ Tty bool `json:"Tty"`
+ OpenStdin bool `json:"OpenStdin"`
+ StdinOnce bool `json:"StdinOnce"` //TODO
+ Env []string `json:"Env"`
+ Cmd []string `json:"Cmd"`
+ Image string `json:"Image"`
+ Volumes map[string]struct{} `json:"Volumes"`
+ WorkingDir string `json:"WorkingDir"`
+ Entrypoint string `json:"Entrypoint"`
+ Labels map[string]string `json:"Labels"`
+ Annotations map[string]string `json:"Annotations"`
+ StopSignal uint `json:"StopSignal"`
+}
diff --git a/cmd/podman/kill.go b/cmd/podman/kill.go
new file mode 100644
index 000000000..776c7ef20
--- /dev/null
+++ b/cmd/podman/kill.go
@@ -0,0 +1,80 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "syscall"
+
+ "github.com/docker/docker/pkg/signal"
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+var (
+ killFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "signal, s",
+ Usage: "Signal to send to the container",
+ Value: "KILL",
+ },
+ }
+ killDescription = "The main process inside each container specified will be sent SIGKILL, or any signal specified with option --signal."
+ killCommand = cli.Command{
+ Name: "kill",
+ Usage: "Kill one or more running containers with a specific signal",
+ Description: killDescription,
+ Flags: killFlags,
+ Action: killCmd,
+ ArgsUsage: "[CONTAINER_NAME_OR_ID]",
+ }
+)
+
+// killCmd kills one or more containers with a signal
+func killCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) == 0 {
+ return errors.Errorf("specify one or more containers to kill")
+ }
+ if err := validateFlags(c, killFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ var killSignal uint = uint(syscall.SIGTERM)
+ if c.String("signal") != "" {
+ // Check if the signalString provided by the user is valid
+ // Invalid signals will return err
+ sysSignal, err := signal.ParseSignal(c.String("signal"))
+ if err != nil {
+ return err
+ }
+ killSignal = uint(sysSignal)
+ }
+
+ var lastError error
+ for _, container := range c.Args() {
+ ctr, err := runtime.LookupContainer(container)
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "unable to find container %v", container)
+ continue
+ }
+
+ if err := ctr.Kill(killSignal); err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "unable to find container %v", container)
+ } else {
+ fmt.Println(ctr.ID())
+ }
+ }
+ return lastError
+}
diff --git a/cmd/podman/load.go b/cmd/podman/load.go
new file mode 100644
index 000000000..2f3d9c56d
--- /dev/null
+++ b/cmd/podman/load.go
@@ -0,0 +1,123 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ loadFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "input, i",
+ Usage: "Read from archive file, default is STDIN",
+ Value: "/dev/stdin",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "Suppress the output",
+ },
+ cli.StringFlag{
+ Name: "signature-policy",
+ Usage: "`pathname` of signature policy file (not usually used)",
+ },
+ }
+ loadDescription = "Loads the image from docker-archive stored on the local machine."
+ loadCommand = cli.Command{
+ Name: "load",
+ Usage: "load an image from docker archive",
+ Description: loadDescription,
+ Flags: loadFlags,
+ Action: loadCmd,
+ ArgsUsage: "",
+ }
+)
+
+// loadCmd gets the image/file to be loaded from the command line
+// and calls loadImage to load the image to containers-storage
+func loadCmd(c *cli.Context) error {
+
+ args := c.Args()
+ var image string
+ if len(args) == 1 {
+ image = args[0]
+ }
+ if len(args) > 1 {
+ return errors.New("too many arguments. Requires exactly 1")
+ }
+ if err := validateFlags(c, loadFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ input := c.String("input")
+
+ if input == "/dev/stdin" {
+ fi, err := os.Stdin.Stat()
+ if err != nil {
+ return err
+ }
+ // checking if loading from pipe
+ if !fi.Mode().IsRegular() {
+ outFile, err := ioutil.TempFile("/var/tmp", "podman")
+ if err != nil {
+ return errors.Errorf("error creating file %v", err)
+ }
+ defer outFile.Close()
+ defer os.Remove(outFile.Name())
+
+ inFile, err := os.OpenFile(input, 0, 0666)
+ if err != nil {
+ return errors.Errorf("error reading file %v", err)
+ }
+ defer inFile.Close()
+
+ _, err = io.Copy(outFile, inFile)
+ if err != nil {
+ return errors.Errorf("error copying file %v", err)
+ }
+
+ input = outFile.Name()
+ }
+ }
+
+ var writer io.Writer
+ if !c.Bool("quiet") {
+ writer = os.Stdout
+ }
+
+ options := libpod.CopyOptions{
+ SignaturePolicyPath: c.String("signature-policy"),
+ Writer: writer,
+ }
+
+ src := libpod.DockerArchive + ":" + input
+ imgName, err := runtime.PullImage(src, options)
+ if err != nil {
+ // generate full src name with specified image:tag
+ fullSrc := libpod.OCIArchive + ":" + input
+ if image != "" {
+ fullSrc = fullSrc + ":" + image
+ }
+ imgName, err = runtime.PullImage(fullSrc, options)
+ if err != nil {
+ src = libpod.DirTransport + ":" + input
+ imgName, err = runtime.PullImage(src, options)
+ if err != nil {
+ return errors.Wrapf(err, "error pulling %q", src)
+ }
+ }
+ }
+ fmt.Println("Loaded image: ", imgName)
+ return nil
+}
diff --git a/cmd/podman/login.go b/cmd/podman/login.go
new file mode 100644
index 000000000..8984d069c
--- /dev/null
+++ b/cmd/podman/login.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/containers/image/docker"
+ "github.com/containers/image/pkg/docker/config"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod/common"
+ "github.com/urfave/cli"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+var (
+ loginFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "password, p",
+ Usage: "Password for registry",
+ },
+ cli.StringFlag{
+ Name: "username, u",
+ Usage: "Username for registry",
+ },
+ cli.StringFlag{
+ Name: "authfile",
+ Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
+ },
+ }
+ loginDescription = "Login to a container registry on a specified server."
+ loginCommand = cli.Command{
+ Name: "login",
+ Usage: "login to a container registry",
+ Description: loginDescription,
+ Flags: loginFlags,
+ Action: loginCmd,
+ ArgsUsage: "REGISTRY",
+ }
+)
+
+// loginCmd uses the authentication package to store a user's authenticated credentials
+// in an auth.json file for future use
+func loginCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments, login takes only 1 argument")
+ }
+ if len(args) == 0 {
+ return errors.Errorf("registry must be given")
+ }
+ var server string
+ if len(args) == 1 {
+ server = args[0]
+ }
+
+ sc := common.GetSystemContext("", c.String("authfile"), false)
+
+ // username of user logged in to server (if one exists)
+ userFromAuthFile := config.GetUserLoggedIn(sc, server)
+ username, password, err := getUserAndPass(c.String("username"), c.String("password"), userFromAuthFile)
+ if err != nil {
+ return errors.Wrapf(err, "error getting username and password")
+ }
+
+ if err = docker.CheckAuth(context.TODO(), sc, username, password, server); err == nil {
+ if err := config.SetAuthentication(sc, server, username, password); err != nil {
+ return err
+ }
+ }
+ switch err {
+ case nil:
+ fmt.Println("Login Succeeded!")
+ return nil
+ case docker.ErrUnauthorizedForCredentials:
+ return errors.Errorf("error logging into %q: invalid username/password\n", server)
+ default:
+ return errors.Wrapf(err, "error authenticating creds for %q", server)
+ }
+}
+
+// getUserAndPass gets the username and password from STDIN if not given
+// using the -u and -p flags
+func getUserAndPass(username, password, userFromAuthFile string) (string, string, error) {
+ var err error
+ reader := bufio.NewReader(os.Stdin)
+ if username == "" {
+ if userFromAuthFile != "" {
+ fmt.Printf("Username (%s): ", userFromAuthFile)
+ } else {
+ fmt.Print("Username: ")
+ }
+ username, err = reader.ReadString('\n')
+ if err != nil {
+ return "", "", errors.Wrapf(err, "error reading username")
+ }
+ }
+ if password == "" {
+ fmt.Print("Password: ")
+ pass, err := terminal.ReadPassword(0)
+ if err != nil {
+ return "", "", errors.Wrapf(err, "error reading password")
+ }
+ password = string(pass)
+ fmt.Println()
+ }
+ return strings.TrimSpace(username), password, err
+}
diff --git a/cmd/podman/logout.go b/cmd/podman/logout.go
new file mode 100644
index 000000000..cae8ddfb2
--- /dev/null
+++ b/cmd/podman/logout.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/containers/image/pkg/docker/config"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod/common"
+ "github.com/urfave/cli"
+)
+
+var (
+ logoutFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "authfile",
+ Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
+ },
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "Remove the cached credentials for all registries in the auth file",
+ },
+ }
+ logoutDescription = "Remove the cached username and password for the registry."
+ logoutCommand = cli.Command{
+ Name: "logout",
+ Usage: "logout of a container registry",
+ Description: logoutDescription,
+ Flags: logoutFlags,
+ Action: logoutCmd,
+ ArgsUsage: "REGISTRY",
+ }
+)
+
+// logoutCmd uses the authentication package to remove the authenticated of a registry
+// stored in the auth.json file
+func logoutCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments, logout takes only 1 argument")
+ }
+ if len(args) == 0 {
+ return errors.Errorf("registry must be given")
+ }
+ var server string
+ if len(args) == 1 {
+ server = args[0]
+ }
+
+ sc := common.GetSystemContext("", c.String("authfile"), false)
+
+ if c.Bool("all") {
+ if err := config.RemoveAllAuthentication(sc); err != nil {
+ return err
+ }
+ fmt.Println("Remove login credentials for all registries")
+ return nil
+ }
+
+ err := config.RemoveAuthentication(sc, server)
+ switch err {
+ case nil:
+ fmt.Printf("Remove login credentials for %s\n", server)
+ return nil
+ case config.ErrNotLoggedIn:
+ return errors.Errorf("Not logged into %s\n", server)
+ default:
+ return errors.Wrapf(err, "error logging out of %q", server)
+ }
+}
diff --git a/cmd/podman/logs.go b/cmd/podman/logs.go
new file mode 100644
index 000000000..8745d5d7f
--- /dev/null
+++ b/cmd/podman/logs.go
@@ -0,0 +1,153 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/hpcloud/tail"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+type logOptions struct {
+ details bool
+ follow bool
+ sinceTime time.Time
+ tail uint64
+}
+
+var (
+ logsFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "details",
+ Usage: "Show extra details provided to the logs",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "follow, f",
+ Usage: "Follow log output. The default is false",
+ },
+ cli.StringFlag{
+ Name: "since",
+ Usage: "Show logs since TIMESTAMP",
+ },
+ cli.Uint64Flag{
+ Name: "tail",
+ Usage: "Output the specified number of LINES at the end of the logs. Defaults to 0, which prints all lines",
+ },
+ }
+ logsDescription = "The podman logs command batch-retrieves whatever logs are present for a container at the time of execution. This does not guarantee execution" +
+ "order when combined with podman run (i.e. your run may not have generated any logs at the time you execute podman logs"
+ logsCommand = cli.Command{
+ Name: "logs",
+ Usage: "Fetch the logs of a container",
+ Description: logsDescription,
+ Flags: logsFlags,
+ Action: logsCmd,
+ ArgsUsage: "CONTAINER",
+ }
+)
+
+func logsCmd(c *cli.Context) error {
+ if err := validateFlags(c, logsFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) != 1 {
+ return errors.Errorf("'podman logs' requires exactly one container name/ID")
+ }
+
+ sinceTime := time.Time{}
+ if c.IsSet("since") {
+ // parse time, error out if something is wrong
+ since, err := time.Parse("2006-01-02T15:04:05.999999999-07:00", c.String("since"))
+ if err != nil {
+ return errors.Wrapf(err, "could not parse time: %q", c.String("since"))
+ }
+ sinceTime = since
+ }
+
+ opts := logOptions{
+ details: c.Bool("details"),
+ follow: c.Bool("follow"),
+ sinceTime: sinceTime,
+ tail: c.Uint64("tail"),
+ }
+
+ ctr, err := runtime.LookupContainer(args[0])
+ if err != nil {
+ return err
+ }
+
+ logs := make(chan string)
+ go func() {
+ err = getLogs(ctr, logs, opts)
+ }()
+ printLogs(logs)
+ return err
+}
+
+// getLogs returns the logs of a container from the log file
+// log file is created when the container is started/ran
+func getLogs(container *libpod.Container, logChan chan string, opts logOptions) error {
+ defer close(logChan)
+
+ seekInfo := &tail.SeekInfo{Offset: 0, Whence: 0}
+ if opts.tail > 0 {
+ // seek to correct position in log files
+ seekInfo.Offset = int64(opts.tail)
+ seekInfo.Whence = 2
+ }
+
+ t, err := tail.TailFile(container.LogPath(), tail.Config{Follow: opts.follow, ReOpen: false, Location: seekInfo})
+ for line := range t.Lines {
+ if since, err := logSinceTime(opts.sinceTime, line.Text); err != nil || !since {
+ continue
+ }
+ logMessage := line.Text[secondSpaceIndex(line.Text):]
+ logChan <- logMessage
+ }
+ return err
+}
+
+func printLogs(logs chan string) {
+ for line := range logs {
+ fmt.Println(line)
+ }
+}
+
+// returns true if the time stamps of the logs are equal to or after the
+// timestamp comparing to
+func logSinceTime(sinceTime time.Time, logStr string) (bool, error) {
+ timestamp := strings.Split(logStr, " ")[0]
+ logTime, err := time.Parse("2006-01-02T15:04:05.999999999-07:00", timestamp)
+ if err != nil {
+ return false, err
+ }
+ return logTime.After(sinceTime) || logTime.Equal(sinceTime), nil
+}
+
+// secondSpaceIndex returns the index of the second space in a string
+// In a line of the logs, the first two tokens are a timestamp and stdout/stderr,
+// followed by the message itself. This allows us to get the index of the message
+// and avoid sending the other information back to the caller of GetLogs()
+func secondSpaceIndex(line string) int {
+ index := strings.Index(line, " ")
+ if index == -1 {
+ return 0
+ }
+ index = strings.Index(line[index:], " ")
+ if index == -1 {
+ return 0
+ }
+ return index
+}
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
new file mode 100644
index 000000000..cc6d26992
--- /dev/null
+++ b/cmd/podman/main.go
@@ -0,0 +1,161 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "runtime/pprof"
+
+ "github.com/containers/storage/pkg/reexec"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+// This is populated by the Makefile from the VERSION file
+// in the repository
+var podmanVersion = ""
+
+func main() {
+ debug := false
+ cpuProfile := false
+
+ if reexec.Init() {
+ return
+ }
+
+ app := cli.NewApp()
+ app.Name = "podman"
+ app.Usage = "manage pods and images"
+
+ var v string
+ if podmanVersion != "" {
+ v = podmanVersion
+ }
+ app.Version = v
+
+ app.Commands = []cli.Command{
+ attachCommand,
+ createCommand,
+ diffCommand,
+ execCommand,
+ exportCommand,
+ historyCommand,
+ imagesCommand,
+ importCommand,
+ infoCommand,
+ inspectCommand,
+ killCommand,
+ loadCommand,
+ loginCommand,
+ logoutCommand,
+ logsCommand,
+ mountCommand,
+ pauseCommand,
+ psCommand,
+ pullCommand,
+ pushCommand,
+ rmCommand,
+ rmiCommand,
+ runCommand,
+ saveCommand,
+ startCommand,
+ statsCommand,
+ stopCommand,
+ tagCommand,
+ topCommand,
+ umountCommand,
+ unpauseCommand,
+ versionCommand,
+ waitCommand,
+ }
+ app.Before = func(c *cli.Context) error {
+ logLevel := c.GlobalString("log-level")
+ if logLevel != "" {
+ level, err := logrus.ParseLevel(logLevel)
+ if err != nil {
+ return err
+ }
+
+ logrus.SetLevel(level)
+ }
+
+ if logLevel == "debug" {
+ debug = true
+
+ }
+ if c.GlobalIsSet("cpu-profile") {
+ f, err := os.Create(c.GlobalString("cpu-profile"))
+ if err != nil {
+ return errors.Wrapf(err, "unable to create cpu profiling file %s",
+ c.GlobalString("cpu-profile"))
+ }
+ cpuProfile = true
+ pprof.StartCPUProfile(f)
+ }
+ return nil
+ }
+ app.After = func(*cli.Context) error {
+ // called by Run() when the command handler succeeds
+ shutdownStores()
+ if cpuProfile {
+ pprof.StopCPUProfile()
+ }
+ return nil
+ }
+ cli.OsExiter = func(code int) {
+ // called by Run() when the command fails, bypassing After()
+ shutdownStores()
+ os.Exit(code)
+ }
+ app.Flags = []cli.Flag{
+ cli.StringFlag{
+ Name: "cni-config-dir",
+ Usage: "path of the configuration directory for CNI networks",
+ },
+ cli.StringFlag{
+ Name: "config, c",
+ Usage: "path of a config file detailing container server configuration options",
+ },
+ cli.StringFlag{
+ Name: "conmon",
+ Usage: "path of the conmon binary",
+ },
+ cli.StringFlag{
+ Name: "cpu-profile",
+ Usage: "path for the cpu profiling results",
+ },
+ cli.StringFlag{
+ Name: "log-level",
+ Usage: "log messages above specified level: debug, info, warn, error (default), fatal or panic",
+ Value: "error",
+ },
+ cli.StringFlag{
+ Name: "root",
+ Usage: "path to the root directory in which data, including images, is stored",
+ },
+ cli.StringFlag{
+ Name: "runroot",
+ Usage: "path to the 'run directory' where all state information is stored",
+ },
+ cli.StringFlag{
+ Name: "runtime",
+ Usage: "path to the OCI-compatible binary used to run containers, default is /usr/bin/runc",
+ },
+ cli.StringFlag{
+ Name: "storage-driver, s",
+ Usage: "select which storage driver is used to manage storage of images and containers (default is overlay)",
+ },
+ cli.StringSliceFlag{
+ Name: "storage-opt",
+ Usage: "used to pass an option to the storage driver",
+ },
+ }
+ if err := app.Run(os.Args); err != nil {
+ if debug {
+ logrus.Errorf(err.Error())
+ } else {
+ fmt.Fprintln(os.Stderr, err.Error())
+ }
+ cli.OsExiter(1)
+ }
+}
diff --git a/cmd/podman/mount.go b/cmd/podman/mount.go
new file mode 100644
index 000000000..9db27fcda
--- /dev/null
+++ b/cmd/podman/mount.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+ js "encoding/json"
+ "fmt"
+
+ "github.com/pkg/errors"
+ of "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/urfave/cli"
+)
+
+var (
+ mountDescription = `
+ podman mount
+ Lists all mounted containers mount points
+
+ podman mount CONTAINER-NAME-OR-ID
+ Mounts the specified container and outputs the mountpoint
+`
+
+ mountFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "notruncate",
+ Usage: "do not truncate output",
+ },
+ cli.StringFlag{
+ Name: "label",
+ Usage: "SELinux label for the mount point",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Change the output format to Go template",
+ },
+ }
+ mountCommand = cli.Command{
+ Name: "mount",
+ Usage: "Mount a working container's root filesystem",
+ Description: mountDescription,
+ Action: mountCmd,
+ ArgsUsage: "[CONTAINER-NAME-OR-ID]",
+ Flags: mountFlags,
+ }
+)
+
+// jsonMountPoint stores info about each container
+type jsonMountPoint struct {
+ ID string `json:"id"`
+ Names []string `json:"names"`
+ MountPoint string `json:"mountpoint"`
+}
+
+func mountCmd(c *cli.Context) error {
+ if err := validateFlags(c, mountFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ formats := map[string]bool{
+ "": true,
+ of.JSONString: true,
+ }
+
+ args := c.Args()
+ json := c.String("format") == of.JSONString
+ if !formats[c.String("format")] {
+ return errors.Errorf("%q is not a supported format", c.String("format"))
+ }
+
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments specified")
+ }
+
+ if len(args) == 1 {
+ if json {
+ return errors.Wrapf(err, "json option can not be used with a container id")
+ }
+ ctr, err := runtime.LookupContainer(args[0])
+ if err != nil {
+ return errors.Wrapf(err, "error looking up container %q", args[0])
+ }
+ mountPoint, err := ctr.Mount(c.String("label"))
+ if err != nil {
+ return errors.Wrapf(err, "error mounting container %q", ctr.ID())
+ }
+ fmt.Printf("%s\n", mountPoint)
+ } else {
+ jsonMountPoints := []jsonMountPoint{}
+ containers, err2 := runtime.GetContainers()
+ if err2 != nil {
+ return errors.Wrapf(err2, "error reading list of all containers")
+ }
+ for _, container := range containers {
+ mountPoint, err := container.MountPoint()
+ if err != nil {
+ return errors.Wrapf(err, "error getting mountpoint for %q", container.ID())
+ }
+ if mountPoint == "" {
+ continue
+ }
+ if json {
+ jsonMountPoints = append(jsonMountPoints, jsonMountPoint{ID: container.ID(), Names: []string{container.Name()}, MountPoint: mountPoint})
+ continue
+ }
+
+ if c.Bool("notruncate") {
+ fmt.Printf("%-64s %s\n", container.ID(), mountPoint)
+ } else {
+ fmt.Printf("%-12.12s %s\n", container.ID(), mountPoint)
+ }
+ }
+ if json {
+ data, err := js.MarshalIndent(jsonMountPoints, "", " ")
+ if err != nil {
+ return err
+ }
+ fmt.Printf("%s\n", data)
+ }
+ }
+ return nil
+}
diff --git a/cmd/podman/parse.go b/cmd/podman/parse.go
new file mode 100644
index 000000000..53d49c36c
--- /dev/null
+++ b/cmd/podman/parse.go
@@ -0,0 +1,863 @@
+//nolint
+// most of these validate and parse functions have been taken from projectatomic/docker
+// and modified for cri-o
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/user"
+ "path"
+ "regexp"
+ "strconv"
+ "strings"
+
+ units "github.com/docker/go-units"
+ specs "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ pb "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1/runtime"
+)
+
+// Note: for flags that are in the form <number><unit>, use the RAMInBytes function
+// from the units package in docker/go-units/size.go
+
+var (
+ whiteSpaces = " \t"
+ alphaRegexp = regexp.MustCompile(`[a-zA-Z]`)
+ domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`)
+)
+
+// validateExtraHost validates that the specified string is a valid extrahost and returns it.
+// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6).
+// for add-host flag
+func validateExtraHost(val string) (string, error) { //nolint
+ // allow for IPv6 addresses in extra hosts by only splitting on first ":"
+ arr := strings.SplitN(val, ":", 2)
+ if len(arr) != 2 || len(arr[0]) == 0 {
+ return "", fmt.Errorf("bad format for add-host: %q", val)
+ }
+ if _, err := validateIPAddress(arr[1]); err != nil {
+ return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1])
+ }
+ return val, nil
+}
+
+// validateIPAddress validates an Ip address.
+// for dns, ip, and ip6 flags also
+func validateIPAddress(val string) (string, error) {
+ var ip = net.ParseIP(strings.TrimSpace(val))
+ if ip != nil {
+ return ip.String(), nil
+ }
+ return "", fmt.Errorf("%s is not an ip address", val)
+}
+
+// validateAttach validates that the specified string is a valid attach option.
+// for attach flag
+func validateAttach(val string) (string, error) { //nolint
+ s := strings.ToLower(val)
+ for _, str := range []string{"stdin", "stdout", "stderr"} {
+ if s == str {
+ return s, nil
+ }
+ }
+ return val, fmt.Errorf("valid streams are STDIN, STDOUT and STDERR")
+}
+
+// validate the blkioWeight falls in the range of 10 to 1000
+// for blkio-weight flag
+func validateBlkioWeight(val int64) (int64, error) { //nolint
+ if val >= 10 && val <= 1000 {
+ return val, nil
+ }
+ return -1, errors.Errorf("invalid blkio weight %q, should be between 10 and 1000", val)
+}
+
+// weightDevice is a structure that holds device:weight pair
+type weightDevice struct {
+ path string
+ weight uint16
+}
+
+func (w *weightDevice) String() string {
+ return fmt.Sprintf("%s:%d", w.path, w.weight)
+}
+
+// validateweightDevice validates that the specified string has a valid device-weight format
+// for blkio-weight-device flag
+func validateweightDevice(val string) (*weightDevice, error) {
+ 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)
+ }
+ weight, err := strconv.ParseUint(split[1], 10, 0)
+ if err != nil {
+ return nil, fmt.Errorf("invalid weight for device: %s", val)
+ }
+ if weight > 0 && (weight < 10 || weight > 1000) {
+ return nil, fmt.Errorf("invalid weight for device: %s", val)
+ }
+
+ return &weightDevice{
+ path: split[0],
+ weight: uint16(weight),
+ }, nil
+}
+
+// parseDevice parses a device mapping string to a container.DeviceMapping struct
+// for device flag
+func parseDevice(device string) (*pb.Device, error) { //nolint
+ _, err := validateDevice(device)
+ if err != nil {
+ return nil, errors.Wrapf(err, "device string not valid %q", device)
+ }
+
+ src := ""
+ dst := ""
+ permissions := "rwm"
+ arr := strings.Split(device, ":")
+ switch len(arr) {
+ case 3:
+ permissions = arr[2]
+ fallthrough
+ case 2:
+ if validDeviceMode(arr[1]) {
+ permissions = arr[1]
+ } else {
+ dst = arr[1]
+ }
+ fallthrough
+ case 1:
+ src = arr[0]
+ default:
+ return nil, fmt.Errorf("invalid device specification: %s", device)
+ }
+
+ if dst == "" {
+ dst = src
+ }
+
+ deviceMapping := &pb.Device{
+ ContainerPath: dst,
+ HostPath: src,
+ Permissions: permissions,
+ }
+ return deviceMapping, nil
+}
+
+// validDeviceMode checks if the mode for device is valid or not.
+// Valid mode is a composition of r (read), w (write), and m (mknod).
+func validDeviceMode(mode string) bool {
+ var legalDeviceMode = map[rune]bool{
+ 'r': true,
+ 'w': true,
+ 'm': true,
+ }
+ if mode == "" {
+ return false
+ }
+ for _, c := range mode {
+ if !legalDeviceMode[c] {
+ return false
+ }
+ legalDeviceMode[c] = false
+ }
+ return true
+}
+
+// validateDevice validates a path for devices
+// It will make sure 'val' is in the form:
+// [host-dir:]container-path[:mode]
+// It also validates the device mode.
+func validateDevice(val string) (string, error) {
+ return validatePath(val, validDeviceMode)
+}
+
+func validatePath(val string, validator func(string) bool) (string, error) {
+ var containerPath string
+ var mode string
+
+ if strings.Count(val, ":") > 2 {
+ return val, fmt.Errorf("bad format for path: %s", val)
+ }
+
+ split := strings.SplitN(val, ":", 3)
+ if split[0] == "" {
+ return val, fmt.Errorf("bad format for path: %s", val)
+ }
+ switch len(split) {
+ case 1:
+ containerPath = split[0]
+ val = path.Clean(containerPath)
+ case 2:
+ if isValid := validator(split[1]); isValid {
+ containerPath = split[0]
+ mode = split[1]
+ val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode)
+ } else {
+ containerPath = split[1]
+ val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath))
+ }
+ case 3:
+ containerPath = split[1]
+ mode = split[2]
+ if isValid := validator(split[2]); !isValid {
+ return val, fmt.Errorf("bad mode specified: %s", mode)
+ }
+ val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode)
+ }
+
+ if !path.IsAbs(containerPath) {
+ return val, fmt.Errorf("%s is not an absolute path", containerPath)
+ }
+ return val, nil
+}
+
+// throttleDevice is a structure that holds device:rate_per_second pair
+type throttleDevice struct {
+ path string
+ rate uint64
+}
+
+func (t *throttleDevice) String() string {
+ return fmt.Sprintf("%s:%d", t.path, t.rate)
+}
+
+// validateBpsDevice validates that the specified string has a valid device-rate format
+// for device-read-bps and device-write-bps flags
+func validateBpsDevice(val string) (*throttleDevice, error) {
+ 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)
+ }
+
+ return &throttleDevice{
+ path: split[0],
+ rate: uint64(rate),
+ }, nil
+}
+
+// validateIOpsDevice validates that the specified string has a valid device-rate format
+// for device-write-iops and device-read-iops flags
+func validateIOpsDevice(val string) (*throttleDevice, error) { //nolint
+ 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)
+ }
+ if rate < 0 {
+ return nil, fmt.Errorf("invalid rate for device: %s. The correct format is <device-path>:<number>. Number must be a positive integer", val)
+ }
+
+ return &throttleDevice{
+ path: split[0],
+ rate: uint64(rate),
+ }, nil
+}
+
+// validateDNSSearch validates domain for resolvconf search configuration.
+// A zero length domain is represented by a dot (.).
+// for dns-search flag
+func validateDNSSearch(val string) (string, error) { //nolint
+ if val = strings.Trim(val, " "); val == "." {
+ return val, nil
+ }
+ return validateDomain(val)
+}
+
+func validateDomain(val string) (string, error) {
+ if alphaRegexp.FindString(val) == "" {
+ return "", fmt.Errorf("%s is not a valid domain", val)
+ }
+ ns := domainRegexp.FindSubmatch([]byte(val))
+ if len(ns) > 0 && len(ns[1]) < 255 {
+ return string(ns[1]), nil
+ }
+ return "", fmt.Errorf("%s is not a valid domain", val)
+}
+
+// validateEnv validates an environment variable and returns it.
+// If no value is specified, it returns the current value using os.Getenv.
+// for env flag
+func validateEnv(val string) (string, error) { //nolint
+ arr := strings.Split(val, "=")
+ if len(arr) > 1 {
+ return val, nil
+ }
+ if !doesEnvExist(val) {
+ return val, nil
+ }
+ return fmt.Sprintf("%s=%s", val, os.Getenv(val)), nil
+}
+
+func doesEnvExist(name string) bool {
+ for _, entry := range os.Environ() {
+ parts := strings.SplitN(entry, "=", 2)
+ if parts[0] == name {
+ return true
+ }
+ }
+ return false
+}
+
+// reads a file of line terminated key=value pairs, and overrides any keys
+// present in the file with additional pairs specified in the override parameter
+// for env-file and labels-file flags
+func readKVStrings(env map[string]string, files []string, override []string) error {
+ for _, ef := range files {
+ if err := parseEnvFile(env, ef); err != nil {
+ return err
+ }
+ }
+ for _, line := range override {
+ if err := parseEnv(env, line); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func parseEnv(env map[string]string, line string) error {
+ data := strings.SplitN(line, "=", 2)
+
+ // trim the front of a variable, but nothing else
+ name := strings.TrimLeft(data[0], whiteSpaces)
+ if strings.ContainsAny(name, whiteSpaces) {
+ return errors.Errorf("name %q has white spaces, poorly formatted name", name)
+ }
+
+ if len(data) > 1 {
+ env[name] = data[1]
+ } else {
+ // if only a pass-through variable is given, clean it up.
+ val, exists := os.LookupEnv(name)
+ if !exists {
+ return errors.Errorf("environment variable %q does not exist", name)
+ }
+ env[name] = val
+ }
+ return nil
+}
+
+// parseEnvFile reads a file with environment variables enumerated by lines
+func parseEnvFile(env map[string]string, filename string) error {
+ fh, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer fh.Close()
+
+ scanner := bufio.NewScanner(fh)
+ for scanner.Scan() {
+ // trim the line from all leading whitespace first
+ line := strings.TrimLeft(scanner.Text(), whiteSpaces)
+ // line is not empty, and not starting with '#'
+ if len(line) > 0 && !strings.HasPrefix(line, "#") {
+ if err := parseEnv(env, line); err != nil {
+ return err
+ }
+ }
+ }
+ return scanner.Err()
+}
+
+// NsIpc represents the container ipc stack.
+// for ipc flag
+type NsIpc string
+
+// IsPrivate indicates whether the container uses its private ipc stack.
+func (n NsIpc) IsPrivate() bool {
+ return !(n.IsHost() || n.IsContainer())
+}
+
+// IsHost indicates whether the container uses the host's ipc stack.
+func (n NsIpc) IsHost() bool {
+ return n == "host"
+}
+
+// IsContainer indicates whether the container uses a container's ipc stack.
+func (n NsIpc) IsContainer() bool {
+ parts := strings.SplitN(string(n), ":", 2)
+ return len(parts) > 1 && parts[0] == "container"
+}
+
+// Valid indicates whether the ipc stack is valid.
+func (n NsIpc) Valid() bool {
+ parts := strings.Split(string(n), ":")
+ switch mode := parts[0]; mode {
+ case "", "host":
+ case "container":
+ if len(parts) != 2 || parts[1] == "" {
+ return false
+ }
+ default:
+ return false
+ }
+ return true
+}
+
+// Container returns the name of the container ipc stack is going to be used.
+func (n NsIpc) Container() string {
+ parts := strings.SplitN(string(n), ":", 2)
+ if len(parts) > 1 {
+ return parts[1]
+ }
+ return ""
+}
+
+// validateLabel validates that the specified string is a valid label, and returns it.
+// Labels are in the form on key=value.
+// for label flag
+func validateLabel(val string) (string, error) { //nolint
+ if strings.Count(val, "=") < 1 {
+ return "", fmt.Errorf("bad attribute format: %s", val)
+ }
+ return val, nil
+}
+
+// validateMACAddress validates a MAC address.
+// for mac-address flag
+func validateMACAddress(val string) (string, error) { //nolint
+ _, err := net.ParseMAC(strings.TrimSpace(val))
+ if err != nil {
+ return "", err
+ }
+ return val, nil
+}
+
+// parseLoggingOpts validates the logDriver and logDriverOpts
+// for log-opt and log-driver flags
+func parseLoggingOpts(logDriver string, logDriverOpt []string) (map[string]string, error) { //nolint
+ logOptsMap := convertKVStringsToMap(logDriverOpt)
+ if logDriver == "none" && len(logDriverOpt) > 0 {
+ return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", logDriver)
+ }
+ return logOptsMap, nil
+}
+
+// NsPid represents the pid namespace of the container.
+//for pid flag
+type NsPid string
+
+// IsPrivate indicates whether the container uses its own new pid namespace.
+func (n NsPid) IsPrivate() bool {
+ return !(n.IsHost() || n.IsContainer())
+}
+
+// IsHost indicates whether the container uses the host's pid namespace.
+func (n NsPid) IsHost() bool {
+ return n == "host"
+}
+
+// IsContainer indicates whether the container uses a container's pid namespace.
+func (n NsPid) IsContainer() bool {
+ parts := strings.SplitN(string(n), ":", 2)
+ return len(parts) > 1 && parts[0] == "container"
+}
+
+// Valid indicates whether the pid namespace is valid.
+func (n NsPid) Valid() bool {
+ parts := strings.Split(string(n), ":")
+ switch mode := parts[0]; mode {
+ case "", "host":
+ case "container":
+ if len(parts) != 2 || parts[1] == "" {
+ return false
+ }
+ default:
+ return false
+ }
+ return true
+}
+
+// Container returns the name of the container whose pid namespace is going to be used.
+func (n NsPid) Container() string {
+ parts := strings.SplitN(string(n), ":", 2)
+ if len(parts) > 1 {
+ return parts[1]
+ }
+ return ""
+}
+
+// parsePortSpecs receives port specs in the format of ip:public:private/proto and parses
+// these in to the internal types
+// for publish, publish-all, and expose flags
+func parsePortSpecs(ports []string) ([]*pb.PortMapping, error) { //nolint
+ var portMappings []*pb.PortMapping
+ for _, rawPort := range ports {
+ portMapping, err := parsePortSpec(rawPort)
+ if err != nil {
+ return nil, err
+ }
+
+ portMappings = append(portMappings, portMapping...)
+ }
+ return portMappings, nil
+}
+
+func validateProto(proto string) bool {
+ for _, availableProto := range []string{"tcp", "udp"} {
+ if availableProto == proto {
+ return true
+ }
+ }
+ return false
+}
+
+// parsePortSpec parses a port specification string into a slice of PortMappings
+func parsePortSpec(rawPort string) ([]*pb.PortMapping, error) {
+ var proto string
+ rawIP, hostPort, containerPort := splitParts(rawPort)
+ proto, containerPort = splitProtoPort(containerPort)
+
+ // Strip [] from IPV6 addresses
+ ip, _, err := net.SplitHostPort(rawIP + ":")
+ if err != nil {
+ return nil, fmt.Errorf("Invalid ip address %v: %s", rawIP, err)
+ }
+ if ip != "" && net.ParseIP(ip) == nil {
+ return nil, fmt.Errorf("Invalid ip address: %s", ip)
+ }
+ if containerPort == "" {
+ return nil, fmt.Errorf("No port specified: %s<empty>", rawPort)
+ }
+
+ startPort, endPort, err := parsePortRange(containerPort)
+ if err != nil {
+ return nil, fmt.Errorf("Invalid containerPort: %s", containerPort)
+ }
+
+ var startHostPort, endHostPort uint64 = 0, 0
+ if len(hostPort) > 0 {
+ startHostPort, endHostPort, err = parsePortRange(hostPort)
+ if err != nil {
+ return nil, fmt.Errorf("Invalid hostPort: %s", hostPort)
+ }
+ }
+
+ if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
+ // Allow host port range iff containerPort is not a range.
+ // In this case, use the host port range as the dynamic
+ // host port range to allocate into.
+ if endPort != startPort {
+ return nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
+ }
+ }
+
+ if !validateProto(strings.ToLower(proto)) {
+ return nil, fmt.Errorf("invalid proto: %s", proto)
+ }
+
+ protocol := pb.Protocol_TCP
+ if strings.ToLower(proto) == "udp" {
+ protocol = pb.Protocol_UDP
+ }
+
+ var ports []*pb.PortMapping
+ for i := uint64(0); i <= (endPort - startPort); i++ {
+ containerPort = strconv.FormatUint(startPort+i, 10)
+ if len(hostPort) > 0 {
+ hostPort = strconv.FormatUint(startHostPort+i, 10)
+ }
+ // Set hostPort to a range only if there is a single container port
+ // and a dynamic host port.
+ if startPort == endPort && startHostPort != endHostPort {
+ hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10))
+ }
+
+ ctrPort, err := strconv.ParseInt(containerPort, 10, 32)
+ if err != nil {
+ return nil, err
+ }
+ hPort, err := strconv.ParseInt(hostPort, 10, 32)
+ if err != nil {
+ return nil, err
+ }
+
+ port := &pb.PortMapping{
+ Protocol: protocol,
+ ContainerPort: int32(ctrPort),
+ HostPort: int32(hPort),
+ HostIp: ip,
+ }
+
+ ports = append(ports, port)
+ }
+ return ports, nil
+}
+
+// parsePortRange parses and validates the specified string as a port-range (8000-9000)
+func parsePortRange(ports string) (uint64, uint64, error) {
+ if ports == "" {
+ return 0, 0, fmt.Errorf("empty string specified for ports")
+ }
+ if !strings.Contains(ports, "-") {
+ start, err := strconv.ParseUint(ports, 10, 16)
+ end := start
+ return start, end, err
+ }
+
+ parts := strings.Split(ports, "-")
+ start, err := strconv.ParseUint(parts[0], 10, 16)
+ if err != nil {
+ return 0, 0, err
+ }
+ end, err := strconv.ParseUint(parts[1], 10, 16)
+ if err != nil {
+ return 0, 0, err
+ }
+ if end < start {
+ return 0, 0, fmt.Errorf("Invalid range specified for the Port: %s", ports)
+ }
+ return start, end, nil
+}
+
+// splitParts separates the different parts of rawPort
+func splitParts(rawport string) (string, string, string) {
+ parts := strings.Split(rawport, ":")
+ n := len(parts)
+ containerport := parts[n-1]
+
+ switch n {
+ case 1:
+ return "", "", containerport
+ case 2:
+ return "", parts[0], containerport
+ case 3:
+ return parts[0], parts[1], containerport
+ default:
+ return strings.Join(parts[:n-2], ":"), parts[n-2], containerport
+ }
+}
+
+// splitProtoPort splits a port in the format of port/proto
+func splitProtoPort(rawPort string) (string, string) {
+ parts := strings.Split(rawPort, "/")
+ l := len(parts)
+ if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 {
+ return "", ""
+ }
+ if l == 1 {
+ return "tcp", rawPort
+ }
+ if len(parts[1]) == 0 {
+ return "tcp", parts[0]
+ }
+ return parts[1], parts[0]
+}
+
+// takes a local seccomp file and reads its file contents
+// for security-opt flag
+func parseSecurityOpts(securityOpts []string) ([]string, error) { //nolint
+ for key, opt := range securityOpts {
+ con := strings.SplitN(opt, "=", 2)
+ if len(con) == 1 && con[0] != "no-new-privileges" {
+ if strings.Index(opt, ":") != -1 {
+ con = strings.SplitN(opt, ":", 2)
+ } else {
+ return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt)
+ }
+ }
+ if con[0] == "seccomp" && con[1] != "unconfined" {
+ f, err := ioutil.ReadFile(con[1])
+ if err != nil {
+ return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %v", con[1], err)
+ }
+ b := bytes.NewBuffer(nil)
+ if err := json.Compact(b, f); err != nil {
+ return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err)
+ }
+ securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
+ }
+ }
+
+ return securityOpts, nil
+}
+
+// parses storage options per container into a map
+// for storage-opt flag
+func parseStorageOpts(storageOpts []string) (map[string]string, error) { //nolint
+ m := make(map[string]string)
+ for _, option := range storageOpts {
+ if strings.Contains(option, "=") {
+ opt := strings.SplitN(option, "=", 2)
+ m[opt[0]] = opt[1]
+ } else {
+ return nil, errors.Errorf("invalid storage option %q", option)
+ }
+ }
+ return m, nil
+}
+
+// parseUser parses the the uid and gid in the format <name|uid>[:<group|gid>]
+// for user flag
+// FIXME: Issue from https://github.com/projectatomic/buildah/issues/66
+func parseUser(rootdir, userspec string) (specs.User, error) { //nolint
+ var gid64 uint64
+ var gerr error = user.UnknownGroupError("error looking up group")
+
+ spec := strings.SplitN(userspec, ":", 2)
+ userspec = spec[0]
+ groupspec := ""
+ if userspec == "" {
+ return specs.User{}, nil
+ }
+ if len(spec) > 1 {
+ groupspec = spec[1]
+ }
+
+ uid64, uerr := strconv.ParseUint(userspec, 10, 32)
+ if uerr == nil && groupspec == "" {
+ // We parsed the user name as a number, and there's no group
+ // component, so we need to look up the user's primary GID.
+ var name string
+ name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64)
+ if gerr == nil {
+ userspec = name
+ } else {
+ if userrec, err := user.LookupId(userspec); err == nil {
+ gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32)
+ userspec = userrec.Name
+ }
+ }
+ }
+ if uerr != nil {
+ uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec)
+ gerr = uerr
+ }
+ if uerr != nil {
+ if userrec, err := user.Lookup(userspec); err == nil {
+ uid64, uerr = strconv.ParseUint(userrec.Uid, 10, 32)
+ gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32)
+ }
+ }
+
+ if groupspec != "" {
+ gid64, gerr = strconv.ParseUint(groupspec, 10, 32)
+ if gerr != nil {
+ gid64, gerr = lookupGroupInContainer(rootdir, groupspec)
+ }
+ if gerr != nil {
+ if group, err := user.LookupGroup(groupspec); err == nil {
+ gid64, gerr = strconv.ParseUint(group.Gid, 10, 32)
+ }
+ }
+ }
+
+ if uerr == nil && gerr == nil {
+ u := specs.User{
+ UID: uint32(uid64),
+ GID: uint32(gid64),
+ Username: userspec,
+ }
+ return u, nil
+ }
+
+ err := errors.Wrapf(uerr, "error determining run uid")
+ if uerr == nil {
+ err = errors.Wrapf(gerr, "error determining run gid")
+ }
+ return specs.User{}, err
+}
+
+// convertKVStringsToMap converts ["key=value"] to {"key":"value"}
+func convertKVStringsToMap(values []string) map[string]string {
+ result := make(map[string]string, len(values))
+ for _, value := range values {
+ kv := strings.SplitN(value, "=", 2)
+ if len(kv) == 1 {
+ result[kv[0]] = ""
+ } else {
+ result[kv[0]] = kv[1]
+ }
+ }
+
+ return result
+}
+
+// NsUser represents userns mode in the container.
+// for userns flag
+type NsUser string
+
+// IsHost indicates whether the container uses the host's userns.
+func (n NsUser) IsHost() bool {
+ return n == "host"
+}
+
+// IsPrivate indicates whether the container uses the a private userns.
+func (n NsUser) IsPrivate() bool {
+ return !(n.IsHost())
+}
+
+// Valid indicates whether the userns is valid.
+func (n NsUser) Valid() bool {
+ parts := strings.Split(string(n), ":")
+ switch mode := parts[0]; mode {
+ case "", "host":
+ default:
+ return false
+ }
+ return true
+}
+
+// NsUts represents the UTS namespace of the container.
+// for uts flag
+type NsUts string
+
+// IsPrivate indicates whether the container uses its private UTS namespace.
+func (n NsUts) IsPrivate() bool {
+ return !(n.IsHost())
+}
+
+// IsHost indicates whether the container uses the host's UTS namespace.
+func (n NsUts) IsHost() bool {
+ return n == "host"
+}
+
+// Valid indicates whether the UTS namespace is valid.
+func (n NsUts) Valid() bool {
+ parts := strings.Split(string(n), ":")
+ switch mode := parts[0]; mode {
+ case "", "host":
+ default:
+ return false
+ }
+ return true
+}
+
+// Takes a stringslice and converts to a uint32slice
+func stringSlicetoUint32Slice(inputSlice []string) ([]uint32, error) {
+ var outputSlice []uint32
+ for _, v := range inputSlice {
+ u, err := strconv.ParseUint(v, 10, 32)
+ if err != nil {
+ return outputSlice, err
+ }
+ outputSlice = append(outputSlice, uint32(u))
+ }
+ return outputSlice, nil
+}
diff --git a/cmd/podman/pause.go b/cmd/podman/pause.go
new file mode 100644
index 000000000..cd581b08f
--- /dev/null
+++ b/cmd/podman/pause.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+var (
+ pauseDescription = `
+ podman pause
+
+ Pauses one or more running containers. The container name or ID can be used.
+`
+ pauseCommand = cli.Command{
+ Name: "pause",
+ Usage: "Pauses all the processes in one or more containers",
+ Description: pauseDescription,
+ Action: pauseCmd,
+ ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
+ }
+)
+
+func pauseCmd(c *cli.Context) error {
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) < 1 {
+ return errors.Errorf("you must provide at least one container name or id")
+ }
+
+ var lastError error
+ for _, arg := range args {
+ ctr, err := runtime.LookupContainer(arg)
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "error looking up container %q", arg)
+ continue
+ }
+ if err = ctr.Pause(); err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to pause container %v", ctr.ID())
+ } else {
+ fmt.Println(ctr.ID())
+ }
+ }
+ return lastError
+}
diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go
new file mode 100644
index 000000000..c674c9d1e
--- /dev/null
+++ b/cmd/podman/ps.go
@@ -0,0 +1,606 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/docker/go-units"
+ specs "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+ "k8s.io/apimachinery/pkg/fields"
+)
+
+type psOptions struct {
+ all bool
+ filter string
+ format string
+ last int
+ latest bool
+ noTrunc bool
+ quiet bool
+ size bool
+ label string
+ namespace bool
+}
+
+type psTemplateParams struct {
+ ID string
+ Image string
+ Command string
+ CreatedAt string
+ RunningFor string
+ Status string
+ Ports string
+ Size string
+ Names string
+ Labels string
+ Mounts string
+ PID int
+ Cgroup string
+ IPC string
+ MNT string
+ NET string
+ PIDNS string
+ User string
+ UTS string
+}
+
+// psJSONParams is only used when the JSON format is specified,
+// and is better for data processing from JSON.
+// psJSONParams will be populated by data from libpod.Container,
+// the members of the struct are the sama data types as their sources.
+type psJSONParams struct {
+ ID string `json:"id"`
+ Image string `json:"image"`
+ ImageID string `json:"image_id"`
+ Command string `json:"command"`
+ CreatedAt time.Time `json:"createdAt"`
+ RunningFor time.Duration `json:"runningFor"`
+ Status string `json:"status"`
+ Ports map[string]struct{} `json:"ports"`
+ Size uint `json:"size"`
+ Names string `json:"names"`
+ Labels fields.Set `json:"labels"`
+ Mounts []specs.Mount `json:"mounts"`
+ ContainerRunning bool `json:"ctrRunning"`
+ Namespaces *namespace `json:"namespace,omitempty"`
+}
+
+type namespace struct {
+ PID string `json:"pid,omitempty"`
+ Cgroup string `json:"cgroup,omitempty"`
+ IPC string `json:"ipc,omitempty"`
+ MNT string `json:"mnt,omitempty"`
+ NET string `json:"net,omitempty"`
+ PIDNS string `json:"pidns,omitempty"`
+ User string `json:"user,omitempty"`
+ UTS string `json:"uts,omitempty"`
+}
+
+var (
+ psFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "Show all the containers, default is only running containers",
+ },
+ cli.StringFlag{
+ Name: "filter, f",
+ Usage: "Filter output based on conditions given",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Pretty-print containers to JSON or using a Go template",
+ },
+ cli.IntFlag{
+ Name: "last, n",
+ Usage: "Print the n last created containers (all states)",
+ Value: -1,
+ },
+ cli.BoolFlag{
+ Name: "latest, l",
+ Usage: "Show the latest container created (all states)",
+ },
+ cli.BoolFlag{
+ Name: "no-trunc",
+ Usage: "Display the extended information",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "Print the numeric IDs of the containers only",
+ },
+ cli.BoolFlag{
+ Name: "size, s",
+ Usage: "Display the total file sizes",
+ },
+ cli.BoolFlag{
+ Name: "namespace, ns",
+ Usage: "Display namespace information",
+ },
+ }
+ psDescription = "Prints out information about the containers"
+ psCommand = cli.Command{
+ Name: "ps",
+ Usage: "List containers",
+ Description: psDescription,
+ Flags: psFlags,
+ Action: psCmd,
+ ArgsUsage: "",
+ UseShortOptionHandling: true,
+ }
+)
+
+func psCmd(c *cli.Context) error {
+ if err := validateFlags(c, psFlags); err != nil {
+ return err
+ }
+
+ if err := checkFlagsPassed(c); err != nil {
+ return errors.Wrapf(err, "error with flags passed")
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+
+ defer runtime.Shutdown(false)
+
+ if len(c.Args()) > 0 {
+ return errors.Errorf("too many arguments, ps takes no arguments")
+ }
+
+ format := genPsFormat(c.String("format"), c.Bool("quiet"), c.Bool("size"), c.Bool("namespace"))
+
+ opts := psOptions{
+ all: c.Bool("all"),
+ filter: c.String("filter"),
+ format: format,
+ last: c.Int("last"),
+ latest: c.Bool("latest"),
+ noTrunc: c.Bool("no-trunc"),
+ quiet: c.Bool("quiet"),
+ size: c.Bool("size"),
+ namespace: c.Bool("namespace"),
+ }
+
+ var filterFuncs []libpod.ContainerFilter
+ // When we are dealing with latest or last=n, we need to
+ // get all containers.
+ if !opts.all && !opts.latest && opts.last < 1 {
+ // only get running containers
+ filterFuncs = append(filterFuncs, func(c *libpod.Container) bool {
+ state, _ := c.State()
+ return state == libpod.ContainerStateRunning
+ })
+ }
+
+ if opts.filter != "" {
+ filters := strings.Split(opts.filter, ",")
+ for _, f := range filters {
+ filterSplit := strings.Split(f, "=")
+ if len(filterSplit) < 2 {
+ return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
+ }
+ generatedFunc, err := generateContainerFilterFuncs(filterSplit[0], filterSplit[1], runtime)
+ if err != nil {
+ return errors.Wrapf(err, "invalid filter")
+ }
+ filterFuncs = append(filterFuncs, generatedFunc)
+ }
+ }
+
+ containers, err := runtime.GetContainers(filterFuncs...)
+ var outputContainers []*libpod.Container
+ if opts.latest && len(containers) > 0 {
+ outputContainers = append(outputContainers, containers[0])
+ } else if opts.last > 0 && opts.last <= len(containers) {
+ outputContainers = append(outputContainers, containers[:opts.last]...)
+ } else {
+ outputContainers = containers
+ }
+
+ return generatePsOutput(outputContainers, opts)
+}
+
+// checkFlagsPassed checks if mutually exclusive flags are passed together
+func checkFlagsPassed(c *cli.Context) error {
+ // latest, and last are mutually exclusive.
+ if c.Int("last") >= 0 && c.Bool("latest") {
+ return errors.Errorf("last and latest are mutually exclusive")
+ }
+ // quiet, size, namespace, and format with Go template are mutually exclusive
+ flags := 0
+ if c.Bool("quiet") {
+ flags++
+ }
+ if c.Bool("size") {
+ flags++
+ }
+ if c.Bool("namespace") {
+ flags++
+ }
+ if c.IsSet("format") && c.String("format") != formats.JSONString {
+ flags++
+ }
+ if flags > 1 {
+ return errors.Errorf("quiet, size, namespace, and format with Go template are mutually exclusive")
+ }
+ return nil
+}
+
+func generateContainerFilterFuncs(filter, filterValue string, runtime *libpod.Runtime) (func(container *libpod.Container) bool, error) {
+ switch filter {
+ case "id":
+ return func(c *libpod.Container) bool {
+ return c.ID() == filterValue
+ }, nil
+ case "label":
+ return func(c *libpod.Container) bool {
+ for _, label := range c.Labels() {
+ if label == filterValue {
+ return true
+ }
+ }
+ return false
+ }, nil
+ case "name":
+ return func(c *libpod.Container) bool {
+ return c.Name() == filterValue
+ }, nil
+ case "exited":
+ exitCode, err := strconv.ParseInt(filterValue, 10, 32)
+ if err != nil {
+ return nil, errors.Wrapf(err, "exited code out of range %q", filterValue)
+ }
+ return func(c *libpod.Container) bool {
+ ec, err := c.ExitCode()
+ if ec == int32(exitCode) && err == nil {
+ return true
+ }
+ return false
+ }, nil
+ case "status":
+ if !libpod.StringInSlice(filterValue, []string{"created", "restarting", "running", "paused", "exited", "unknown"}) {
+ return nil, errors.Errorf("%s is not a valid status", filterValue)
+ }
+ return func(c *libpod.Container) bool {
+ status, err := c.State()
+ if err != nil {
+ return false
+ }
+ return status.String() == filterValue
+ }, nil
+ case "ancestor":
+ // This needs to refine to match docker
+ // - ancestor=(<image-name>[:tag]|<image-id>| ⟨image@digest⟩) - containers created from an image or a descendant.
+ return func(c *libpod.Container) bool {
+ containerConfig := c.Config()
+ if containerConfig.RootfsImageID == filterValue || containerConfig.RootfsImageName == filterValue {
+ return true
+ }
+ return false
+ }, nil
+ case "before":
+ ctr, err := runtime.LookupContainer(filterValue)
+ if err != nil {
+ return nil, errors.Errorf("unable to find container by name or id of %s", filterValue)
+ }
+ containerConfig := ctr.Config()
+ createTime := containerConfig.CreatedTime
+ return func(c *libpod.Container) bool {
+ cc := c.Config()
+ return createTime.After(cc.CreatedTime)
+ }, nil
+ case "since":
+ ctr, err := runtime.LookupContainer(filterValue)
+ if err != nil {
+ return nil, errors.Errorf("unable to find container by name or id of %s", filterValue)
+ }
+ containerConfig := ctr.Config()
+ createTime := containerConfig.CreatedTime
+ return func(c *libpod.Container) bool {
+ cc := c.Config()
+ return createTime.Before(cc.CreatedTime)
+ }, nil
+ case "volume":
+ //- volume=(<volume-name>|<mount-point-destination>)
+ return func(c *libpod.Container) bool {
+ containerConfig := c.Config()
+ //TODO We need to still lookup against volumes too
+ return containerConfig.MountLabel == filterValue
+ }, nil
+ }
+ return nil, errors.Errorf("%s is an invalid filter", filter)
+}
+
+// generate the template based on conditions given
+func genPsFormat(format string, quiet, size, namespace bool) string {
+ if format != "" {
+ // "\t" from the command line is not being recognized as a tab
+ // replacing the string "\t" to a tab character if the user passes in "\t"
+ return strings.Replace(format, `\t`, "\t", -1)
+ }
+ if quiet {
+ return formats.IDString
+ }
+ if namespace {
+ return "table {{.ID}}\t{{.Names}}\t{{.PID}}\t{{.Cgroup}}\t{{.IPC}}\t{{.MNT}}\t{{.NET}}\t{{.PIDNS}}\t{{.User}}\t{{.UTS}}\t"
+ }
+ format = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}\t"
+ if size {
+ format += "{{.Size}}\t"
+ }
+ return format
+}
+
+func psToGeneric(templParams []psTemplateParams, JSONParams []psJSONParams) (genericParams []interface{}) {
+ if len(templParams) > 0 {
+ for _, v := range templParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+ }
+ for _, v := range JSONParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+}
+
+// generate the accurate header based on template given
+func (p *psTemplateParams) headerMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(p))
+ values := make(map[string]string)
+
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ if value == "ID" {
+ value = "Container" + value
+ }
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+// getTemplateOutput returns the modified container information
+func getTemplateOutput(containers []*libpod.Container, opts psOptions) ([]psTemplateParams, error) {
+ var psOutput []psTemplateParams
+ var status string
+ for _, ctr := range containers {
+ ctrID := ctr.ID()
+ conConfig := ctr.Config()
+ conState, err := ctr.State()
+ if err != nil {
+ return psOutput, errors.Wrapf(err, "unable to obtain container state")
+ }
+ exitCode, err := ctr.ExitCode()
+ if err != nil {
+ return psOutput, errors.Wrapf(err, "unable to obtain container exit code")
+ }
+ pid, err := ctr.PID()
+ if err != nil {
+ return psOutput, errors.Wrapf(err, "unable to obtain container pid")
+ }
+ runningFor := units.HumanDuration(time.Since(conConfig.CreatedTime))
+ createdAt := runningFor + " ago"
+ imageName := conConfig.RootfsImageName
+
+ // TODO We currently dont have the ability to get many of
+ // these data items. Uncomment as progress is made
+
+ //command := getStrFromSquareBrackets(ctr.ImageCreatedBy)
+ command := strings.Join(ctr.Spec().Process.Args, " ")
+ //mounts := getMounts(ctr.Mounts, opts.noTrunc)
+ //ports := getPorts(ctr.Config.ExposedPorts)
+ //size := units.HumanSize(float64(ctr.SizeRootFs))
+ labels := formatLabels(ctr.Labels())
+ ns := getNamespaces(pid)
+
+ switch conState {
+ case libpod.ContainerStateStopped:
+ status = fmt.Sprintf("Exited (%d) %s ago", exitCode, runningFor)
+ case libpod.ContainerStateRunning:
+ status = "Up " + runningFor + " ago"
+ case libpod.ContainerStatePaused:
+ status = "Paused"
+ case libpod.ContainerStateCreated:
+ status = "Created"
+ default:
+ status = "Dead"
+ }
+
+ if !opts.noTrunc {
+ ctrID = ctr.ID()[:idTruncLength]
+ imageName = conConfig.RootfsImageName
+ }
+
+ // TODO We currently dont have the ability to get many of
+ // these data items. Uncomment as progress is made
+
+ params := psTemplateParams{
+ ID: ctrID,
+ Image: imageName,
+ Command: command,
+ CreatedAt: createdAt,
+ RunningFor: runningFor,
+ Status: status,
+ //Ports: ports,
+ //Size: size,
+ Names: ctr.Name(),
+ Labels: labels,
+ //Mounts: mounts,
+ PID: pid,
+ Cgroup: ns.Cgroup,
+ IPC: ns.IPC,
+ MNT: ns.MNT,
+ NET: ns.NET,
+ PIDNS: ns.PID,
+ User: ns.User,
+ UTS: ns.UTS,
+ }
+ psOutput = append(psOutput, params)
+ }
+ return psOutput, nil
+}
+
+func getNamespaces(pid int) *namespace {
+ ctrPID := strconv.Itoa(pid)
+ cgroup, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup"))
+ ipc, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc"))
+ mnt, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "mnt"))
+ net, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "net"))
+ pidns, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "pid"))
+ user, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "user"))
+ uts, _ := getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "uts"))
+
+ return &namespace{
+ PID: ctrPID,
+ Cgroup: cgroup,
+ IPC: ipc,
+ MNT: mnt,
+ NET: net,
+ PIDNS: pidns,
+ User: user,
+ UTS: uts,
+ }
+}
+
+func getNamespaceInfo(path string) (string, error) {
+ val, err := os.Readlink(path)
+ if err != nil {
+ return "", errors.Wrapf(err, "error getting info from %q", path)
+ }
+ return getStrFromSquareBrackets(val), nil
+}
+
+// getJSONOutput returns the container info in its raw form
+func getJSONOutput(containers []*libpod.Container, nSpace bool) ([]psJSONParams, error) {
+ var psOutput []psJSONParams
+ var ns *namespace
+ for _, ctr := range containers {
+ pid, err := ctr.PID()
+ if err != nil {
+ return psOutput, errors.Wrapf(err, "unable to obtain container pid")
+ }
+ if nSpace {
+ ns = getNamespaces(pid)
+ }
+ cc := ctr.Config()
+ conState, err := ctr.State()
+ if err != nil {
+ return psOutput, errors.Wrapf(err, "unable to obtain container state for JSON output")
+ }
+ params := psJSONParams{
+ // TODO When we have ability to obtain the commented out data, we need
+ // TODO to add it
+ ID: ctr.ID(),
+ Image: cc.RootfsImageName,
+ ImageID: cc.RootfsImageID,
+ //Command: getStrFromSquareBrackets(ctr.ImageCreatedBy),
+ Command: strings.Join(ctr.Spec().Process.Args, " "),
+ CreatedAt: cc.CreatedTime,
+ RunningFor: time.Since(cc.CreatedTime),
+ Status: conState.String(),
+ //Ports: cc.Spec.Linux.Resources.Network.
+ //Size: ctr.SizeRootFs,
+ Names: cc.Name,
+ Labels: cc.Labels,
+ Mounts: cc.Spec.Mounts,
+ ContainerRunning: conState == libpod.ContainerStateRunning,
+ Namespaces: ns,
+ }
+ psOutput = append(psOutput, params)
+ }
+ return psOutput, nil
+}
+
+func generatePsOutput(containers []*libpod.Container, opts psOptions) error {
+ if len(containers) == 0 && opts.format != formats.JSONString {
+ return nil
+ }
+ var out formats.Writer
+
+ switch opts.format {
+ case formats.JSONString:
+ psOutput, err := getJSONOutput(containers, opts.namespace)
+ if err != nil {
+ return errors.Wrapf(err, "unable to create JSON for output")
+ }
+ out = formats.JSONStructArray{Output: psToGeneric([]psTemplateParams{}, psOutput)}
+ default:
+ psOutput, err := getTemplateOutput(containers, opts)
+ if err != nil {
+ return errors.Wrapf(err, "unable to create output")
+ }
+ out = formats.StdoutTemplateArray{Output: psToGeneric(psOutput, []psJSONParams{}), Template: opts.format, Fields: psOutput[0].headerMap()}
+ }
+
+ return formats.Writer(out).Out()
+}
+
+// getStrFromSquareBrackets gets the string inside [] from a string
+func getStrFromSquareBrackets(cmd string) string {
+ reg, err := regexp.Compile(".*\\[|\\].*")
+ if err != nil {
+ return ""
+ }
+ arr := strings.Split(reg.ReplaceAllLiteralString(cmd, ""), ",")
+ return strings.Join(arr, ",")
+}
+
+// getLabels converts the labels to a string of the form "key=value, key2=value2"
+func formatLabels(labels map[string]string) string {
+ var arr []string
+ if len(labels) > 0 {
+ for key, val := range labels {
+ temp := key + "=" + val
+ arr = append(arr, temp)
+ }
+ return strings.Join(arr, ",")
+ }
+ return ""
+}
+
+/*
+// getMounts converts the volumes mounted to a string of the form "mount1, mount2"
+// it truncates it if noTrunc is false
+func getMounts(mounts []specs.Mount, noTrunc bool) string {
+ var arr []string
+ if len(mounts) == 0 {
+ return ""
+ }
+ for _, mount := range mounts {
+ if noTrunc {
+ arr = append(arr, mount.Source)
+ continue
+ }
+ tempArr := strings.SplitAfter(mount.Source, "/")
+ if len(tempArr) >= 3 {
+ arr = append(arr, strings.Join(tempArr[:3], ""))
+ } else {
+ arr = append(arr, mount.Source)
+ }
+ }
+ return strings.Join(arr, ",")
+}
+// getPorts converts the ports used to a string of the from "port1, port2"
+func getPorts(ports map[string]struct{}) string {
+ var arr []string
+ if len(ports) == 0 {
+ return ""
+ }
+ for key := range ports {
+ arr = append(arr, key)
+ }
+ return strings.Join(arr, ",")
+}
+*/
diff --git a/cmd/podman/pull.go b/cmd/podman/pull.go
new file mode 100644
index 000000000..5726b20f1
--- /dev/null
+++ b/cmd/podman/pull.go
@@ -0,0 +1,120 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "golang.org/x/crypto/ssh/terminal"
+
+ "github.com/containers/image/types"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/projectatomic/libpod/libpod/common"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var (
+ pullFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "authfile",
+ Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
+ },
+ cli.StringFlag{
+ Name: "cert-dir",
+ Usage: "`pathname` of a directory containing TLS certificates and keys",
+ },
+ cli.StringFlag{
+ Name: "creds",
+ Usage: "`credentials` (USERNAME:PASSWORD) to use for authenticating to a registry",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "Suppress output information when pulling images",
+ },
+ cli.StringFlag{
+ Name: "signature-policy",
+ Usage: "`pathname` of signature policy file (not usually used)",
+ },
+ cli.BoolTFlag{
+ Name: "tls-verify",
+ Usage: "require HTTPS and verify certificates when contacting registries (default: true)",
+ },
+ }
+
+ pullDescription = "Pulls an image from a registry and stores it locally.\n" +
+ "An image can be pulled using its tag or digest. If a tag is not\n" +
+ "specified, the image with the 'latest' tag (if it exists) is pulled."
+ pullCommand = cli.Command{
+ Name: "pull",
+ Usage: "pull an image from a registry",
+ Description: pullDescription,
+ Flags: pullFlags,
+ Action: pullCmd,
+ ArgsUsage: "",
+ }
+)
+
+// pullCmd gets the data from the command line and calls pullImage
+// to copy an image from a registry to a local machine
+func pullCmd(c *cli.Context) error {
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) == 0 {
+ logrus.Errorf("an image name must be specified")
+ return nil
+ }
+ if len(args) > 1 {
+ logrus.Errorf("too many arguments. Requires exactly 1")
+ return nil
+ }
+ if err := validateFlags(c, pullFlags); err != nil {
+ return err
+ }
+ image := args[0]
+
+ var registryCreds *types.DockerAuthConfig
+ if c.String("creds") != "" {
+ creds, err := common.ParseRegistryCreds(c.String("creds"))
+ if err != nil {
+ if err == common.ErrNoPassword {
+ fmt.Print("Password: ")
+ password, err := terminal.ReadPassword(0)
+ if err != nil {
+ return errors.Wrapf(err, "could not read password from terminal")
+ }
+ creds.Password = string(password)
+ } else {
+ return err
+ }
+ }
+ registryCreds = creds
+ }
+
+ var writer io.Writer
+ if !c.Bool("quiet") {
+ writer = os.Stdout
+ }
+
+ options := libpod.CopyOptions{
+ SignaturePolicyPath: c.String("signature-policy"),
+ AuthFile: c.String("authfile"),
+ DockerRegistryOptions: common.DockerRegistryOptions{
+ DockerRegistryCreds: registryCreds,
+ DockerCertPath: c.String("cert-dir"),
+ DockerInsecureSkipTLSVerify: !c.BoolT("tls-verify"),
+ },
+ Writer: writer,
+ }
+
+ if _, err := runtime.PullImage(image, options); err != nil {
+ return errors.Wrapf(err, "error pulling image %q", image)
+ }
+ return nil
+}
diff --git a/cmd/podman/push.go b/cmd/podman/push.go
new file mode 100644
index 000000000..69d6e6629
--- /dev/null
+++ b/cmd/podman/push.go
@@ -0,0 +1,167 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "github.com/containers/image/manifest"
+ "github.com/containers/image/types"
+ "github.com/containers/storage/pkg/archive"
+ imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/projectatomic/libpod/libpod/common"
+ "github.com/urfave/cli"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+var (
+ pushFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "signature-policy",
+ Usage: "`pathname` of signature policy file (not usually used)",
+ Hidden: true,
+ },
+ cli.StringFlag{
+ Name: "creds",
+ Usage: "`credentials` (USERNAME:PASSWORD) to use for authenticating to a registry",
+ },
+ cli.StringFlag{
+ Name: "cert-dir",
+ Usage: "`pathname` of a directory containing TLS certificates and keys",
+ },
+ cli.BoolFlag{
+ Name: "compress",
+ Usage: "compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)",
+ },
+ cli.StringFlag{
+ Name: "format, f",
+ Usage: "manifest type (oci, v2s1, or v2s2) to use when pushing an image using the 'dir:' transport (default is manifest type of source)",
+ },
+ cli.BoolTFlag{
+ Name: "tls-verify",
+ Usage: "require HTTPS and verify certificates when contacting registries (default: true)",
+ },
+ cli.BoolFlag{
+ Name: "remove-signatures",
+ Usage: "discard any pre-existing signatures in the image",
+ },
+ cli.StringFlag{
+ Name: "sign-by",
+ Usage: "add a signature at the destination using the specified key",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "don't output progress information when pushing images",
+ },
+ cli.StringFlag{
+ Name: "authfile",
+ Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
+ },
+ }
+ pushDescription = fmt.Sprintf(`
+ Pushes an image to a specified location.
+ The Image "DESTINATION" uses a "transport":"details" format.
+ See podman-push(1) section "DESTINATION" for the expected format`)
+
+ pushCommand = cli.Command{
+ Name: "push",
+ Usage: "push an image to a specified destination",
+ Description: pushDescription,
+ Flags: pushFlags,
+ Action: pushCmd,
+ ArgsUsage: "IMAGE DESTINATION",
+ }
+)
+
+func pushCmd(c *cli.Context) error {
+ var registryCreds *types.DockerAuthConfig
+
+ args := c.Args()
+ if len(args) < 2 {
+ return errors.New("podman push requires exactly 2 arguments")
+ }
+ if err := validateFlags(c, pushFlags); err != nil {
+ return err
+ }
+ srcName := args[0]
+ destName := args[1]
+
+ // --compress and --format can only be used for the "dir" transport
+ splitArg := strings.SplitN(destName, ":", 2)
+ if c.IsSet("compress") || c.IsSet("format") {
+ if splitArg[0] != libpod.DirTransport {
+ return errors.Errorf("--compress and --format can be set only when pushing to a directory using the 'dir' transport")
+ }
+ }
+
+ registryCredsString := c.String("creds")
+ certPath := c.String("cert-dir")
+ skipVerify := !c.BoolT("tls-verify")
+ removeSignatures := c.Bool("remove-signatures")
+ signBy := c.String("sign-by")
+
+ if registryCredsString != "" {
+ creds, err := common.ParseRegistryCreds(registryCredsString)
+ if err != nil {
+ if err == common.ErrNoPassword {
+ fmt.Print("Password: ")
+ password, err := terminal.ReadPassword(0)
+ if err != nil {
+ return errors.Wrapf(err, "could not read password from terminal")
+ }
+ creds.Password = string(password)
+ } else {
+ return err
+ }
+ }
+ registryCreds = creds
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not create runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ var writer io.Writer
+ if !c.Bool("quiet") {
+ writer = os.Stdout
+ }
+
+ var manifestType string
+ if c.IsSet("format") {
+ switch c.String("format") {
+ case "oci":
+ manifestType = imgspecv1.MediaTypeImageManifest
+ case "v2s1":
+ manifestType = manifest.DockerV2Schema1SignedMediaType
+ case "v2s2", "docker":
+ manifestType = manifest.DockerV2Schema2MediaType
+ default:
+ return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", c.String("format"))
+ }
+ }
+
+ options := libpod.CopyOptions{
+ Compression: archive.Uncompressed,
+ SignaturePolicyPath: c.String("signature-policy"),
+ DockerRegistryOptions: common.DockerRegistryOptions{
+ DockerRegistryCreds: registryCreds,
+ DockerCertPath: certPath,
+ DockerInsecureSkipTLSVerify: skipVerify,
+ },
+ SigningOptions: common.SigningOptions{
+ RemoveSignatures: removeSignatures,
+ SignBy: signBy,
+ },
+ AuthFile: c.String("authfile"),
+ Writer: writer,
+ ManifestMIMEType: manifestType,
+ ForceCompress: c.Bool("compress"),
+ }
+
+ return runtime.PushImage(srcName, destName, options)
+}
diff --git a/cmd/podman/rm.go b/cmd/podman/rm.go
new file mode 100644
index 000000000..dcb8fac57
--- /dev/null
+++ b/cmd/podman/rm.go
@@ -0,0 +1,90 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ rmFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "force, f",
+ Usage: "Force removal of a running container. The default is false",
+ },
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "Remove all containers",
+ },
+ }
+ rmDescription = "Remove one or more containers"
+ rmCommand = cli.Command{
+ Name: "rm",
+ Usage: fmt.Sprintf(`podman rm will remove one or more containers from the host. The container name or ID can be used.
+ This does not remove images. Running containers will not be removed without the -f option.`),
+ Description: rmDescription,
+ Flags: rmFlags,
+ Action: rmCmd,
+ ArgsUsage: "",
+ UseShortOptionHandling: true,
+ }
+)
+
+// saveCmd saves the image to either docker-archive or oci
+func rmCmd(c *cli.Context) error {
+ if err := validateFlags(c, rmFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) == 0 && !c.Bool("all") {
+ return errors.Errorf("specify one or more containers to remove")
+ }
+
+ var delContainers []*libpod.Container
+ var lastError error
+ if c.Bool("all") {
+ delContainers, err = runtime.GetContainers()
+ if err != nil {
+ return errors.Wrapf(err, "unable to get container list")
+ }
+ } else {
+ for _, i := range args {
+ container, err := runtime.LookupContainer(i)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ lastError = errors.Wrapf(err, "unable to find container %s", i)
+ continue
+ }
+ delContainers = append(delContainers, container)
+ }
+ }
+ for _, container := range delContainers {
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to find container %s", container.ID())
+ continue
+ }
+ err = runtime.RemoveContainer(container, c.Bool("force"))
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to delete container %v", container.ID())
+ } else {
+ fmt.Println(container.ID())
+ }
+ }
+ return lastError
+}
diff --git a/cmd/podman/rmi.go b/cmd/podman/rmi.go
new file mode 100644
index 000000000..1b4cb7390
--- /dev/null
+++ b/cmd/podman/rmi.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "fmt"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ rmiDescription = "removes one or more locally stored images."
+ rmiFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "remove all images",
+ },
+ cli.BoolFlag{
+ Name: "force, f",
+ Usage: "force removal of the image",
+ },
+ }
+ rmiCommand = cli.Command{
+ Name: "rmi",
+ Usage: "removes one or more images from local storage",
+ Description: rmiDescription,
+ Action: rmiCmd,
+ ArgsUsage: "IMAGE-NAME-OR-ID [...]",
+ Flags: rmiFlags,
+ UseShortOptionHandling: true,
+ }
+)
+
+func rmiCmd(c *cli.Context) error {
+ if err := validateFlags(c, rmiFlags); err != nil {
+ return err
+ }
+ removeAll := c.Bool("all")
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) == 0 && !removeAll {
+ return errors.Errorf("image name or ID must be specified")
+ }
+ if len(args) > 0 && removeAll {
+ return errors.Errorf("when using the --all switch, you may not pass any images names or IDs")
+ }
+ imagesToDelete := args[:]
+ if removeAll {
+ localImages, err := runtime.GetImages(&libpod.ImageFilterParams{})
+ if err != nil {
+ return errors.Wrapf(err, "unable to query local images")
+ }
+ for _, image := range localImages {
+ imagesToDelete = append(imagesToDelete, image.ID)
+ }
+ }
+
+ for _, arg := range imagesToDelete {
+ image, err := runtime.GetImage(arg)
+ if err != nil {
+ return errors.Wrapf(err, "could not get image %q", arg)
+ }
+ id, err := runtime.RemoveImage(image, c.Bool("force"))
+ if err != nil {
+ return errors.Wrapf(err, "error removing image %q", id)
+ }
+ fmt.Printf("%s\n", id)
+ }
+ return nil
+}
diff --git a/cmd/podman/run.go b/cmd/podman/run.go
new file mode 100644
index 000000000..6142983ad
--- /dev/null
+++ b/cmd/podman/run.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var runDescription = "Runs a command in a new container from the given image"
+
+var runCommand = cli.Command{
+ Name: "run",
+ Usage: "run a command in a new container",
+ Description: runDescription,
+ Flags: createFlags,
+ Action: runCmd,
+ ArgsUsage: "IMAGE [COMMAND [ARG...]]",
+ SkipArgReorder: true,
+ UseShortOptionHandling: true,
+}
+
+func runCmd(c *cli.Context) error {
+ var imageName string
+ if err := validateFlags(c, createFlags); err != nil {
+ return err
+ }
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ createConfig, err := parseCreateOpts(c, runtime)
+ if err != nil {
+ return err
+ }
+
+ createImage := runtime.NewImage(createConfig.Image)
+ createImage.LocalName, _ = createImage.GetLocalImageName()
+ if createImage.LocalName == "" {
+ // The image wasnt found by the user input'd name or its fqname
+ // Pull the image
+ fmt.Printf("Trying to pull %s...", createImage.PullName)
+ createImage.Pull()
+ }
+
+ runtimeSpec, err := createConfigToOCISpec(createConfig)
+ if err != nil {
+ return err
+ }
+ logrus.Debug("spec is ", runtimeSpec)
+
+ if createImage.LocalName != "" {
+ nameIsID, err := runtime.IsImageID(createImage.LocalName)
+ if err != nil {
+ return err
+ }
+ if nameIsID {
+ // If the input from the user is an ID, then we need to get the image
+ // name for cstorage
+ createImage.LocalName, err = createImage.GetNameByID()
+ if err != nil {
+ return err
+ }
+ }
+ imageName = createImage.LocalName
+ } else {
+ imageName, err = createImage.GetFQName()
+ }
+ if err != nil {
+ return err
+ }
+ logrus.Debug("imageName is ", imageName)
+
+ imageID, err := createImage.GetImageID()
+ if err != nil {
+ return err
+ }
+ logrus.Debug("imageID is ", imageID)
+
+ options, err := createConfig.GetContainerCreateOptions()
+ if err != nil {
+ return errors.Wrapf(err, "unable to parse new container options")
+ }
+
+ // Gather up the options for NewContainer which consist of With... funcs
+ options = append(options, libpod.WithRootFSFromImage(imageID, imageName, false))
+ options = append(options, libpod.WithSELinuxLabels(createConfig.ProcessLabel, createConfig.MountLabel))
+ options = append(options, libpod.WithShmDir(createConfig.ShmDir))
+ ctr, err := runtime.NewContainer(runtimeSpec, options...)
+ if err != nil {
+ return err
+ }
+
+ logrus.Debug("new container created ", ctr.ID())
+ if err := ctr.Init(); err != nil {
+ return err
+ }
+ logrus.Debugf("container storage created for %q", ctr.ID())
+
+ if c.String("cidfile") != "" {
+ libpod.WriteFile(ctr.ID(), c.String("cidfile"))
+ return nil
+ }
+
+ // Create a bool channel to track that the console socket attach
+ // is successful.
+ attached := make(chan bool)
+ // Create a waitgroup so we can sync and wait for all goroutines
+ // to finish before exiting main
+ var wg sync.WaitGroup
+
+ if !createConfig.Detach {
+ // We increment the wg counter because we need to do the attach
+ wg.Add(1)
+ // Attach to the running container
+ go func() {
+ logrus.Debugf("trying to attach to the container %s", ctr.ID())
+ defer wg.Done()
+ if err := ctr.Attach(false, c.String("detach-keys"), attached); err != nil {
+ logrus.Errorf("unable to attach to container %s: %q", ctr.ID(), err)
+ }
+ }()
+ if !<-attached {
+ return errors.Errorf("unable to attach to container %s", ctr.ID())
+ }
+ }
+ // Start the container
+ if err := ctr.Start(); err != nil {
+ return errors.Wrapf(err, "unable to start container %q", ctr.ID())
+ }
+ if createConfig.Detach {
+ fmt.Printf("%s\n", ctr.ID())
+ return nil
+ }
+ wg.Wait()
+
+ if createConfig.Rm {
+ return runtime.RemoveContainer(ctr, true)
+ }
+ return ctr.CleanupStorage()
+}
diff --git a/cmd/podman/save.go b/cmd/podman/save.go
new file mode 100644
index 000000000..85a8c7930
--- /dev/null
+++ b/cmd/podman/save.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "io"
+ "os"
+ "strings"
+
+ "github.com/containers/image/manifest"
+ imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+const (
+ ociManifestDir = "oci-dir"
+ v2s2ManifestDir = "docker-dir"
+)
+
+var (
+ saveFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "compress",
+ Usage: "compress tarball image layers when saving to a directory using the 'dir' transport. (default is same compression type as source)",
+ },
+ cli.StringFlag{
+ Name: "output, o",
+ Usage: "Write to a file, default is STDOUT",
+ Value: "/dev/stdout",
+ },
+ cli.BoolFlag{
+ Name: "quiet, q",
+ Usage: "Suppress the output",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Save image to oci-archive, oci-dir (directory with oci manifest type), docker-dir (directory with v2s2 manifest type)",
+ },
+ }
+ saveDescription = `
+ Save an image to docker-archive or oci-archive on the local machine.
+ Default is docker-archive`
+
+ saveCommand = cli.Command{
+ Name: "save",
+ Usage: "Save image to an archive",
+ Description: saveDescription,
+ Flags: saveFlags,
+ Action: saveCmd,
+ ArgsUsage: "",
+ }
+)
+
+// saveCmd saves the image to either docker-archive or oci
+func saveCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) == 0 {
+ return errors.Errorf("need at least 1 argument")
+ }
+ if err := validateFlags(c, saveFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not create runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ if c.IsSet("compress") && (c.String("format") != ociManifestDir && c.String("format") != v2s2ManifestDir && c.String("format") == "") {
+ return errors.Errorf("--compress can only be set when --format is either 'oci-dir' or 'docker-dir'")
+ }
+
+ var writer io.Writer
+ if !c.Bool("quiet") {
+ writer = os.Stdout
+ }
+
+ output := c.String("output")
+ if output == "/dev/stdout" {
+ fi := os.Stdout
+ if logrus.IsTerminal(fi) {
+ return errors.Errorf("refusing to save to terminal. Use -o flag or redirect")
+ }
+ }
+
+ var dst, manifestType string
+ switch c.String("format") {
+ case libpod.OCIArchive:
+ dst = libpod.OCIArchive + ":" + output
+ case "oci-dir":
+ dst = libpod.DirTransport + ":" + output
+ manifestType = imgspecv1.MediaTypeImageManifest
+ case "docker-dir":
+ dst = libpod.DirTransport + ":" + output
+ manifestType = manifest.DockerV2Schema2MediaType
+ case libpod.DockerArchive:
+ fallthrough
+ case "":
+ dst = libpod.DockerArchive + ":" + output
+ default:
+ return errors.Errorf("unknown format option %q", c.String("format"))
+ }
+
+ saveOpts := libpod.CopyOptions{
+ SignaturePolicyPath: "",
+ Writer: writer,
+ ManifestMIMEType: manifestType,
+ ForceCompress: c.Bool("compress"),
+ }
+
+ // only one image is supported for now
+ // future pull requests will fix this
+ for _, image := range args {
+ dest := dst
+ // need dest to be in the format transport:path:reference for the following transports
+ if strings.Contains(dst, libpod.OCIArchive) || strings.Contains(dst, libpod.DockerArchive) {
+ dest = dst + ":" + image
+ }
+ if err := runtime.PushImage(image, dest, saveOpts); err != nil {
+ if err2 := os.Remove(output); err2 != nil {
+ logrus.Errorf("error deleting %q: %v", output, err)
+ }
+ return errors.Wrapf(err, "unable to save %q", image)
+ }
+ }
+ return nil
+}
diff --git a/cmd/podman/spec.go b/cmd/podman/spec.go
new file mode 100644
index 000000000..adfdf7347
--- /dev/null
+++ b/cmd/podman/spec.go
@@ -0,0 +1,561 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "github.com/cri-o/ocicni/pkg/ocicni"
+ "github.com/docker/docker/daemon/caps"
+ "github.com/docker/docker/pkg/mount"
+ "github.com/docker/go-units"
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/opencontainers/runtime-tools/generate"
+ "github.com/opencontainers/selinux/go-selinux/label"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ ann "github.com/projectatomic/libpod/pkg/annotations"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/sys/unix"
+)
+
+func blockAccessToKernelFilesystems(config *createConfig, g *generate.Generator) {
+ if !config.Privileged {
+ for _, mp := range []string{
+ "/proc/kcore",
+ "/proc/latency_stats",
+ "/proc/timer_list",
+ "/proc/timer_stats",
+ "/proc/sched_debug",
+ "/proc/scsi",
+ "/sys/firmware",
+ } {
+ g.AddLinuxMaskedPaths(mp)
+ }
+
+ for _, rp := range []string{
+ "/proc/asound",
+ "/proc/bus",
+ "/proc/fs",
+ "/proc/irq",
+ "/proc/sys",
+ "/proc/sysrq-trigger",
+ } {
+ g.AddLinuxReadonlyPaths(rp)
+ }
+ }
+}
+
+func addPidNS(config *createConfig, g *generate.Generator) error {
+ pidMode := config.PidMode
+ if pidMode.IsHost() {
+ return g.RemoveLinuxNamespace(libpod.PIDNamespace)
+ }
+ if pidMode.IsContainer() {
+ ctr, err := config.Runtime.LookupContainer(pidMode.Container())
+ if err != nil {
+ return errors.Wrapf(err, "container %q not found", pidMode.Container())
+ }
+ pid, err := ctr.PID()
+ if err != nil {
+ return errors.Wrapf(err, "Failed to get pid of container %q", pidMode.Container())
+ }
+ pidNsPath := fmt.Sprintf("/proc/%d/ns/pid", pid)
+ if err := g.AddOrReplaceLinuxNamespace(libpod.PIDNamespace, pidNsPath); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addNetNS(config *createConfig, g *generate.Generator) error {
+ netMode := config.NetMode
+ if netMode.IsHost() {
+ return g.RemoveLinuxNamespace(libpod.NetNamespace)
+ }
+ if netMode.IsNone() {
+ return libpod.ErrNotImplemented
+ }
+ if netMode.IsBridge() {
+ return libpod.ErrNotImplemented
+ }
+ if netMode.IsContainer() {
+ ctr, err := config.Runtime.LookupContainer(netMode.ConnectedContainer())
+ if err != nil {
+ return errors.Wrapf(err, "container %q not found", netMode.ConnectedContainer())
+ }
+ pid, err := ctr.PID()
+ if err != nil {
+ return errors.Wrapf(err, "Failed to get pid of container %q", netMode.ConnectedContainer())
+ }
+ nsPath := fmt.Sprintf("/proc/%d/ns/net", pid)
+ if err := g.AddOrReplaceLinuxNamespace(libpod.NetNamespace, nsPath); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addUTSNS(config *createConfig, g *generate.Generator) error {
+ utsMode := config.UtsMode
+ if utsMode.IsHost() {
+ return g.RemoveLinuxNamespace(libpod.UTSNamespace)
+ }
+ return nil
+}
+
+func addIpcNS(config *createConfig, g *generate.Generator) error {
+ ipcMode := config.IpcMode
+ if ipcMode.IsHost() {
+ return g.RemoveLinuxNamespace(libpod.IPCNamespace)
+ }
+ if ipcMode.IsContainer() {
+ ctr, err := config.Runtime.LookupContainer(ipcMode.Container())
+ if err != nil {
+ return errors.Wrapf(err, "container %q not found", ipcMode.Container())
+ }
+ pid, err := ctr.PID()
+ if err != nil {
+ return errors.Wrapf(err, "Failed to get pid of container %q", ipcMode.Container())
+ }
+ nsPath := fmt.Sprintf("/proc/%d/ns/ipc", pid)
+ if err := g.AddOrReplaceLinuxNamespace(libpod.IPCNamespace, nsPath); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func addRlimits(config *createConfig, g *generate.Generator) error {
+ var (
+ ul *units.Ulimit
+ err error
+ )
+
+ for _, u := range config.Resources.Ulimit {
+ if ul, err = units.ParseUlimit(u); err != nil {
+ return errors.Wrapf(err, "ulimit option %q requires name=SOFT:HARD, failed to be parsed", u)
+ }
+
+ g.AddProcessRlimits("RLIMIT_"+strings.ToUpper(ul.Name), uint64(ul.Soft), uint64(ul.Hard))
+ }
+ return nil
+}
+
+func setupCapabilities(config *createConfig, configSpec *spec.Spec) error {
+ var err error
+ var caplist []string
+ if config.Privileged {
+ caplist = caps.GetAllCapabilities()
+ } else {
+ caplist, err = caps.TweakCapabilities(configSpec.Process.Capabilities.Bounding, config.CapAdd, config.CapDrop)
+ if err != nil {
+ return err
+ }
+ }
+
+ configSpec.Process.Capabilities.Bounding = caplist
+ configSpec.Process.Capabilities.Permitted = caplist
+ configSpec.Process.Capabilities.Inheritable = caplist
+ configSpec.Process.Capabilities.Effective = caplist
+ return nil
+}
+
+// Parses information needed to create a container into an OCI runtime spec
+func createConfigToOCISpec(config *createConfig) (*spec.Spec, error) {
+ g := generate.New()
+ g.AddCgroupsMount("ro")
+ g.SetProcessCwd(config.WorkDir)
+ g.SetProcessArgs(config.Command)
+ g.SetProcessTerminal(config.Tty)
+ // User and Group must go together
+ g.SetProcessUID(config.User)
+ g.SetProcessGID(config.Group)
+ for _, gid := range config.GroupAdd {
+ g.AddProcessAdditionalGid(gid)
+ }
+ for key, val := range config.GetAnnotations() {
+ g.AddAnnotation(key, val)
+ }
+ g.SetRootReadonly(config.ReadOnlyRootfs)
+ g.SetHostname(config.Hostname)
+ if config.Hostname != "" {
+ g.AddProcessEnv("HOSTNAME", config.Hostname)
+ }
+
+ for _, sysctl := range config.Sysctl {
+ s := strings.SplitN(sysctl, "=", 2)
+ g.AddLinuxSysctl(s[0], s[1])
+ }
+
+ // RESOURCES - MEMORY
+ if config.Resources.Memory != 0 {
+ g.SetLinuxResourcesMemoryLimit(config.Resources.Memory)
+ }
+ if config.Resources.MemoryReservation != 0 {
+ g.SetLinuxResourcesMemoryReservation(config.Resources.MemoryReservation)
+ }
+ if config.Resources.MemorySwap != 0 {
+ g.SetLinuxResourcesMemorySwap(config.Resources.MemorySwap)
+ }
+ if config.Resources.KernelMemory != 0 {
+ g.SetLinuxResourcesMemoryKernel(config.Resources.KernelMemory)
+ }
+ if config.Resources.MemorySwappiness != -1 {
+ g.SetLinuxResourcesMemorySwappiness(uint64(config.Resources.MemorySwappiness))
+ }
+ g.SetLinuxResourcesMemoryDisableOOMKiller(config.Resources.DisableOomKiller)
+ g.SetProcessOOMScoreAdj(config.Resources.OomScoreAdj)
+
+ // RESOURCES - CPU
+
+ if config.Resources.CPUShares != 0 {
+ g.SetLinuxResourcesCPUShares(config.Resources.CPUShares)
+ }
+ if config.Resources.CPUQuota != 0 {
+ g.SetLinuxResourcesCPUQuota(config.Resources.CPUQuota)
+ }
+ if config.Resources.CPUPeriod != 0 {
+ g.SetLinuxResourcesCPUPeriod(config.Resources.CPUPeriod)
+ }
+ if config.Resources.CPURtRuntime != 0 {
+ g.SetLinuxResourcesCPURealtimeRuntime(config.Resources.CPURtRuntime)
+ }
+ if config.Resources.CPURtPeriod != 0 {
+ g.SetLinuxResourcesCPURealtimePeriod(config.Resources.CPURtPeriod)
+ }
+ if config.Resources.CPUs != "" {
+ g.SetLinuxResourcesCPUCpus(config.Resources.CPUs)
+ }
+ if config.Resources.CPUsetMems != "" {
+ g.SetLinuxResourcesCPUMems(config.Resources.CPUsetMems)
+ }
+
+ // SECURITY OPTS
+ g.SetProcessNoNewPrivileges(config.NoNewPrivileges)
+ g.SetProcessApparmorProfile(config.ApparmorProfile)
+ g.SetProcessSelinuxLabel(config.ProcessLabel)
+ g.SetLinuxMountLabel(config.MountLabel)
+ blockAccessToKernelFilesystems(config, &g)
+
+ // RESOURCES - PIDS
+ if config.Resources.PidsLimit != 0 {
+ g.SetLinuxResourcesPidsLimit(config.Resources.PidsLimit)
+ }
+
+ for _, i := range config.Tmpfs {
+ options := []string{"rw", "noexec", "nosuid", "nodev", "size=65536k"}
+ spliti := strings.SplitN(i, ":", 2)
+ if len(spliti) > 1 {
+ if _, _, err := mount.ParseTmpfsOptions(spliti[1]); err != nil {
+ return nil, err
+ }
+ options = strings.Split(spliti[1], ",")
+ }
+ // Default options if nothing passed
+ g.AddTmpfsMount(spliti[0], options)
+ }
+
+ for name, val := range config.Env {
+ g.AddProcessEnv(name, val)
+ }
+
+ if err := addRlimits(config, &g); err != nil {
+ return nil, err
+ }
+
+ if err := addPidNS(config, &g); err != nil {
+ return nil, err
+ }
+
+ if err := addNetNS(config, &g); err != nil {
+ return nil, err
+ }
+
+ if err := addUTSNS(config, &g); err != nil {
+ return nil, err
+ }
+
+ if err := addIpcNS(config, &g); err != nil {
+ return nil, err
+ }
+ configSpec := g.Spec()
+
+ if config.SeccompProfilePath != "" && config.SeccompProfilePath != "unconfined" {
+ seccompProfile, err := ioutil.ReadFile(config.SeccompProfilePath)
+ if err != nil {
+ return nil, errors.Wrapf(err, "opening seccomp profile (%s) failed", config.SeccompProfilePath)
+ }
+ var seccompConfig spec.LinuxSeccomp
+ if err := json.Unmarshal(seccompProfile, &seccompConfig); err != nil {
+ return nil, errors.Wrapf(err, "decoding seccomp profile (%s) failed", config.SeccompProfilePath)
+ }
+ configSpec.Linux.Seccomp = &seccompConfig
+ }
+
+ // BIND MOUNTS
+ mounts, err := config.GetVolumeMounts()
+ if err != nil {
+ return nil, errors.Wrapf(err, "error getting volume mounts")
+ }
+ configSpec.Mounts = append(configSpec.Mounts, mounts...)
+ for _, mount := range configSpec.Mounts {
+ for _, opt := range mount.Options {
+ switch opt {
+ case "private", "rprivate", "slave", "rslave", "shared", "rshared":
+ if err := g.SetLinuxRootPropagation(opt); err != nil {
+ return nil, errors.Wrapf(err, "error setting root propagation for %q", mount.Destination)
+ }
+ }
+ }
+ }
+
+ // HANDLE CAPABILITIES
+ if err := setupCapabilities(config, configSpec); err != nil {
+ return nil, err
+ }
+
+ /*
+ Hooks: &configSpec.Hooks{},
+ //Annotations
+ Resources: &configSpec.LinuxResources{
+ Devices: config.GetDefaultDevices(),
+ BlockIO: &blkio,
+ //HugepageLimits:
+ Network: &configSpec.LinuxNetwork{
+ // ClassID *uint32
+ // Priorites []LinuxInterfacePriority
+ },
+ },
+ //CgroupsPath:
+ //Namespaces: []LinuxNamespace
+ //Devices
+ // DefaultAction:
+ // Architectures
+ // Syscalls:
+ },
+ // RootfsPropagation
+ // MaskedPaths
+ // ReadonlyPaths:
+ // IntelRdt
+ },
+ }
+ */
+ return configSpec, nil
+}
+
+func (c *createConfig) CreateBlockIO() (spec.LinuxBlockIO, error) {
+ bio := spec.LinuxBlockIO{}
+ bio.Weight = &c.Resources.BlkioWeight
+ if len(c.Resources.BlkioWeightDevice) > 0 {
+ var lwds []spec.LinuxWeightDevice
+ for _, i := range c.Resources.BlkioWeightDevice {
+ wd, err := validateweightDevice(i)
+ if err != nil {
+ return bio, errors.Wrapf(err, "invalid values for blkio-weight-device")
+ }
+ wdStat := getStatFromPath(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)
+ }
+ }
+ if len(c.Resources.DeviceReadBps) > 0 {
+ readBps, err := makeThrottleArray(c.Resources.DeviceReadBps)
+ if err != nil {
+ return bio, err
+ }
+ bio.ThrottleReadBpsDevice = readBps
+ }
+ if len(c.Resources.DeviceWriteBps) > 0 {
+ writeBpds, err := makeThrottleArray(c.Resources.DeviceWriteBps)
+ if err != nil {
+ return bio, err
+ }
+ bio.ThrottleWriteBpsDevice = writeBpds
+ }
+ if len(c.Resources.DeviceReadIOps) > 0 {
+ readIOps, err := makeThrottleArray(c.Resources.DeviceReadIOps)
+ if err != nil {
+ return bio, err
+ }
+ bio.ThrottleReadIOPSDevice = readIOps
+ }
+ if len(c.Resources.DeviceWriteIOps) > 0 {
+ writeIOps, err := makeThrottleArray(c.Resources.DeviceWriteIOps)
+ if err != nil {
+ return bio, err
+ }
+ bio.ThrottleWriteIOPSDevice = writeIOps
+ }
+
+ return bio, nil
+}
+
+// GetAnnotations returns the all the annotations for the container
+func (c *createConfig) GetAnnotations() map[string]string {
+ a := getDefaultAnnotations()
+ // TODO - Which annotations do we want added by default
+ // TODO - This should be added to the DB long term
+ if c.Tty {
+ a["io.kubernetes.cri-o.TTY"] = "true"
+ }
+ return a
+}
+
+func getDefaultAnnotations() map[string]string {
+ var annotations map[string]string
+ annotations = make(map[string]string)
+ annotations[ann.Annotations] = ""
+ annotations[ann.ContainerID] = ""
+ annotations[ann.ContainerName] = ""
+ annotations[ann.ContainerType] = ""
+ annotations[ann.Created] = ""
+ annotations[ann.HostName] = ""
+ annotations[ann.IP] = ""
+ annotations[ann.Image] = ""
+ annotations[ann.ImageName] = ""
+ annotations[ann.ImageRef] = ""
+ annotations[ann.KubeName] = ""
+ annotations[ann.Labels] = ""
+ annotations[ann.LogPath] = ""
+ annotations[ann.Metadata] = ""
+ annotations[ann.Name] = ""
+ annotations[ann.PrivilegedRuntime] = ""
+ annotations[ann.ResolvPath] = ""
+ annotations[ann.HostnamePath] = ""
+ annotations[ann.SandboxID] = ""
+ annotations[ann.SandboxName] = ""
+ annotations[ann.ShmPath] = ""
+ annotations[ann.MountPoint] = ""
+ annotations[ann.TrustedSandbox] = ""
+ annotations[ann.TTY] = "false"
+ annotations[ann.Stdin] = ""
+ annotations[ann.StdinOnce] = ""
+ annotations[ann.Volumes] = ""
+
+ return annotations
+}
+
+//GetVolumeMounts takes user provided input for bind mounts and creates Mount structs
+func (c *createConfig) GetVolumeMounts() ([]spec.Mount, error) {
+ var m []spec.Mount
+ var options []string
+ for _, i := range c.Volumes {
+ // We need to handle SELinux options better here, specifically :Z
+ spliti := strings.Split(i, ":")
+ if len(spliti) > 2 {
+ options = strings.Split(spliti[2], ",")
+ }
+ options = append(options, "rbind")
+ var foundrw, foundro, foundz, foundZ bool
+ var rootProp string
+ for _, opt := range options {
+ switch opt {
+ case "rw":
+ foundrw = true
+ case "ro":
+ foundro = true
+ case "z":
+ foundz = true
+ case "Z":
+ foundZ = true
+ case "private", "rprivate", "slave", "rslave", "shared", "rshared":
+ rootProp = opt
+ }
+ }
+ if !foundrw && !foundro {
+ options = append(options, "rw")
+ }
+ if foundz {
+ if err := label.Relabel(spliti[0], c.MountLabel, true); err != nil {
+ return nil, errors.Wrapf(err, "relabel failed %q", spliti[0])
+ }
+ }
+ if foundZ {
+ if err := label.Relabel(spliti[0], c.MountLabel, false); err != nil {
+ return nil, errors.Wrapf(err, "relabel failed %q", spliti[0])
+ }
+ }
+ if rootProp == "" {
+ options = append(options, "rprivate")
+ }
+
+ m = append(m, spec.Mount{
+ Destination: spliti[1],
+ Type: string(TypeBind),
+ Source: spliti[0],
+ Options: options,
+ })
+ }
+ return m, nil
+}
+
+//GetTmpfsMounts takes user provided input for Tmpfs mounts and creates Mount structs
+func (c *createConfig) GetTmpfsMounts() []spec.Mount {
+ var m []spec.Mount
+ for _, i := range c.Tmpfs {
+ // Default options if nothing passed
+ options := []string{"rw", "noexec", "nosuid", "nodev", "size=65536k"}
+ spliti := strings.Split(i, ":")
+ destPath := spliti[0]
+ if len(spliti) > 1 {
+ options = strings.Split(spliti[1], ",")
+ }
+ m = append(m, spec.Mount{
+ Destination: destPath,
+ Type: string(TypeTmpfs),
+ Options: options,
+ Source: string(TypeTmpfs),
+ })
+ }
+ return m
+}
+
+func (c *createConfig) GetContainerCreateOptions() ([]libpod.CtrCreateOption, error) {
+ var options []libpod.CtrCreateOption
+
+ // Uncomment after talking to mheon about unimplemented funcs
+ // options = append(options, libpod.WithLabels(c.labels))
+
+ if c.Interactive {
+ options = append(options, libpod.WithStdin())
+ }
+ if c.Name != "" {
+ logrus.Debugf("appending name %s", c.Name)
+ options = append(options, libpod.WithName(c.Name))
+ }
+ // TODO parse ports into libpod format and include
+ // TODO should not happen if --net=host
+ options = append(options, libpod.WithNetNS([]ocicni.PortMapping{}))
+
+ return options, nil
+}
+
+func getStatFromPath(path string) unix.Stat_t {
+ s := unix.Stat_t{}
+ _ = unix.Stat(path, &s)
+ return s
+}
+
+func makeThrottleArray(throttleInput []string) ([]spec.LinuxThrottleDevice, error) {
+ var ltds []spec.LinuxThrottleDevice
+ for _, i := range throttleInput {
+ t, err := validateBpsDevice(i)
+ if err != nil {
+ return []spec.LinuxThrottleDevice{}, err
+ }
+ ltd := spec.LinuxThrottleDevice{}
+ ltd.Rate = t.rate
+ ltdStat := getStatFromPath(t.path)
+ ltd.Major = int64(unix.Major(ltdStat.Rdev))
+ ltd.Minor = int64(unix.Major(ltdStat.Rdev))
+ ltds = append(ltds, ltd)
+ }
+ return ltds, nil
+}
diff --git a/cmd/podman/spec_test.go b/cmd/podman/spec_test.go
new file mode 100644
index 000000000..01e1a4ad3
--- /dev/null
+++ b/cmd/podman/spec_test.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "reflect"
+ "testing"
+
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateConfig_GetVolumeMounts(t *testing.T) {
+ data := spec.Mount{
+ Destination: "/foobar",
+ Type: "bind",
+ Source: "foobar",
+ Options: []string{"ro", "rbind", "rprivate"},
+ }
+ config := createConfig{
+ Volumes: []string{"foobar:/foobar:ro"},
+ }
+ specMount, err := config.GetVolumeMounts()
+ assert.NoError(t, err)
+ assert.True(t, reflect.DeepEqual(data, specMount[0]))
+}
+
+func TestCreateConfig_GetTmpfsMounts(t *testing.T) {
+ data := spec.Mount{
+ Destination: "/homer",
+ Type: "tmpfs",
+ Source: "tmpfs",
+ Options: []string{"rw", "size=787448k", "mode=1777"},
+ }
+ config := createConfig{
+ Tmpfs: []string{"/homer:rw,size=787448k,mode=1777"},
+ }
+ tmpfsMount := config.GetTmpfsMounts()
+ assert.True(t, reflect.DeepEqual(data, tmpfsMount[0]))
+
+}
diff --git a/cmd/podman/start.go b/cmd/podman/start.go
new file mode 100644
index 000000000..33bc354bb
--- /dev/null
+++ b/cmd/podman/start.go
@@ -0,0 +1,131 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "sync"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var (
+ startFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "attach, a",
+ Usage: "Attach container's STDOUT and STDERR",
+ },
+ cli.StringFlag{
+ Name: "detach-keys",
+ Usage: "Override the key sequence for detaching a container. Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _.",
+ },
+ cli.BoolFlag{
+ Name: "interactive, i",
+ Usage: "Keep STDIN open even if not attached",
+ },
+ }
+ startDescription = `
+ podman start
+
+ Starts one or more containers. The container name or ID can be used.
+`
+
+ startCommand = cli.Command{
+ Name: "start",
+ Usage: "Start one or more containers",
+ Description: startDescription,
+ Flags: startFlags,
+ Action: startCmd,
+ ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
+ UseShortOptionHandling: true,
+ }
+)
+
+func startCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) < 1 {
+ return errors.Errorf("you must provide at least one container name or id")
+ }
+
+ attach := c.Bool("attach")
+
+ if len(args) > 1 && attach {
+ return errors.Errorf("you cannot start and attach multiple containers at once")
+ }
+
+ if err := validateFlags(c, startFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ var lastError error
+ for _, container := range args {
+ // Create a bool channel to track that the console socket attach
+ // is successful.
+ attached := make(chan bool)
+ // Create a waitgroup so we can sync and wait for all goroutines
+ // to finish before exiting main
+ var wg sync.WaitGroup
+
+ ctr, err := runtime.LookupContainer(container)
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "unable to find container %s", container)
+ continue
+ }
+
+ if err := ctr.Init(); err != nil && errors.Cause(err) != libpod.ErrCtrExists {
+ return err
+ }
+
+ // We can only be interactive if both the config and the command-line say so
+ if c.Bool("interactive") && !ctr.Config().Stdin {
+ return errors.Errorf("the container was not created with the interactive option")
+ }
+ noStdIn := c.Bool("interactive")
+ tty, err := strconv.ParseBool(ctr.Spec().Annotations["io.kubernetes.cri-o.TTY"])
+ if err != nil {
+ return errors.Wrapf(err, "unable to parse annotations in %s", ctr.ID())
+ }
+ // We only get a terminal session if both a tty was specified in the spec and
+ // -a on the command-line was given.
+ if attach && tty {
+ // We increment the wg counter because we need to do the attach
+ wg.Add(1)
+ // Attach to the running container
+ go func() {
+ logrus.Debugf("trying to attach to the container %s", ctr.ID())
+ defer wg.Done()
+ if err := ctr.Attach(noStdIn, c.String("detach-keys"), attached); err != nil {
+ logrus.Errorf("unable to attach to container %s: %q", ctr.ID(), err)
+ }
+ }()
+ if !<-attached {
+ return errors.Errorf("unable to attach to container %s", ctr.ID())
+ }
+ }
+ err = ctr.Start()
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "unable to start %s", container)
+ continue
+ }
+ if !attach {
+ fmt.Println(ctr.ID())
+ }
+ wg.Wait()
+ }
+ return lastError
+}
diff --git a/cmd/podman/stats.go b/cmd/podman/stats.go
new file mode 100644
index 000000000..cf54a8bfe
--- /dev/null
+++ b/cmd/podman/stats.go
@@ -0,0 +1,226 @@
+package main
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+ "time"
+
+ tm "github.com/buger/goterm"
+ "github.com/docker/go-units"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+type statsOutputParams struct {
+ Container string `json:"name"`
+ ID string `json:"id"`
+ CPUPerc string `json:"cpu_percent"`
+ MemUsage string `json:"mem_usage"`
+ MemPerc string `json:"mem_percent"`
+ NetIO string `json:"netio"`
+ BlockIO string `json:"blocki"`
+ PIDS uint64 `json:"pids"`
+}
+
+var (
+ statsFlags = []cli.Flag{
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "show all containers. Only running containers are shown by default. The default is false",
+ },
+ cli.BoolFlag{
+ Name: "no-stream",
+ Usage: "disable streaming stats and only pull the first result, default setting is false",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "pretty-print container statistics using a Go template",
+ },
+ cli.BoolFlag{
+ Name: "no-reset",
+ Usage: "disable resetting the screen between intervals",
+ },
+ }
+
+ statsDescription = "display a live stream of one or more containers' resource usage statistics"
+ statsCommand = cli.Command{
+ Name: "stats",
+ Usage: "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers",
+ Description: statsDescription,
+ Flags: statsFlags,
+ Action: statsCmd,
+ ArgsUsage: "",
+ }
+)
+
+func statsCmd(c *cli.Context) error {
+ if err := validateFlags(c, statsFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ times := -1
+ if c.Bool("no-stream") {
+ times = 1
+ }
+
+ var format string
+ var ctrs []*libpod.Container
+ var containerFunc func() ([]*libpod.Container, error)
+ all := c.Bool("all")
+
+ if c.IsSet("format") {
+ format = c.String("format")
+ } else {
+ format = genStatsFormat()
+ }
+
+ if len(c.Args()) > 0 {
+ containerFunc = func() ([]*libpod.Container, error) { return runtime.GetContainersByList(c.Args()) }
+ } else if all {
+ containerFunc = runtime.GetAllContainers
+ } else {
+ containerFunc = runtime.GetRunningContainers
+ }
+
+ ctrs, err = containerFunc()
+ if err != nil {
+ return errors.Wrapf(err, "unable to get list of containers")
+ }
+
+ containerStats := map[string]*libpod.ContainerStats{}
+ for _, ctr := range ctrs {
+ initialStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
+ if err != nil {
+ return err
+ }
+ containerStats[ctr.ID()] = initialStats
+ }
+ step := 1
+ if times == -1 {
+ times = 1
+ step = 0
+ }
+ for i := 0; i < times; i += step {
+ reportStats := []*libpod.ContainerStats{}
+ for _, ctr := range ctrs {
+ id := ctr.ID()
+ if _, ok := containerStats[ctr.ID()]; !ok {
+ initialStats, err := ctr.GetContainerStats(&libpod.ContainerStats{})
+ if err != nil {
+ return err
+ }
+ containerStats[id] = initialStats
+ }
+ stats, err := ctr.GetContainerStats(containerStats[id])
+ if err != nil {
+ return err
+ }
+ // replace the previous measurement with the current one
+ containerStats[id] = stats
+ reportStats = append(reportStats, stats)
+ }
+ ctrs, err = containerFunc()
+ if err != nil {
+ return err
+ }
+ if strings.ToLower(format) != formats.JSONString && !c.Bool("no-reset") {
+ tm.Clear()
+ tm.MoveCursor(1, 1)
+ tm.Flush()
+ }
+ outputStats(reportStats, format)
+ time.Sleep(time.Second)
+ }
+ return nil
+}
+
+func outputStats(stats []*libpod.ContainerStats, format string) error {
+ var out formats.Writer
+ var outputStats []statsOutputParams
+ for _, s := range stats {
+ outputStats = append(outputStats, getStatsOutputParams(s))
+ }
+ if len(outputStats) == 0 {
+ return nil
+ }
+ if strings.ToLower(format) == formats.JSONString {
+ out = formats.JSONStructArray{Output: statsToGeneric(outputStats, []statsOutputParams{})}
+ } else {
+ out = formats.StdoutTemplateArray{Output: statsToGeneric(outputStats, []statsOutputParams{}), Template: format, Fields: outputStats[0].headerMap()}
+ }
+ return formats.Writer(out).Out()
+}
+
+func genStatsFormat() (format string) {
+ return "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}"
+}
+
+// imagesToGeneric creates an empty array of interfaces for output
+func statsToGeneric(templParams []statsOutputParams, JSONParams []statsOutputParams) (genericParams []interface{}) {
+ if len(templParams) > 0 {
+ for _, v := range templParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+ }
+ for _, v := range JSONParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+}
+
+// generate the header based on the template provided
+func (i *statsOutputParams) headerMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(i))
+ values := make(map[string]string)
+
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ switch value {
+ case "CPUPerc":
+ value = "CPU%"
+ case "MemUsage":
+ value = "MemUsage/Limit"
+ case "MemPerc":
+ value = "Mem%"
+ }
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+func combineHumanValues(a, b uint64) string {
+ return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b)))
+}
+
+func floatToPercentString(f float64) string {
+ strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f)
+ if err != nil {
+ // If things go bazinga, return a safe value
+ return "0.00 %"
+ }
+ return fmt.Sprintf("%.2f", strippedFloat) + "%"
+}
+
+func getStatsOutputParams(stats *libpod.ContainerStats) statsOutputParams {
+ return statsOutputParams{
+ Container: stats.ContainerID[:12],
+ ID: stats.ContainerID,
+ CPUPerc: floatToPercentString(stats.CPU),
+ MemUsage: combineHumanValues(stats.MemUsage, stats.MemLimit),
+ MemPerc: floatToPercentString(stats.MemPerc),
+ NetIO: combineHumanValues(stats.NetInput, stats.NetOutput),
+ BlockIO: combineHumanValues(stats.BlockInput, stats.BlockOutput),
+ PIDS: stats.PIDs,
+ }
+}
diff --git a/cmd/podman/stop.go b/cmd/podman/stop.go
new file mode 100644
index 000000000..3b1ffbba5
--- /dev/null
+++ b/cmd/podman/stop.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+var (
+ defaultTimeout int64 = 10
+ stopFlags = []cli.Flag{
+ cli.Int64Flag{
+ Name: "timeout, t",
+ Usage: "Seconds to wait for stop before killing the container",
+ Value: defaultTimeout,
+ },
+ cli.BoolFlag{
+ Name: "all, a",
+ Usage: "stop all running containers",
+ },
+ }
+ stopDescription = `
+ podman stop
+
+ Stops one or more running containers. The container name or ID can be used.
+ A timeout to forcibly stop the container can also be set but defaults to 10
+ seconds otherwise.
+`
+
+ stopCommand = cli.Command{
+ Name: "stop",
+ Usage: "Stop one or more containers",
+ Description: stopDescription,
+ Flags: stopFlags,
+ Action: stopCmd,
+ ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
+ }
+)
+
+func stopCmd(c *cli.Context) error {
+ args := c.Args()
+ stopTimeout := c.Int64("timeout")
+ if c.Bool("all") && len(args) > 0 {
+ return errors.Errorf("no arguments are needed with -a")
+ }
+ if len(args) < 1 && !c.Bool("all") {
+ return errors.Errorf("you must provide at least one container name or id")
+ }
+ if err := validateFlags(c, stopFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ logrus.Debugf("Stopping containers with timeout %d", stopTimeout)
+
+ var filterFuncs []libpod.ContainerFilter
+ var containers []*libpod.Container
+ var lastError error
+
+ if c.Bool("all") {
+ // only get running containers
+ filterFuncs = append(filterFuncs, func(c *libpod.Container) bool {
+ state, _ := c.State()
+ return state == libpod.ContainerStateRunning
+ })
+ containers, err = runtime.GetContainers(filterFuncs...)
+ if err != nil {
+ return errors.Wrapf(err, "unable to get running containers")
+ }
+ } else {
+ for _, i := range args {
+ container, err := runtime.LookupContainer(i)
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "unable to find container %s", i)
+ continue
+ }
+ containers = append(containers, container)
+ }
+ }
+
+ for _, ctr := range containers {
+ if err := ctr.Stop(stopTimeout); err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to stop container %v", ctr.ID())
+ } else {
+ fmt.Println(ctr.ID())
+ }
+ }
+ return lastError
+}
diff --git a/cmd/podman/tag.go b/cmd/podman/tag.go
new file mode 100644
index 000000000..f29c8c182
--- /dev/null
+++ b/cmd/podman/tag.go
@@ -0,0 +1,77 @@
+package main
+
+import (
+ "github.com/containers/image/docker/reference"
+ "github.com/containers/storage"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ tagDescription = "Adds one or more additional names to locally-stored image"
+ tagCommand = cli.Command{
+ Name: "tag",
+ Usage: "Add an additional name to a local image",
+ Description: tagDescription,
+ Action: tagCmd,
+ ArgsUsage: "IMAGE-NAME [IMAGE-NAME ...]",
+ }
+)
+
+func tagCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) < 2 {
+ return errors.Errorf("image name and at least one new name must be specified")
+ }
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not create runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ img, err := runtime.GetImage(args[0])
+ if err != nil {
+ return err
+ }
+ if img == nil {
+ return errors.New("null image")
+ }
+ err = addImageNames(runtime, img, args[1:])
+ if err != nil {
+ return errors.Wrapf(err, "error adding names %v to image %q", args[1:], args[0])
+ }
+ return nil
+}
+
+func addImageNames(runtime *libpod.Runtime, image *storage.Image, addNames []string) error {
+ // Add tags to the names if applicable
+ names, err := expandedTags(addNames)
+ if err != nil {
+ return err
+ }
+ for _, name := range names {
+ if err := runtime.TagImage(image, name); err != nil {
+ return errors.Wrapf(err, "error adding name (%v) to image %q", name, image.ID)
+ }
+ }
+ return nil
+}
+
+func expandedTags(tags []string) ([]string, error) {
+ expandedNames := []string{}
+ for _, tag := range tags {
+ var labelName string
+ name, err := reference.Parse(tag)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error parsing tag %q", name)
+ }
+ if _, ok := name.(reference.NamedTagged); ok {
+ labelName = name.String()
+ } else {
+ labelName = name.String() + ":latest"
+ }
+ expandedNames = append(expandedNames, labelName)
+ }
+ return expandedNames, nil
+}
diff --git a/cmd/podman/top.go b/cmd/podman/top.go
new file mode 100644
index 000000000..796b31c1d
--- /dev/null
+++ b/cmd/podman/top.go
@@ -0,0 +1,258 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/urfave/cli"
+)
+
+var (
+ topFlags = []cli.Flag{
+ cli.StringFlag{
+ Name: "format",
+ Usage: "Change the output to JSON",
+ },
+ }
+ topDescription = `
+ podman top
+
+ Display the running processes of the container.
+`
+
+ topCommand = cli.Command{
+ Name: "top",
+ Usage: "Display the running processes of a container",
+ Description: topDescription,
+ Flags: topFlags,
+ Action: topCmd,
+ ArgsUsage: "CONTAINER-NAME",
+ SkipArgReorder: true,
+ }
+)
+
+func topCmd(c *cli.Context) error {
+ doJSON := false
+ if c.IsSet("format") {
+ if strings.ToUpper(c.String("format")) == "JSON" {
+ doJSON = true
+ } else {
+ return errors.Errorf("only 'json' is supported for a format option")
+ }
+ }
+ args := c.Args()
+ var psArgs []string
+ psOpts := []string{"-o", "uid,pid,ppid,c,stime,tname,time,cmd"}
+ if len(args) < 1 {
+ return errors.Errorf("you must provide the name or id of a running container")
+ }
+ if err := validateFlags(c, topFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+ if len(args) > 1 {
+ psOpts = args[1:]
+ }
+
+ container, err := runtime.LookupContainer(args[0])
+ if err != nil {
+ return errors.Wrapf(err, "unable to lookup %s", args[0])
+ }
+ conStat, err := container.State()
+ if err != nil {
+ return errors.Wrapf(err, "unable to look up state for %s", args[0])
+ }
+ if conStat != libpod.ContainerStateRunning {
+ return errors.Errorf("top can only be used on running containers")
+ }
+
+ psArgs = append(psArgs, psOpts...)
+
+ results, err := container.GetContainerPidInformation(psArgs)
+ if err != nil {
+ return err
+ }
+ headers := getHeaders(results[0])
+ format := genTopFormat(headers)
+ var out formats.Writer
+ psParams, err := psDataToPSParams(results[1:], headers)
+ if err != nil {
+ return errors.Wrap(err, "unable to convert ps data to proper structure")
+ }
+ if doJSON {
+ out = formats.JSONStructArray{Output: topToGeneric(psParams)}
+ } else {
+ out = formats.StdoutTemplateArray{Output: topToGeneric(psParams), Template: format, Fields: createTopHeaderMap(headers)}
+ }
+ formats.Writer(out).Out()
+ return nil
+}
+
+func getHeaders(s string) []string {
+ var headers []string
+ tmpHeaders := strings.Fields(s)
+ for _, header := range tmpHeaders {
+ headers = append(headers, strings.Replace(header, "%", "", -1))
+ }
+ return headers
+}
+
+func genTopFormat(headers []string) string {
+ format := "table "
+ for _, header := range headers {
+ format = fmt.Sprintf("%s{{.%s}}\t", format, header)
+ }
+ return format
+}
+
+// imagesToGeneric creates an empty array of interfaces for output
+func topToGeneric(templParams []PSParams) (genericParams []interface{}) {
+ for _, v := range templParams {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return
+}
+
+// generate the header based on the template provided
+func createTopHeaderMap(v []string) map[string]string {
+ values := make(map[string]string)
+ for _, key := range v {
+ value := key
+ if value == "CPU" {
+ value = "%CPU"
+ } else if value == "MEM" {
+ value = "%MEM"
+ }
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+// PSDataToParams converts a string array of data and its headers to an
+// arra if PSParams
+func psDataToPSParams(data []string, headers []string) ([]PSParams, error) {
+ var params []PSParams
+ for _, line := range data {
+ tmpMap := make(map[string]string)
+ tmpArray := strings.Fields(line)
+ if len(tmpArray) == 0 {
+ continue
+ }
+ for index, v := range tmpArray {
+ header := headers[index]
+ tmpMap[header] = v
+ }
+ jsonData, _ := json.Marshal(tmpMap)
+ var r PSParams
+ err := json.Unmarshal(jsonData, &r)
+ if err != nil {
+ return []PSParams{}, err
+ }
+ params = append(params, r)
+ }
+ return params, nil
+}
+
+//PSParams is a list of options that the command line ps recognizes
+type PSParams struct {
+ CPU string
+ MEM string
+ COMMAND string
+ BLOCKED string
+ START string
+ TIME string
+ C string
+ CAUGHT string
+ CGROUP string
+ CLSCLS string
+ CLS string
+ CMD string
+ CP string
+ DRS string
+ EGID string
+ EGROUP string
+ EIP string
+ ESP string
+ ELAPSED string
+ EUIDE string
+ USER string
+ F string
+ FGID string
+ FGROUP string
+ FUID string
+ FUSER string
+ GID string
+ GROUP string
+ IGNORED string
+ IPCNS string
+ LABEL string
+ STARTED string
+ SESSION string
+ LWP string
+ MACHINE string
+ MAJFLT string
+ MINFLT string
+ MNTNS string
+ NETNS string
+ NI string
+ NLWP string
+ OWNER string
+ PENDING string
+ PGID string
+ PGRP string
+ PID string
+ PIDNS string
+ POL string
+ PPID string
+ PRI string
+ PSR string
+ RGID string
+ RGROUP string
+ RSS string
+ RSZ string
+ RTPRIO string
+ RUID string
+ RUSER string
+ S string
+ SCH string
+ SEAT string
+ SESS string
+ P string
+ SGID string
+ SGROUP string
+ SID string
+ SIZE string
+ SLICE string
+ SPID string
+ STACKP string
+ STIME string
+ SUID string
+ SUPGID string
+ SUPGRP string
+ SUSER string
+ SVGID string
+ SZ string
+ TGID string
+ THCNT string
+ TID string
+ TTY string
+ TPGID string
+ TRS string
+ TT string
+ UID string
+ UNIT string
+ USERNS string
+ UTSNS string
+ UUNIT string
+ VSZ string
+ WCHAN string
+}
diff --git a/cmd/podman/umount.go b/cmd/podman/umount.go
new file mode 100644
index 000000000..4b6aba99e
--- /dev/null
+++ b/cmd/podman/umount.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+var (
+ umountCommand = cli.Command{
+ Name: "umount",
+ Aliases: []string{"unmount"},
+ Usage: "Unmount a working container's root filesystem",
+ Description: "Unmounts a working container's root filesystem",
+ Action: umountCmd,
+ ArgsUsage: "CONTAINER-NAME-OR-ID",
+ }
+)
+
+func umountCmd(c *cli.Context) error {
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) == 0 {
+ return errors.Errorf("container ID must be specified")
+ }
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments specified")
+ }
+
+ ctr, err := runtime.LookupContainer(args[0])
+ if err != nil {
+ return errors.Wrapf(err, "error looking up container %q", args[0])
+ }
+
+ return ctr.Unmount()
+}
diff --git a/cmd/podman/unpause.go b/cmd/podman/unpause.go
new file mode 100644
index 000000000..a7ef65f85
--- /dev/null
+++ b/cmd/podman/unpause.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+var (
+ unpauseDescription = `
+ podman unpause
+
+ Unpauses one or more running containers. The container name or ID can be used.
+`
+ unpauseCommand = cli.Command{
+ Name: "unpause",
+ Usage: "Unpause the processes in one or more containers",
+ Description: unpauseDescription,
+ Action: unpauseCmd,
+ ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
+ }
+)
+
+func unpauseCmd(c *cli.Context) error {
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ args := c.Args()
+ if len(args) < 1 {
+ return errors.Errorf("you must provide at least one container name or id")
+ }
+
+ var lastError error
+ for _, arg := range args {
+ ctr, err := runtime.LookupContainer(arg)
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "error looking up container %q", arg)
+ continue
+ }
+ if err = ctr.Unpause(); err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to unpause container %v", ctr.ID())
+ } else {
+ fmt.Println(ctr.ID())
+ }
+ }
+ return lastError
+}
diff --git a/cmd/podman/user.go b/cmd/podman/user.go
new file mode 100644
index 000000000..3e2e308c5
--- /dev/null
+++ b/cmd/podman/user.go
@@ -0,0 +1,121 @@
+package main
+
+// #include <sys/types.h>
+// #include <grp.h>
+// #include <pwd.h>
+// #include <stdlib.h>
+// #include <stdio.h>
+// #include <string.h>
+// typedef FILE * pFILE;
+import "C"
+
+import (
+ "fmt"
+ "os/user"
+ "path/filepath"
+ "sync"
+ "syscall"
+ "unsafe"
+
+ "github.com/pkg/errors"
+)
+
+func fopenContainerFile(rootdir, filename string) (C.pFILE, error) {
+ var st, lst syscall.Stat_t
+
+ ctrfile := filepath.Join(rootdir, filename)
+ cctrfile := C.CString(ctrfile)
+ defer C.free(unsafe.Pointer(cctrfile))
+ mode := C.CString("r")
+ defer C.free(unsafe.Pointer(mode))
+ f, err := C.fopen(cctrfile, mode)
+ if f == nil || err != nil {
+ return nil, errors.Wrapf(err, "error opening %q", ctrfile)
+ }
+ if err = syscall.Fstat(int(C.fileno(f)), &st); err != nil {
+ return nil, errors.Wrapf(err, "fstat(%q)", ctrfile)
+ }
+ if err = syscall.Lstat(ctrfile, &lst); err != nil {
+ return nil, errors.Wrapf(err, "lstat(%q)", ctrfile)
+ }
+ if st.Dev != lst.Dev || st.Ino != lst.Ino {
+ return nil, errors.Errorf("%q is not a regular file", ctrfile)
+ }
+ return f, nil
+}
+
+var (
+ lookupUser, lookupGroup sync.Mutex
+)
+
+func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) {
+ name := C.CString(username)
+ defer C.free(unsafe.Pointer(name))
+
+ f, err := fopenContainerFile(rootdir, "/etc/passwd")
+ if err != nil {
+ return 0, 0, err
+ }
+ defer C.fclose(f)
+
+ lookupUser.Lock()
+ defer lookupUser.Unlock()
+
+ pwd := C.fgetpwent(f)
+ for pwd != nil {
+ if C.strcmp(pwd.pw_name, name) != 0 {
+ pwd = C.fgetpwent(f)
+ continue
+ }
+ return uint64(pwd.pw_uid), uint64(pwd.pw_gid), nil
+ }
+
+ return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username))
+}
+
+func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) {
+ f, err := fopenContainerFile(rootdir, "/etc/passwd")
+ if err != nil {
+ return "", 0, err
+ }
+ defer C.fclose(f)
+
+ lookupUser.Lock()
+ defer lookupUser.Unlock()
+
+ pwd := C.fgetpwent(f)
+ for pwd != nil {
+ if uint64(pwd.pw_uid) != userid {
+ pwd = C.fgetpwent(f)
+ continue
+ }
+ return C.GoString(pwd.pw_name), uint64(pwd.pw_gid), nil
+ }
+
+ return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up user with UID %d", userid))
+}
+
+func lookupGroupInContainer(rootdir, groupname string) (uint64, error) {
+ name := C.CString(groupname)
+ defer C.free(unsafe.Pointer(name))
+
+ f, err := fopenContainerFile(rootdir, "/etc/group")
+ if err != nil {
+ return 0, err
+ }
+ defer C.fclose(f)
+
+ lookupGroup.Lock()
+ defer lookupGroup.Unlock()
+
+ grp := C.fgetgrent(f)
+ for grp != nil {
+ if C.strcmp(grp.gr_name, name) != 0 {
+ grp = C.fgetgrent(f)
+ continue
+ }
+ return uint64(grp.gr_gid), nil
+ }
+
+ return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname))
+}
diff --git a/cmd/podman/version.go b/cmd/podman/version.go
new file mode 100644
index 000000000..be9b406e7
--- /dev/null
+++ b/cmd/podman/version.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "fmt"
+ "runtime"
+ "strconv"
+ "time"
+
+ "github.com/urfave/cli"
+)
+
+// Overwritten at build time
+var (
+ // gitCommit is the commit that the binary is being built from.
+ // It will be populated by the Makefile.
+ gitCommit string
+ // buildInfo is the time at which the binary was built
+ // It will be populated by the Makefile.
+ buildInfo string
+)
+
+// versionCmd gets and prints version info for version command
+func versionCmd(c *cli.Context) error {
+ fmt.Println("Version: ", c.App.Version)
+ fmt.Println("Go Version: ", runtime.Version())
+ if gitCommit != "" {
+ fmt.Println("Git Commit: ", gitCommit)
+ }
+ if buildInfo != "" {
+ // Converts unix time from string to int64
+ buildTime, err := strconv.ParseInt(buildInfo, 10, 64)
+ if err != nil {
+ return err
+ }
+ // Prints out the build time in readable format
+ fmt.Println("Built: ", time.Unix(buildTime, 0).Format(time.ANSIC))
+ }
+ fmt.Println("OS/Arch: ", runtime.GOOS+"/"+runtime.GOARCH)
+
+ return nil
+}
+
+// Cli command to print out the full version of podman
+var versionCommand = cli.Command{
+ Name: "version",
+ Usage: "Display the PODMAN Version Information",
+ Action: versionCmd,
+}
diff --git a/cmd/podman/wait.go b/cmd/podman/wait.go
new file mode 100644
index 000000000..27cfecac9
--- /dev/null
+++ b/cmd/podman/wait.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+var (
+ waitDescription = `
+ podman wait
+
+ Block until one or more containers stop and then print their exit codes
+`
+
+ waitCommand = cli.Command{
+ Name: "wait",
+ Usage: "Block on one or more containers",
+ Description: waitDescription,
+ Action: waitCmd,
+ ArgsUsage: "CONTAINER-NAME [CONTAINER-NAME ...]",
+ }
+)
+
+func waitCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) < 1 {
+ return errors.Errorf("you must provide at least one container name or id")
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "error creating libpod runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ if err != nil {
+ return errors.Wrapf(err, "could not get config")
+ }
+
+ var lastError error
+ for _, container := range c.Args() {
+ ctr, err := runtime.LookupContainer(container)
+ if err != nil {
+ return errors.Wrapf(err, "unable to find container %s", container)
+ }
+ returnCode, err := ctr.Wait()
+ if err != nil {
+ if lastError != nil {
+ fmt.Fprintln(os.Stderr, lastError)
+ }
+ lastError = errors.Wrapf(err, "failed to wait for the container %v", container)
+ } else {
+ fmt.Println(returnCode)
+ }
+ }
+
+ return lastError
+}