package main import ( "reflect" "regexp" "strconv" "strings" "time" "github.com/docker/go-units" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/fields" "github.com/kubernetes-incubator/cri-o/cmd/kpod/formats" "github.com/kubernetes-incubator/cri-o/libkpod" "github.com/kubernetes-incubator/cri-o/oci" "github.com/pkg/errors" "github.com/urfave/cli" ) type psOptions struct { all bool filter string format string last int latest bool noTrunc bool quiet bool size bool label string } 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 } // 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 libkpod.ContainerData, // the members of the struct are the sama data types as their sources. type psJSONParams struct { ID string `json:"id"` Image string `json:"image"` 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"` } const runningState = "running" 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)", }, 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", }, } psDescription = "Prints out information about the containers" psCommand = cli.Command{ Name: "ps", Usage: "List containers", Description: psDescription, Flags: psFlags, Action: psCmd, ArgsUsage: "", } ) func psCmd(c *cli.Context) error { config, err := getConfig(c) if err != nil { return errors.Wrapf(err, "could not get config") } server, err := libkpod.New(config) if err != nil { return errors.Wrapf(err, "error creating server") } if err := server.Update(); err != nil { return errors.Wrapf(err, "error updating list of containers") } if len(c.Args()) > 0 { return errors.Errorf("too many arguments, ps takes no arguments") } format := genPsFormat(c.Bool("quiet"), c.Bool("size")) if c.IsSet("format") { format = c.String("format") } 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"), } // all, latest, and last are mutually exclusive. Only one flag can be used at a time exclusiveOpts := 0 if opts.last > 0 { exclusiveOpts++ } if opts.latest { exclusiveOpts++ } if opts.all { exclusiveOpts++ } if exclusiveOpts > 1 { return errors.Errorf("Last, latest and all are mutually exclusive") } containers, err := server.ListContainers() if err != nil { return errors.Wrapf(err, "error getting containers from server") } var params *FilterParamsPS if opts.filter != "" { params, err = parseFilter(opts.filter, containers) if err != nil { return errors.Wrapf(err, "error parsing filter") } } else { params = nil } containerList := getContainersMatchingFilter(containers, params, server) return generatePsOutput(containerList, server, opts) } // generate the template based on conditions given func genPsFormat(quiet, size bool) (format string) { if quiet { return formats.IDString } format = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}\t" if size { format += "{{.Size}}\t" } return } 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 } // getContainers gets the containers that match the flags given func getContainers(containers []*libkpod.ContainerData, opts psOptions) []*libkpod.ContainerData { var containersOutput []*libkpod.ContainerData if opts.last > 0 && opts.last < len(containers) { for i := 0; i < opts.last; i++ { containersOutput = append(containersOutput, containers[i]) } return containersOutput } if opts.latest { return []*libkpod.ContainerData{containers[0]} } if opts.all || opts.last >= len(containers) { return containers } for _, ctr := range containers { if ctr.State.Status == runningState { containersOutput = append(containersOutput, ctr) } } return containersOutput } // getTemplateOutput returns the modified container information func getTemplateOutput(containers []*libkpod.ContainerData, opts psOptions) (psOutput []psTemplateParams) { var status string for _, ctr := range containers { ctrID := ctr.ID runningFor := units.HumanDuration(time.Since(ctr.State.Created)) createdAt := runningFor + " ago" command := getCommand(ctr.ImageCreatedBy) imageName := ctr.FromImage mounts := getMounts(ctr.Mounts, opts.noTrunc) ports := getPorts(ctr.Config.ExposedPorts) size := units.HumanSize(float64(ctr.SizeRootFs)) labels := getLabels(ctr.Labels) switch ctr.State.Status { case "stopped": status = "Exited (" + strconv.FormatInt(int64(ctr.State.ExitCode), 10) + ") " + runningFor + " ago" case runningState: status = "Up " + runningFor + " ago" default: status = "Created" } if !opts.noTrunc { ctrID = ctr.ID[:idTruncLength] imageName = getImageName(ctr.FromImage) } 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, } psOutput = append(psOutput, params) } return } // getJSONOutput returns the container info in its raw form func getJSONOutput(containers []*libkpod.ContainerData) (psOutput []psJSONParams) { for _, ctr := range containers { params := psJSONParams{ ID: ctr.ID, Image: ctr.FromImage, Command: ctr.ImageCreatedBy, CreatedAt: ctr.State.Created, RunningFor: time.Since(ctr.State.Created), Status: ctr.State.Status, Ports: ctr.Config.ExposedPorts, Size: ctr.SizeRootFs, Names: ctr.Name, Labels: ctr.Labels, Mounts: ctr.Mounts, ContainerRunning: ctr.State.Status == runningState, } psOutput = append(psOutput, params) } return } func generatePsOutput(containers []*libkpod.ContainerData, server *libkpod.ContainerServer, opts psOptions) error { containersOutput := getContainers(containers, opts) if len(containersOutput) == 0 { return nil } var out formats.Writer switch opts.format { case formats.JSONString: psOutput := getJSONOutput(containersOutput) out = formats.JSONStructArray{Output: psToGeneric([]psTemplateParams{}, psOutput)} default: psOutput := getTemplateOutput(containersOutput, opts) out = formats.StdoutTemplateArray{Output: psToGeneric(psOutput, []psJSONParams{}), Template: opts.format, Fields: psOutput[0].headerMap()} } return formats.Writer(out).Out() } // getCommand gets the actual command from the whole command func getCommand(cmd string) string { reg, err := regexp.Compile(".*\\[|\\].*") if err != nil { return "" } arr := strings.Split(reg.ReplaceAllLiteralString(cmd, ""), ",") return strings.Join(arr, ",") } // getImageName shortens the image name func getImageName(img string) string { arr := strings.Split(img, "/") if arr[0] == "docker.io" && arr[1] == "library" { img = strings.Join(arr[2:], "/") } else if arr[0] == "docker.io" { img = strings.Join(arr[1:], "/") } return img } // getLabels converts the labels to a string of the form "key=value, key2=value2" func getLabels(labels fields.Set) 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, ",") } // FilterParamsPS contains the filter options for ps type FilterParamsPS struct { id string label string name string exited int32 status string ancestor string before time.Time since time.Time volume string } // parseFilter takes a filter string and a list of containers and filters it func parseFilter(filter string, containers []*oci.Container) (*FilterParamsPS, error) { params := new(FilterParamsPS) allFilters := strings.Split(filter, ",") for _, param := range allFilters { pair := strings.SplitN(param, "=", 2) switch strings.TrimSpace(pair[0]) { case "id": params.id = pair[1] case "label": params.label = pair[1] case "name": params.name = pair[1] case "exited": exitedCode, err := strconv.ParseInt(pair[1], 10, 32) if err != nil { return nil, errors.Errorf("exited code out of range %q", pair[1]) } params.exited = int32(exitedCode) case "status": params.status = pair[1] case "ancestor": params.ancestor = pair[1] case "before": if ctr, err := findContainer(containers, pair[1]); err == nil { params.before = ctr.CreatedAt() } else { return nil, errors.Wrapf(err, "no such container %q", pair[1]) } case "since": if ctr, err := findContainer(containers, pair[1]); err == nil { params.before = ctr.CreatedAt() } else { return nil, errors.Wrapf(err, "no such container %q", pair[1]) } case "volume": params.volume = pair[1] default: return nil, errors.Errorf("invalid filter %q", pair[0]) } } return params, nil } // findContainer finds a container with a specific name or id from a list of containers func findContainer(containers []*oci.Container, ref string) (*oci.Container, error) { for _, ctr := range containers { if strings.HasPrefix(ctr.ID(), ref) || ctr.Name() == ref { return ctr, nil } } return nil, errors.Errorf("could not find container") } // matchesFilter checks if a container matches all the filter parameters func matchesFilter(ctrData *libkpod.ContainerData, params *FilterParamsPS) bool { if params == nil { return true } if params.id != "" && !matchesID(ctrData, params.id) { return false } if params.name != "" && !matchesName(ctrData, params.name) { return false } if !params.before.IsZero() && !matchesBeforeContainer(ctrData, params.before) { return false } if !params.since.IsZero() && !matchesSinceContainer(ctrData, params.since) { return false } if params.exited > 0 && !matchesExited(ctrData, params.exited) { return false } if params.status != "" && !matchesStatus(ctrData, params.status) { return false } if params.ancestor != "" && !matchesAncestor(ctrData, params.ancestor) { return false } if params.label != "" && !matchesLabel(ctrData, params.label) { return false } if params.volume != "" && !matchesVolume(ctrData, params.volume) { return false } return true } // GetContainersMatchingFilter returns a slice of all the containers that match the provided filter parameters func getContainersMatchingFilter(containers []*oci.Container, filter *FilterParamsPS, server *libkpod.ContainerServer) []*libkpod.ContainerData { var filteredCtrs []*libkpod.ContainerData for _, ctr := range containers { ctrData, err := server.GetContainerData(ctr.ID(), true) if err != nil { logrus.Warn("unable to get container data for matched container") } if filter == nil || matchesFilter(ctrData, filter) { filteredCtrs = append(filteredCtrs, ctrData) } } return filteredCtrs } // matchesID returns true if the id's match func matchesID(ctrData *libkpod.ContainerData, id string) bool { return strings.HasPrefix(ctrData.ID, id) } // matchesBeforeContainer returns true if the container was created before the filter image func matchesBeforeContainer(ctrData *libkpod.ContainerData, beforeTime time.Time) bool { return ctrData.State.Created.Before(beforeTime) } // matchesSincecontainer returns true if the container was created since the filter image func matchesSinceContainer(ctrData *libkpod.ContainerData, sinceTime time.Time) bool { return ctrData.State.Created.After(sinceTime) } // matchesLabel returns true if the container label matches that of the filter label func matchesLabel(ctrData *libkpod.ContainerData, label string) bool { pair := strings.SplitN(label, "=", 2) if val, ok := ctrData.Labels[pair[0]]; ok { if len(pair) == 2 && val == pair[1] { return true } if len(pair) == 1 { return true } return false } return false } // matchesName returns true if the names are identical func matchesName(ctrData *libkpod.ContainerData, name string) bool { return ctrData.Name == name } // matchesExited returns true if the exit codes are identical func matchesExited(ctrData *libkpod.ContainerData, exited int32) bool { return ctrData.State.ExitCode == exited } // matchesStatus returns true if the container status matches that of filter status func matchesStatus(ctrData *libkpod.ContainerData, status string) bool { return ctrData.State.Status == status } // matchesAncestor returns true if filter ancestor is in container image name func matchesAncestor(ctrData *libkpod.ContainerData, ancestor string) bool { return strings.Contains(ctrData.FromImage, ancestor) } // matchesVolue returns true if the volume mounted or path to volue of the container matches that of filter volume func matchesVolume(ctrData *libkpod.ContainerData, volume string) bool { for _, vol := range ctrData.Mounts { if strings.Contains(vol.Source, volume) { return true } } return false }