diff options
author | baude <bbaude@redhat.com> | 2018-10-10 12:22:35 -0500 |
---|---|---|
committer | baude <bbaude@redhat.com> | 2018-10-23 08:26:21 -0500 |
commit | ee8f19e7be8626b17498a3c5602a7390a4d671e3 (patch) | |
tree | d9846560bf0723305600e9cf43749536c9befd14 /cmd/podman | |
parent | 5b2478ed87eedd8178c1672a786f95a6716a8edb (diff) | |
download | podman-ee8f19e7be8626b17498a3c5602a7390a4d671e3.tar.gz podman-ee8f19e7be8626b17498a3c5602a7390a4d671e3.tar.bz2 podman-ee8f19e7be8626b17498a3c5602a7390a4d671e3.zip |
Make podman ps fast
Like Ricky Bobby, we want to go fast.
Signed-off-by: baude <bbaude@redhat.com>
Diffstat (limited to 'cmd/podman')
-rw-r--r-- | cmd/podman/ps.go | 377 | ||||
-rw-r--r-- | cmd/podman/shared/container.go | 260 |
2 files changed, 404 insertions, 233 deletions
diff --git a/cmd/podman/ps.go b/cmd/podman/ps.go index 32b3a0574..a468f6121 100644 --- a/cmd/podman/ps.go +++ b/cmd/podman/ps.go @@ -1,11 +1,15 @@ package main import ( + "encoding/json" "fmt" + "html/template" + "os" "reflect" "sort" "strconv" "strings" + "text/tabwriter" "time" "github.com/containers/libpod/cmd/podman/formats" @@ -16,12 +20,31 @@ import ( "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/go-units" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/urfave/cli" "k8s.io/apimachinery/pkg/fields" ) -const mountTruncLength = 12 +const ( + mountTruncLength = 12 + hid = "CONTAINER ID" + himage = "IMAGE" + hcommand = "COMMAND" + hcreated = "CREATED" + hstatus = "STATUS" + hports = "PORTS" + hnames = "NAMES" + hsize = "SIZE" + hinfra = "IS INFRA" + hpod = "POD" + nspid = "PID" + nscgroup = "CGROUPNS" + nsipc = "IPC" + nsmnt = "MNT" + nsnet = "NET" + nspidns = "PIDNS" + nsuserns = "USERNS" + nsuts = "UTS" +) type psTemplateParams struct { ID string @@ -76,7 +99,7 @@ type psJSONParams struct { } // Type declaration and functions for sorting the PS output -type psSorted []psJSONParams +type psSorted []shared.PsContainerOutput func (a psSorted) Len() int { return len(a) } func (a psSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } @@ -84,7 +107,7 @@ 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, " ") + return a.psSorted[i].Command < a.psSorted[j].Command } type psSortedCreated struct{ psSorted } @@ -201,6 +224,11 @@ var ( ) func psCmd(c *cli.Context) error { + var ( + filterFuncs []libpod.ContainerFilter + outputContainers []*libpod.Container + ) + if err := validateFlags(c, psFlags); err != nil { return err } @@ -220,11 +248,9 @@ func psCmd(c *cli.Context) error { return errors.Errorf("too many arguments, ps takes no arguments") } - format := genPsFormat(c.String("format"), c.Bool("quiet"), c.Bool("size"), c.Bool("namespace"), c.Bool("pod"), c.Bool("all")) - opts := shared.PsOptions{ All: c.Bool("all"), - Format: format, + Format: c.String("format"), Last: c.Int("last"), Latest: c.Bool("latest"), NoTrunc: c.Bool("no-trunc"), @@ -235,18 +261,6 @@ func psCmd(c *cli.Context) error { Sort: c.String("sort"), } - 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() - // Don't return infra containers - return state == libpod.ContainerStateRunning && !c.IsInfra() - }) - } - filters := c.StringSlice("filter") if len(filters) > 0 { for _, f := range filters { @@ -262,8 +276,6 @@ func psCmd(c *cli.Context) error { } } - var outputContainers []*libpod.Container - if !opts.Latest { // Get all containers containers, err := runtime.GetContainers(filterFuncs...) @@ -288,7 +300,92 @@ func psCmd(c *cli.Context) error { outputContainers = []*libpod.Container{latestCtr} } - return generatePsOutput(outputContainers, opts) + pss := shared.PBatch(outputContainers, 8, opts) + if opts.Sort != "" { + pss, err = sortPsOutput(opts.Sort, pss) + if err != nil { + return err + } + } + + // If quiet, print only cids and return + if opts.Quiet { + return printQuiet(pss) + } + + // If the user wants their own GO template format + if opts.Format != "" { + if opts.Format == "json" { + return dumpJSON(pss) + } + return printFormat(opts.Format, pss) + } + + // Define a tab writer with stdout as the output + w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) + defer w.Flush() + + // Output standard PS headers + if !opts.Namespace { + fmt.Fprintf(w, "\n%s\t%s\t%s\t%s\t%s\t%s\t%s", hid, himage, hcommand, hcreated, hstatus, hports, hnames) + // If the user does not want size OR pod info, we print the isInfra bool + if !opts.Size && !opts.Pod { + fmt.Fprintf(w, "\t%s", hinfra) + } + // User wants pod info + if opts.Pod { + fmt.Fprintf(w, "\t%s", hpod) + } + //User wants size info + if opts.Size { + fmt.Fprintf(w, "\t%s", hsize) + } + } else { + // Output Namespace headers + fmt.Fprintf(w, "\n%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s", hid, hnames, nspid, nscgroup, nsipc, nsmnt, nsnet, nspidns, nsuserns, nsuts) + } + + // Now iterate each container and output its information + for _, container := range pss { + + // Standard PS output + if !opts.Namespace { + fmt.Fprintf(w, "\n%s\t%s\t%s\t%s\t%s\t%s\t%s", container.ID, container.Image, container.Command, container.Created, container.Status, container.Ports, container.Names) + + // If not size and not pod info, do isInfra + if !opts.Size && !opts.Pod { + fmt.Fprintf(w, "\t%t", container.IsInfra) + } + // User wants pod info + if opts.Pod { + fmt.Fprintf(w, "\t%s", container.Pod) + } + //User wants size info + if opts.Size { + var size string + if container.Size == nil { + size = units.HumanSizeWithPrecision(0, 0) + } else { + size = units.HumanSizeWithPrecision(float64(container.Size.RwSize), 3) + " (virtual " + units.HumanSizeWithPrecision(float64(container.Size.RootFsSize), 3) + ")" + fmt.Fprintf(w, "\t%s", size) + } + } + + } else { + // Print namespace information + ns := shared.GetNamespaces(container.Pid) + fmt.Fprintf(w, "\n%s\t%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s", container.ID, container.Names, container.Pid, ns.Cgroup, ns.IPC, ns.MNT, ns.NET, ns.PIDNS, ns.User, ns.UTS) + } + + } + return nil +} + +func printQuiet(containers []shared.PsContainerOutput) error { + for _, c := range containers { + fmt.Println(c.ID) + } + return nil } // checkFlagsPassed checks if mutually exclusive flags are passed together @@ -420,47 +517,6 @@ func generateContainerFilterFuncs(filter, filterValue string, runtime *libpod.Ru return nil, errors.Errorf("%s is an invalid filter", filter) } -// generate the template based on conditions given -func genPsFormat(format string, quiet, size, namespace, pod, infra 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 - } - podappend := "" - if pod { - podappend = "{{.Pod}}\t" - } - if namespace { - return fmt.Sprintf("table {{.ID}}\t{{.Names}}\t%s{{.PID}}\t{{.CGROUPNS}}\t{{.IPC}}\t{{.MNT}}\t{{.NET}}\t{{.PIDNS}}\t{{.USERNS}}\t{{.UTS}}", podappend) - } - format = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.Created}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}\t" - format += podappend - if size { - format += "{{.Size}}\t" - } - if infra { - format += "{{.IsInfra}}\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)) @@ -503,176 +559,6 @@ func sortPsOutput(sortBy string, psOutput psSorted) (psSorted, error) { return psOutput, nil } -// getTemplateOutput returns the modified container information -func getTemplateOutput(psParams []psJSONParams, opts shared.PsOptions) ([]psTemplateParams, error) { - var ( - psOutput []psTemplateParams - pod, status, size string - ns *shared.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 - } - if strings.Contains(opts.Format, ".Pod") || opts.Sort == "pod" { - opts.Pod = true - } - - for _, psParam := range psParams { - // do we need this? - imageName := psParam.Image - ctrID := psParam.ID - - if opts.Namespace { - ns = psParam.Namespaces - } - if opts.Size { - if psParam.Size == nil { - size = units.HumanSizeWithPrecision(0, 0) - } else { - size = units.HumanSizeWithPrecision(float64(psParam.Size.RwSize), 3) + " (virtual " + units.HumanSizeWithPrecision(float64(psParam.Size.RootFsSize), 3) + ")" - } - } - if opts.Pod { - pod = psParam.Pod - } - - command := strings.Join(psParam.Command, " ") - if !opts.NoTrunc { - if len(command) > 20 { - command = command[:19] + "..." - } - } - ports := portsToString(psParam.Ports) - labels := formatLabels(psParam.Labels) - - switch psParam.Status { - case libpod.ContainerStateExited.String(): - fallthrough - case libpod.ContainerStateStopped.String(): - exitedSince := units.HumanDuration(time.Since(psParam.ExitedAt)) - status = fmt.Sprintf("Exited (%d) %s ago", psParam.ExitCode, exitedSince) - case libpod.ContainerStateRunning.String(): - status = "Up " + units.HumanDuration(time.Since(psParam.StartedAt)) + " ago" - case libpod.ContainerStatePaused.String(): - status = "Paused" - case libpod.ContainerStateCreated.String(), libpod.ContainerStateConfigured.String(): - status = "Created" - default: - status = "Error" - } - - if !opts.NoTrunc { - ctrID = shortID(psParam.ID) - pod = shortID(psParam.Pod) - } - params := psTemplateParams{ - ID: ctrID, - Image: imageName, - Command: command, - CreatedAtTime: psParam.CreatedAt, - Created: units.HumanDuration(time.Since(psParam.CreatedAt)) + " ago", - Status: status, - Ports: ports, - Size: size, - Names: psParam.Names, - Labels: labels, - Mounts: getMounts(psParam.Mounts, opts.NoTrunc), - PID: psParam.PID, - Pod: pod, - IsInfra: psParam.IsInfra, - } - - if opts.Namespace { - params.CGROUPNS = ns.Cgroup - params.IPC = ns.IPC - params.MNT = ns.MNT - params.NET = ns.NET - params.PIDNS = ns.PIDNS - params.USERNS = ns.User - params.UTS = ns.UTS - } - psOutput = append(psOutput, params) - } - - return psOutput, nil -} - -// getAndSortJSONOutput returns the container info in its raw, sorted form -func getAndSortJSONParams(containers []*libpod.Container, opts shared.PsOptions) ([]psJSONParams, error) { - var ( - psOutput psSorted - ns *shared.Namespace - ) - for _, ctr := range containers { - batchInfo, err := shared.BatchContainerOp(ctr, opts) - if err != nil { - if errors.Cause(err) == libpod.ErrNoSuchCtr { - logrus.Warn(err) - continue - } - return nil, err - } - - if opts.Namespace { - ns = shared.GetNamespaces(batchInfo.Pid) - } - params := psJSONParams{ - ID: ctr.ID(), - Image: batchInfo.ConConfig.RootfsImageName, - ImageID: batchInfo.ConConfig.RootfsImageID, - Command: batchInfo.ConConfig.Spec.Process.Args, - ExitCode: batchInfo.ExitCode, - Exited: batchInfo.Exited, - CreatedAt: batchInfo.ConConfig.CreatedTime, - StartedAt: batchInfo.StartedTime, - ExitedAt: batchInfo.ExitedTime, - Status: batchInfo.ConState.String(), - PID: batchInfo.Pid, - Ports: batchInfo.ConConfig.PortMappings, - Size: batchInfo.Size, - Names: batchInfo.ConConfig.Name, - Labels: batchInfo.ConConfig.Labels, - Mounts: batchInfo.ConConfig.UserVolumes, - ContainerRunning: batchInfo.ConState == libpod.ContainerStateRunning, - Namespaces: ns, - Pod: ctr.PodID(), - IsInfra: ctr.IsInfra(), - } - - psOutput = append(psOutput, params) - } - return sortPsOutput(opts.Sort, psOutput) -} - -func generatePsOutput(containers []*libpod.Container, opts shared.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: - if err != nil { - return errors.Wrapf(err, "unable to create JSON for output") - } - out = formats.JSONStructArray{Output: psToGeneric([]psTemplateParams{}, psOutput)} - default: - psOutput, err := getTemplateOutput(psOutput, 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() -} - // getLabels converts the labels to a string of the form "key=value, key2=value2" func formatLabels(labels map[string]string) string { var arr []string @@ -723,3 +609,28 @@ func portsToString(ports []ocicni.PortMapping) string { } return strings.Join(portDisplay, ", ") } + +func printFormat(format string, containers []shared.PsContainerOutput) error { + out := template.New("output") + out, err := out.Parse(format + "\n") + + if err != nil { + return err + } + for _, container := range containers { + if err := out.Execute(os.Stdout, container); err != nil { + return err + } + + } + return nil +} + +func dumpJSON(containers []shared.PsContainerOutput) error { + b, err := json.MarshalIndent(containers, "", "\t") + if err != nil { + return err + } + os.Stdout.Write(b) + return nil +} diff --git a/cmd/podman/shared/container.go b/cmd/podman/shared/container.go index f44d0f7c9..4af737e0a 100644 --- a/cmd/podman/shared/container.go +++ b/cmd/podman/shared/container.go @@ -2,11 +2,15 @@ package shared import ( "encoding/json" + "fmt" + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/docker/go-units" "os" "path/filepath" "regexp" "strconv" "strings" + "sync" "time" "github.com/containers/libpod/libpod" @@ -17,6 +21,11 @@ import ( "github.com/sirupsen/logrus" ) +const ( + cidTruncLength = 12 + podTruncLength = 12 +) + // PsOptions describes the struct being formed for ps type PsOptions struct { All bool @@ -45,6 +54,35 @@ type BatchContainerStruct struct { Size *ContainerSize } +// PsContainerOutput is the struct being returned from a parallel +// Batch operation +type PsContainerOutput struct { + ID string + Image string + Command string + Created string + Ports string + Names string + IsInfra bool + Status string + State libpod.ContainerStatus + Pid int + Size *ContainerSize + Pod string + CreatedAt time.Time + ExitedAt time.Time + StartedAt time.Time + Labels map[string]string + PID string + Cgroup string + IPC string + MNT string + NET string + PIDNS string + User string + UTS string +} + // Namespace describes output for ps namespace type Namespace struct { PID string `json:"pid,omitempty"` @@ -64,6 +102,212 @@ type ContainerSize struct { RwSize int64 `json:"rwSize"` } +// NewBatchContainer runs a batch process under one lock to get container information and only +// be called in PBatch +func NewBatchContainer(ctr *libpod.Container, opts PsOptions) (PsContainerOutput, error) { + var ( + conState libpod.ContainerStatus + command string + created string + status string + exitedAt time.Time + startedAt time.Time + exitCode int32 + err error + pid int + size *ContainerSize + ns *Namespace + pso PsContainerOutput + ) + batchErr := ctr.Batch(func(c *libpod.Container) error { + conState, err = c.State() + if err != nil { + return errors.Wrapf(err, "unable to obtain container state") + } + command = strings.Join(c.Command(), " ") + created = units.HumanDuration(time.Since(c.CreatedTime())) + " ago" + + exitCode, _, err = c.ExitCode() + if err != nil { + return errors.Wrapf(err, "unable to obtain container exit code") + } + startedAt, err = c.StartedTime() + if err != nil { + logrus.Errorf("error getting started time for %q: %v", c.ID(), err) + } + exitedAt, err = c.FinishedTime() + if err != nil { + logrus.Errorf("error getting exited time for %q: %v", c.ID(), err) + } + if opts.Namespace { + pid, err = c.PID() + if err != nil { + return errors.Wrapf(err, "unable to obtain container pid") + } + ns = GetNamespaces(pid) + } + if opts.Size { + size = new(ContainerSize) + + rootFsSize, err := c.RootFsSize() + if err != nil { + logrus.Errorf("error getting root fs size for %q: %v", c.ID(), err) + } + + rwSize, err := c.RWSize() + if err != nil { + logrus.Errorf("error getting rw size for %q: %v", c.ID(), err) + } + + size.RootFsSize = rootFsSize + size.RwSize = rwSize + } + + return nil + }) + + if batchErr != nil { + return pso, batchErr + } + + switch conState.String() { + case libpod.ContainerStateExited.String(): + fallthrough + case libpod.ContainerStateStopped.String(): + exitedSince := units.HumanDuration(time.Since(exitedAt)) + status = fmt.Sprintf("Exited (%d) %s ago", exitCode, exitedSince) + case libpod.ContainerStateRunning.String(): + status = "Up " + units.HumanDuration(time.Since(startedAt)) + " ago" + case libpod.ContainerStatePaused.String(): + status = "Paused" + case libpod.ContainerStateCreated.String(), libpod.ContainerStateConfigured.String(): + status = "Created" + default: + status = "Error" + } + + _, imageName := ctr.Image() + cid := ctr.ID() + pod := ctr.PodID() + if !opts.NoTrunc { + cid = cid[0:cidTruncLength] + if len(pod) > 12 { + pod = pod[0:podTruncLength] + } + } + + pso.ID = cid + pso.Image = imageName + pso.Command = command + pso.Created = created + pso.Ports = portsToString(ctr.PortMappings()) + pso.Names = ctr.Name() + pso.IsInfra = ctr.IsInfra() + pso.Status = status + pso.State = conState + pso.Pid = pid + pso.Size = size + pso.Pod = pod + pso.ExitedAt = exitedAt + pso.CreatedAt = ctr.CreatedTime() + pso.StartedAt = startedAt + pso.Labels = ctr.Labels() + + if opts.Namespace { + pso.Cgroup = ns.Cgroup + pso.IPC = ns.IPC + pso.MNT = ns.MNT + pso.NET = ns.NET + pso.User = ns.User + pso.UTS = ns.UTS + pso.PIDNS = ns.PIDNS + } + + return pso, nil +} + +type pFunc func() (PsContainerOutput, error) + +type workerInput struct { + parallelFunc pFunc + opts PsOptions + cid string + job int +} + +// worker is a "threaded" worker that takes jobs from the channel "queue" +func worker(wg *sync.WaitGroup, jobs <-chan workerInput, results chan<- PsContainerOutput, errors chan<- error) { + for j := range jobs { + r, err := j.parallelFunc() + // If we find an error, we return just the error + if err != nil { + errors <- err + } else { + // Return the result + results <- r + } + wg.Done() + } +} + +// PBatch is performs batch operations on a container in parallel. It spawns the number of workers +// relative to the the number of parallel operations desired. +func PBatch(containers []*libpod.Container, workers int, opts PsOptions) []PsContainerOutput { + var ( + wg sync.WaitGroup + psResults []PsContainerOutput + ) + + // If the number of containers in question is less than the number of + // proposed parallel operations, we shouldnt spawn so many workers + if workers > len(containers) { + workers = len(containers) + } + + jobs := make(chan workerInput, len(containers)) + results := make(chan PsContainerOutput, len(containers)) + batchErrors := make(chan error, len(containers)) + + // Create the workers + for w := 1; w <= workers; w++ { + go worker(&wg, jobs, results, batchErrors) + } + + // Add jobs to the workers + for i, j := range containers { + j := j + wg.Add(1) + f := func() (PsContainerOutput, error) { + return NewBatchContainer(j, opts) + } + jobs <- workerInput{ + parallelFunc: f, + opts: opts, + cid: j.ID(), + job: i, + } + } + close(jobs) + wg.Wait() + close(results) + close(batchErrors) + for err := range batchErrors { + logrus.Errorf("unable to get container info: %q", err) + } + for res := range results { + // We sort out running vs non-running here to save lots of copying + // later. + if !opts.All && !opts.Latest && opts.Last < 1 { + if !res.IsInfra && res.State == libpod.ContainerStateRunning { + psResults = append(psResults, res) + } + } else { + psResults = append(psResults, res) + } + } + return psResults +} + // BatchContainer is used in ps to reduce performance hits by "batching" // locks. func BatchContainerOp(ctr *libpod.Container, opts PsOptions) (BatchContainerStruct, error) { @@ -325,3 +569,19 @@ func getCgroup(spec *specs.Spec) string { } return cgroup } + +// portsToString converts the ports used to a string of the from "port1, port2" +func portsToString(ports []ocicni.PortMapping) string { + var portDisplay []string + if len(ports) == 0 { + return "" + } + for _, v := range ports { + hostIP := v.HostIP + if hostIP == "" { + hostIP = "0.0.0.0" + } + portDisplay = append(portDisplay, fmt.Sprintf("%s:%d->%d/%s", hostIP, v.HostPort, v.ContainerPort, v.Protocol)) + } + return strings.Join(portDisplay, ", ") +} |