diff options
author | haircommander <pehunt@redhat.com> | 2018-06-14 15:29:46 -0400 |
---|---|---|
committer | Atomic Bot <atomic-devel@projectatomic.io> | 2018-06-19 14:45:29 +0000 |
commit | 4ab054073dadf5257bdd0ff21e154b859e139f1e (patch) | |
tree | 73cc17e44047506e8d548b8625346400899af0f1 | |
parent | dfd34b1b03d3ec0447fb04e15b2d0861a4a9778d (diff) | |
download | podman-4ab054073dadf5257bdd0ff21e154b859e139f1e.tar.gz podman-4ab054073dadf5257bdd0ff21e154b859e139f1e.tar.bz2 podman-4ab054073dadf5257bdd0ff21e154b859e139f1e.zip |
Added --sort to ps
Also podman ps now allows user to only output size of root FS, changed language of images and ps --sort to be by "created" as opposed to "time", and refactored the way templates are created (converted from psJSONParams type).
Signed-off-by: haircommander <pehunt@redhat.com>
Closes: #948
Approved by: rhatdan
-rw-r--r-- | cmd/podman/batchcontainer/container.go | 1 | ||||
-rw-r--r-- | cmd/podman/images.go | 12 | ||||
-rw-r--r-- | cmd/podman/ps.go | 234 | ||||
-rw-r--r-- | completions/bash/podman | 1 | ||||
-rw-r--r-- | docs/podman-images.1.md | 2 | ||||
-rw-r--r-- | docs/podman-ps.1.md | 11 | ||||
-rw-r--r-- | test/e2e/ps_test.go | 62 |
7 files changed, 222 insertions, 101 deletions
diff --git a/cmd/podman/batchcontainer/container.go b/cmd/podman/batchcontainer/container.go index 8da73a811..09db4f8a2 100644 --- a/cmd/podman/batchcontainer/container.go +++ b/cmd/podman/batchcontainer/container.go @@ -27,6 +27,7 @@ type PsOptions struct { NoTrunc bool Quiet bool Size bool + Sort string Label string Namespace bool } diff --git a/cmd/podman/images.go b/cmd/podman/images.go index 654e59845..fdf2eb02c 100644 --- a/cmd/podman/images.go +++ b/cmd/podman/images.go @@ -53,9 +53,9 @@ type imagesSorted []imagesTemplateParams func (a imagesSorted) Len() int { return len(a) } func (a imagesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -type imagesSortedTime struct{ imagesSorted } +type imagesSortedCreated struct{ imagesSorted } -func (a imagesSortedTime) Less(i, j int) bool { +func (a imagesSortedCreated) Less(i, j int) bool { return a.imagesSorted[i].CreatedTime.After(a.imagesSorted[j].CreatedTime) } @@ -113,8 +113,8 @@ var ( }, cli.StringFlag{ Name: "sort", - Usage: "Sort by size, time, id, repository or tag", - Value: "time", + Usage: "Sort by created, id, repository, size, or tag", + Value: "created", }, } @@ -252,8 +252,8 @@ func sortImagesOutput(sortBy string, imagesOutput imagesSorted) imagesSorted { case "repository": sort.Sort(imagesSortedRepository{imagesOutput}) default: - // default is time - sort.Sort(imagesSortedTime{imagesOutput}) + // default is created time + sort.Sort(imagesSortedCreated{imagesOutput}) } return imagesOutput } diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index 69911248b..18c75495c 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "reflect" "sort" @@ -11,15 +10,12 @@ import ( "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/go-units" - specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/projectatomic/libpod/cmd/podman/batchcontainer" "github.com/projectatomic/libpod/cmd/podman/formats" "github.com/projectatomic/libpod/cmd/podman/libpodruntime" "github.com/projectatomic/libpod/libpod" - cc "github.com/projectatomic/libpod/pkg/spec" "github.com/projectatomic/libpod/pkg/util" - "github.com/sirupsen/logrus" "github.com/urfave/cli" "k8s.io/apimachinery/pkg/fields" ) @@ -49,8 +45,9 @@ type psTemplateParams struct { UTS string } -// psJSONParams is only used when the JSON format is specified, -// and is better for data processing from JSON. +// psJSONParams is used as a base structure for the psParams +// If template output is requested, psJSONParams will be converted to +// psTemplateParams. // 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 { @@ -59,24 +56,65 @@ type psJSONParams struct { ImageID string `json:"image_id"` Command []string `json:"command"` CreatedAt time.Time `json:"createdAt"` + ExitCode int32 `json:"exitCode"` RunningFor time.Duration `json:"runningFor"` Status string `json:"status"` + PID int `json:"PID"` Ports []ocicni.PortMapping `json:"ports"` RootFsSize int64 `json:"rootFsSize"` RWSize int64 `json:"rwSize"` Names string `json:"names"` Labels fields.Set `json:"labels"` - Mounts []specs.Mount `json:"mounts"` + Mounts []string `json:"mounts"` ContainerRunning bool `json:"ctrRunning"` Namespaces *batchcontainer.Namespace `json:"namespace,omitempty"` } -// Type declaration and functions for sorting the PS output by time -type psSorted []psTemplateParams +// Type declaration and functions for sorting the PS output +type psSorted []psJSONParams -func (a psSorted) Len() int { return len(a) } -func (a psSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a psSorted) Less(i, j int) bool { return a[i].CreatedAtTime.After(a[j].CreatedAtTime) } +func (a psSorted) Len() int { return len(a) } +func (a psSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type psSortedCommand struct{ psSorted } + +func (a psSortedCommand) Less(i, j int) bool { + return strings.Join(a.psSorted[i].Command, " ") < strings.Join(a.psSorted[j].Command, " ") +} + +type psSortedCreated struct{ psSorted } + +func (a psSortedCreated) Less(i, j int) bool { + return a.psSorted[i].CreatedAt.After(a.psSorted[j].CreatedAt) +} + +type psSortedId struct{ psSorted } + +func (a psSortedId) Less(i, j int) bool { return a.psSorted[i].ID < a.psSorted[j].ID } + +type psSortedImage struct{ psSorted } + +func (a psSortedImage) Less(i, j int) bool { return a.psSorted[i].Image < a.psSorted[j].Image } + +type psSortedNames struct{ psSorted } + +func (a psSortedNames) Less(i, j int) bool { return a.psSorted[i].Names < a.psSorted[j].Names } + +type psSortedRunningFor struct{ psSorted } + +func (a psSortedRunningFor) Less(i, j int) bool { + return a.psSorted[i].RunningFor < a.psSorted[j].RunningFor +} + +type psSortedStatus struct{ psSorted } + +func (a psSortedStatus) Less(i, j int) bool { return a.psSorted[i].Status < a.psSorted[j].Status } + +type psSortedSize struct{ psSorted } + +func (a psSortedSize) Less(i, j int) bool { + return a.psSorted[i].RootFsSize < a.psSorted[j].RootFsSize +} var ( psFlags = []cli.Flag{ @@ -102,6 +140,10 @@ var ( Usage: "Show the latest container created (all states)", }, cli.BoolFlag{ + Name: "namespace, ns", + Usage: "Display namespace information", + }, + cli.BoolFlag{ Name: "no-trunc", Usage: "Display the extended information", }, @@ -113,9 +155,10 @@ var ( Name: "size, s", Usage: "Display the total file sizes", }, - cli.BoolFlag{ - Name: "namespace, ns", - Usage: "Display namespace information", + cli.StringFlag{ + Name: "sort", + Usage: "Sort output by command, created, id, image, names, runningfor, size, or status", + Value: "created", }, } psDescription = "Prints out information about the containers" @@ -171,6 +214,7 @@ func psCmd(c *cli.Context) error { Quiet: c.Bool("quiet"), Size: c.Bool("size"), Namespace: c.Bool("namespace"), + Sort: c.String("sort"), } var filterFuncs []libpod.ContainerFilter @@ -405,94 +449,95 @@ func (p *psTemplateParams) headerMap() map[string]string { return values } +func sortPsOutput(sortBy string, psOutput psSorted) (psSorted, error) { + switch sortBy { + case "id": + sort.Sort(psSortedId{psOutput}) + case "image": + sort.Sort(psSortedImage{psOutput}) + case "command": + sort.Sort(psSortedCommand{psOutput}) + case "runningfor": + sort.Sort(psSortedRunningFor{psOutput}) + case "status": + sort.Sort(psSortedStatus{psOutput}) + case "size": + sort.Sort(psSortedSize{psOutput}) + case "names": + sort.Sort(psSortedNames{psOutput}) + case "created": + sort.Sort(psSortedCreated{psOutput}) + default: + return nil, errors.Errorf("invalid option for --sort, options are: command, created, id, image, names, runningfor, size, or status") + } + return psOutput, nil +} + // getTemplateOutput returns the modified container information -func getTemplateOutput(containers []*libpod.Container, opts batchcontainer.PsOptions) (psSorted, error) { +func getTemplateOutput(psParams []psJSONParams, opts batchcontainer.PsOptions) ([]psTemplateParams, error) { var ( - psOutput psSorted + psOutput []psTemplateParams status, size string ns *batchcontainer.Namespace ) + // If the user is trying to filter based on size, or opted to sort on size + // the size bool must be set. + if strings.Contains(opts.Format, ".Size") || opts.Sort == "size" { + opts.Size = true + } - for _, ctr := range containers { - batchInfo, err := batchcontainer.BatchContainerOp(ctr, opts) - if err != nil { - // If the error was ErrNoSuchCtr, it was probably - // removed sometime after we got the initial list. - // Just ignore it. - if errors.Cause(err) == libpod.ErrNoSuchCtr { - logrus.Debugf("Container %s removed before batch, ignoring in output", ctr.ID()) - continue - } - - return nil, err - } - ctrID := ctr.ID() - runningFor := "" - // If the container has not be started, the "zero" value of time is 0001-01-01 00:00:00 +0000 UTC - // which would make the time elapsed about a few hundred of years. So checking for the "zero" value of time.Time - if !batchInfo.StartedTime.IsZero() { - runningFor = units.HumanDuration(time.Since(batchInfo.StartedTime)) - } - imageName := batchInfo.ConConfig.RootfsImageName + for _, psParam := range psParams { + // do we need this? + imageName := psParam.Image + ctrID := psParam.ID - var createArtifact cc.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 ps information, error getting artifact %q: %v", ctr.ID(), err) - } if opts.Namespace { - ns = batchcontainer.GetNamespaces(batchInfo.Pid) + ns = psParam.Namespaces } if opts.Size { - - size = units.HumanSizeWithPrecision(float64(batchInfo.RwSize), 3) + " (virtual " + units.HumanSizeWithPrecision(float64(batchInfo.RootFsSize), 3) + ")" + size = units.HumanSizeWithPrecision(float64(psParam.RWSize), 3) + " (virtual " + units.HumanSizeWithPrecision(float64(psParam.RootFsSize), 3) + ")" } + runningFor := units.HumanDuration(psParam.RunningFor) - command := strings.Join(batchInfo.ConConfig.Spec.Process.Args, " ") + command := strings.Join(psParam.Command, " ") if !opts.NoTrunc { if len(command) > 20 { command = command[:19] + "..." } } - ports := portsToString(batchInfo.ConConfig.PortMappings) - mounts := getMounts(createArtifact.Volumes, opts.NoTrunc) - labels := formatLabels(ctr.Labels()) - - switch batchInfo.ConState { - case libpod.ContainerStateStopped: - status = fmt.Sprintf("Exited (%d) %s ago", batchInfo.ExitCode, runningFor) - case libpod.ContainerStateRunning: + ports := portsToString(psParam.Ports) + labels := formatLabels(psParam.Labels) + + switch psParam.Status { + case libpod.ContainerStateStopped.String(): + status = fmt.Sprintf("Exited (%d) %s ago", psParam.ExitCode, runningFor) + case libpod.ContainerStateRunning.String(): status = "Up " + runningFor + " ago" - case libpod.ContainerStatePaused: + case libpod.ContainerStatePaused.String(): status = "Paused" - case libpod.ContainerStateCreated, libpod.ContainerStateConfigured: + case libpod.ContainerStateCreated.String(), libpod.ContainerStateConfigured.String(): status = "Created" default: status = "Dead" } if !opts.NoTrunc { - ctrID = shortID(ctr.ID()) - imageName = batchInfo.ConConfig.RootfsImageName + ctrID = shortID(psParam.ID) } params := psTemplateParams{ ID: ctrID, Image: imageName, Command: command, - CreatedAtTime: batchInfo.ConConfig.CreatedTime, - Created: units.HumanDuration(time.Since(batchInfo.ConConfig.CreatedTime)) + " ago", + CreatedAtTime: psParam.CreatedAt, + Created: units.HumanDuration(time.Since(psParam.CreatedAt)) + " ago", RunningFor: runningFor, Status: status, Ports: ports, Size: size, - Names: ctr.Name(), + Names: psParam.Names, Labels: labels, - Mounts: mounts, - PID: batchInfo.Pid, + Mounts: getMounts(psParam.Mounts, opts.NoTrunc), + PID: psParam.PID, } if opts.Namespace { @@ -506,16 +551,14 @@ func getTemplateOutput(containers []*libpod.Container, opts batchcontainer.PsOpt } psOutput = append(psOutput, params) } - // Sort the ps entries by created time - sort.Sort(psSorted(psOutput)) return psOutput, nil } -// getJSONOutput returns the container info in its raw form -func getJSONOutput(containers []*libpod.Container, opts batchcontainer.PsOptions) ([]psJSONParams, error) { +// getAndSortJSONOutput returns the container info in its raw, sorted form +func getAndSortJSONParams(containers []*libpod.Container, opts batchcontainer.PsOptions) ([]psJSONParams, error) { var ( - psOutput []psJSONParams + psOutput psSorted ns *batchcontainer.Namespace ) for _, ctr := range containers { @@ -523,24 +566,24 @@ func getJSONOutput(containers []*libpod.Container, opts batchcontainer.PsOptions if err != nil { return nil, err } + if opts.Namespace { ns = batchcontainer.GetNamespaces(batchInfo.Pid) } params := psJSONParams{ - ID: ctr.ID(), - Image: batchInfo.ConConfig.RootfsImageName, - ImageID: batchInfo.ConConfig.RootfsImageID, - Command: batchInfo.ConConfig.Spec.Process.Args, - CreatedAt: batchInfo.ConConfig.CreatedTime, - Status: batchInfo.ConState.String(), - Ports: batchInfo.ConConfig.PortMappings, - RootFsSize: batchInfo.RootFsSize, - RWSize: batchInfo.RwSize, - Names: batchInfo.ConConfig.Name, - Labels: batchInfo.ConConfig.Labels, - Mounts: batchInfo.ConConfig.Spec.Mounts, - ContainerRunning: batchInfo.ConState == libpod.ContainerStateRunning, - Namespaces: ns, + ID: ctr.ID(), + Image: batchInfo.ConConfig.RootfsImageName, + ImageID: batchInfo.ConConfig.RootfsImageID, + Command: batchInfo.ConConfig.Spec.Process.Args, + CreatedAt: batchInfo.ConConfig.CreatedTime, + Status: batchInfo.ConState.String(), + Ports: batchInfo.ConConfig.PortMappings, + RootFsSize: batchInfo.RootFsSize, + RWSize: batchInfo.RwSize, + Names: batchInfo.ConConfig.Name, + Labels: batchInfo.ConConfig.Labels, + Mounts: batchInfo.ConConfig.UserVolumes, + Namespaces: ns, } if !batchInfo.StartedTime.IsZero() { @@ -549,24 +592,27 @@ func getJSONOutput(containers []*libpod.Container, opts batchcontainer.PsOptions psOutput = append(psOutput, params) } - return psOutput, nil + return sortPsOutput(opts.Sort, psOutput) } func generatePsOutput(containers []*libpod.Container, opts batchcontainer.PsOptions) error { if len(containers) == 0 && opts.Format != formats.JSONString { return nil } + psOutput, err := getAndSortJSONParams(containers, opts) + if err != nil { + return err + } var out formats.Writer switch opts.Format { case formats.JSONString: - psOutput, err := getJSONOutput(containers, opts) 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) + psOutput, err := getTemplateOutput(psOutput, opts) if err != nil { return errors.Wrapf(err, "unable to create output") } @@ -592,9 +638,13 @@ func formatLabels(labels map[string]string) string { // getMounts converts the volumes mounted to a string of the form "mount1, mount2" // it truncates it if noTrunc is false func getMounts(mounts []string, noTrunc bool) string { + return strings.Join(getMountsArray(mounts, noTrunc), ",") +} + +func getMountsArray(mounts []string, noTrunc bool) []string { var arr []string if len(mounts) == 0 { - return "" + return mounts } for _, mount := range mounts { splitArr := strings.Split(mount, ":") @@ -604,7 +654,7 @@ func getMounts(mounts []string, noTrunc bool) string { } arr = append(arr, splitArr[0]) } - return strings.Join(arr, ",") + return arr } // portsToString converts the ports used to a string of the from "port1, port2" diff --git a/completions/bash/podman b/completions/bash/podman index 292aa4b4b..bda93fa8a 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -1846,6 +1846,7 @@ _podman_ps() { --filter -f --format --last -n + --sort " local boolean_options=" --all -a diff --git a/docs/podman-images.1.md b/docs/podman-images.1.md index a97785de7..730ef4e3c 100644 --- a/docs/podman-images.1.md +++ b/docs/podman-images.1.md @@ -42,7 +42,7 @@ Lists only the image IDs. **--sort** -Sort by id, repository, size, tag or time (default: time) +Sort by created, id, repository, size or tag (default: created) ## EXAMPLE diff --git a/docs/podman-ps.1.md b/docs/podman-ps.1.md index f5d57c13c..f708c7d74 100644 --- a/docs/podman-ps.1.md +++ b/docs/podman-ps.1.md @@ -63,6 +63,11 @@ Valid placeholders for the Go template are listed below: | .Labels | All the labels assigned to the container | | .Mounts | Volumes mounted in the container | +**--sort** + +Sort by command, created, id, image, names, runningfor, size, or status", +Note: Choosing size will sort by size of rootFs, not alphabetically like the rest of the options +Default: created **--size, -s** @@ -128,6 +133,12 @@ CONTAINER ID NAMES a31ebbee9cee7 k8s_podsandbox1-redis_podsandbox1_redhat.test.crio_redhat-test-crio_0 29717 4026531835 4026532585 4026532587 4026532508 4026532589 4026531837 4026532588 ``` +``` +sudo podman ps -a --size --sort names +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +69ed779d8ef9f redis:alpine "redis-server" 25 hours ago Created 6379/tcp k8s_container1_podsandbox1_redhat.test.crio_redhat-test-crio_1 +02f65160e14ca redis:alpine "redis-server" 19 hours ago Exited (-1) 19 hours ago 6379/tcp k8s_podsandbox1-redis_podsandbox1_redhat.test.crio_redhat-test-crio_0 +``` ## ps Print a list of containers diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index feb0628b3..38b59e1d1 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -1,10 +1,12 @@ package integration import ( - "os" - "fmt" + "os" + "regexp" + "sort" + "github.com/docker/go-units" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -186,4 +188,60 @@ var _ = Describe("Podman ps", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Not(Equal(0))) }) + + It("podman --sort by size", func() { + // these images chosen because their size would be sorted differently alphabetically vs + // by the size of their virtual fs + session := podmanTest.Podman([]string{"run", "docker.io/mattdm/fedora-small", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + podmanTest.RestoreArtifact(nginx) + session = podmanTest.Podman([]string{"run", "-dt", "-P", "docker.io/library/nginx:latest"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"ps", "-a", "--sort=size", "--format", "{{.Size}}"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + sortedArr := session.OutputToStringArray() + + Expect(sort.SliceIsSorted(sortedArr, func(i, j int) bool { + r := regexp.MustCompile(`^\S+\s+\(virtual (\S+)\)`) + matches1 := r.FindStringSubmatch(sortedArr[i]) + matches2 := r.FindStringSubmatch(sortedArr[j]) + + // sanity check in case an oddly formatted size appears + if len(matches1) < 2 || len(matches2) < 2 { + return sortedArr[i] < sortedArr[j] + } else { + size1, _ := units.FromHumanSize(matches1[1]) + size2, _ := units.FromHumanSize(matches2[1]) + return size1 < size2 + } + })).To(BeTrue()) + + }) + + It("podman --sort by command", func() { + session := podmanTest.RunTopContainer("") + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + podmanTest.RestoreArtifact(nginx) + session = podmanTest.Podman([]string{"run", "-d", fedoraMinimal, "pwd"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"ps", "-a", "--sort=command", "--format", "{{.Command}}"}) + + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + sortedArr := session.OutputToStringArray() + + Expect(sort.SliceIsSorted(sortedArr, func(i, j int) bool { return sortedArr[i] < sortedArr[j] })).To(BeTrue()) + + }) }) |