Add 'kpod ps' command
kpod ps lists the containers currently stored Displays the list of containers Signed-off-by: umohnani8 <umohnani@redhat.com>
This commit is contained in:
parent
c67859731f
commit
35ca80abe6
10 changed files with 1001 additions and 24 deletions
547
cmd/kpod/ps.go
Normal file
547
cmd/kpod/ps.go
Normal file
|
@ -0,0 +1,547 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 psOutputParams struct {
|
||||
ID string `json:"id"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
CreatedAt string `json:"created"`
|
||||
RunningFor string `json:"running"`
|
||||
Status string `json:"status"`
|
||||
Ports string `json:"ports"`
|
||||
Size string `json:"size"`
|
||||
Names string `json:"names"`
|
||||
Labels string `json:"labels"`
|
||||
Mounts string `json:"mounts"`
|
||||
}
|
||||
|
||||
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 psOutput(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(params []psOutputParams) []interface{} {
|
||||
genericParams := make([]interface{}, len(params))
|
||||
for i, v := range params {
|
||||
genericParams[i] = interface{}(v)
|
||||
}
|
||||
return genericParams
|
||||
}
|
||||
|
||||
// generate the accurate header based on template given
|
||||
func (p *psOutputParams) 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
|
||||
}
|
||||
|
||||
func psOutput(containers []*libkpod.ContainerData, server *libkpod.ContainerServer, opts psOptions) error {
|
||||
var (
|
||||
output []psOutputParams
|
||||
containersOutput []*libkpod.ContainerData
|
||||
status string
|
||||
ctrID string
|
||||
command string
|
||||
runningFor string
|
||||
imageName string
|
||||
mounts string
|
||||
ports string
|
||||
size string
|
||||
labels string
|
||||
createdAt string
|
||||
)
|
||||
|
||||
if len(containers) == 0 {
|
||||
fmt.Println("hereee")
|
||||
return nil
|
||||
}
|
||||
containersOutput = getContainers(containers, opts)
|
||||
|
||||
for _, ctr := range containersOutput {
|
||||
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 := psOutputParams{
|
||||
ID: ctrID,
|
||||
Image: imageName,
|
||||
Command: command,
|
||||
CreatedAt: createdAt,
|
||||
RunningFor: runningFor,
|
||||
Status: status,
|
||||
Ports: ports,
|
||||
Size: size,
|
||||
Names: ctr.Name,
|
||||
Labels: labels,
|
||||
Mounts: mounts,
|
||||
}
|
||||
output = append(output, params)
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out formats.Writer
|
||||
|
||||
switch opts.format {
|
||||
case formats.JSONString:
|
||||
out = formats.JSONStructArray{Output: psToGeneric(output)}
|
||||
default:
|
||||
out = formats.StdoutTemplateArray{Output: psToGeneric(output), Template: opts.format, Fields: output[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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue