diff options
author | Daniel J Walsh <dwalsh@redhat.com> | 2017-12-15 16:58:36 -0500 |
---|---|---|
committer | Atomic Bot <atomic-devel@projectatomic.io> | 2017-12-18 16:46:05 +0000 |
commit | 5770dc2640c216525ab84031e3712fcc46b3b087 (patch) | |
tree | 8a1c5c4e4a6ce6a35a3767247623a62bfd698f77 /cmd/podman | |
parent | de3468e120d489d046c08dad72ba2262e222ccb1 (diff) | |
download | podman-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')
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 +} |