573 lines
16 KiB
Go
573 lines
16 KiB
Go
package containers
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containers/common/libnetwork/types"
|
|
"github.com/containers/common/pkg/completion"
|
|
"github.com/containers/common/pkg/report"
|
|
"github.com/containers/podman/v5/cmd/podman/common"
|
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
|
"github.com/containers/podman/v5/cmd/podman/utils"
|
|
"github.com/containers/podman/v5/cmd/podman/validate"
|
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
|
"github.com/docker/go-units"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
psDescription = "Prints out information about the containers"
|
|
psCommand = &cobra.Command{
|
|
Use: "ps [options]",
|
|
Short: "List containers",
|
|
Long: psDescription,
|
|
RunE: ps,
|
|
Args: validate.NoArgs,
|
|
ValidArgsFunction: completion.AutocompleteNone,
|
|
Example: `podman ps -a
|
|
podman ps -a --format "{{.ID}} {{.Image}} {{.Labels}} {{.Mounts}}"
|
|
podman ps --size --sort names`,
|
|
}
|
|
|
|
psContainerCommand = &cobra.Command{
|
|
Use: psCommand.Use,
|
|
Short: psCommand.Short,
|
|
Long: psCommand.Long,
|
|
RunE: psCommand.RunE,
|
|
Args: psCommand.Args,
|
|
ValidArgsFunction: psCommand.ValidArgsFunction,
|
|
Example: strings.ReplaceAll(psCommand.Example, "podman ps", "podman container ps"),
|
|
}
|
|
)
|
|
var (
|
|
listOpts = entities.ContainerListOptions{
|
|
Filters: make(map[string][]string),
|
|
}
|
|
filters []string
|
|
noTrunc bool
|
|
)
|
|
|
|
func init() {
|
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
|
Command: psCommand,
|
|
})
|
|
listFlagSet(psCommand)
|
|
validate.AddLatestFlag(psCommand, &listOpts.Latest)
|
|
|
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
|
Command: psContainerCommand,
|
|
Parent: containerCmd,
|
|
})
|
|
listFlagSet(psContainerCommand)
|
|
validate.AddLatestFlag(psContainerCommand, &listOpts.Latest)
|
|
}
|
|
|
|
func listFlagSet(cmd *cobra.Command) {
|
|
flags := cmd.Flags()
|
|
|
|
flags.BoolVarP(&listOpts.All, "all", "a", false, "Show all the containers, default is only running containers")
|
|
flags.BoolVar(&listOpts.External, "external", false, "Show containers in storage not controlled by Podman")
|
|
|
|
filterFlagName := "filter"
|
|
flags.StringArrayVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
|
|
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
|
|
|
|
formatFlagName := "format"
|
|
flags.StringVar(&listOpts.Format, formatFlagName, "", "Pretty-print containers to JSON or using a Go template")
|
|
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&psReporter{}))
|
|
|
|
lastFlagName := "last"
|
|
flags.IntVarP(&listOpts.Last, lastFlagName, "n", -1, "Print the n last created containers (all states)")
|
|
_ = cmd.RegisterFlagCompletionFunc(lastFlagName, completion.AutocompleteNone)
|
|
|
|
flags.BoolVar(&listOpts.Namespace, "ns", false, "Display namespace information")
|
|
flags.BoolVar(&noTrunc, "no-trunc", false, "Display the extended information")
|
|
flags.BoolVarP(&listOpts.Pod, "pod", "p", false, "Print the ID and name of the pod the containers are associated with")
|
|
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Print the numeric IDs of the containers only")
|
|
flags.Bool("noheading", false, "Do not print headers")
|
|
flags.BoolVarP(&listOpts.Size, "size", "s", false, "Display the total file sizes")
|
|
flags.BoolVar(&listOpts.Sync, "sync", false, "Sync container state with OCI runtime")
|
|
|
|
watchFlagName := "watch"
|
|
flags.UintVarP(&listOpts.Watch, watchFlagName, "w", 0, "Watch the ps output on an interval in seconds")
|
|
_ = cmd.RegisterFlagCompletionFunc(watchFlagName, completion.AutocompleteNone)
|
|
|
|
sort := validate.Value(&listOpts.Sort, "command", "created", "id", "image", "names", "runningfor", "size", "status")
|
|
sortFlagName := "sort"
|
|
flags.Var(sort, sortFlagName, "Sort output by: "+sort.Choices())
|
|
_ = cmd.RegisterFlagCompletionFunc(sortFlagName, common.AutocompletePsSort)
|
|
|
|
flags.SetNormalizeFunc(utils.AliasFlags)
|
|
}
|
|
func checkFlags(c *cobra.Command) error {
|
|
// latest, and last are mutually exclusive.
|
|
if listOpts.Last >= 0 && listOpts.Latest {
|
|
return errors.New("last and latest are mutually exclusive")
|
|
}
|
|
// Quiet conflicts with size and namespace and is overridden by a Go
|
|
// template.
|
|
if listOpts.Quiet {
|
|
if listOpts.Size || listOpts.Namespace {
|
|
return errors.New("quiet conflicts with size and namespace")
|
|
}
|
|
}
|
|
// Size and namespace conflict with each other
|
|
if listOpts.Size && listOpts.Namespace {
|
|
return errors.New("size and namespace options conflict")
|
|
}
|
|
|
|
if listOpts.Watch > 0 && listOpts.Latest {
|
|
return errors.New("the watch and latest flags cannot be used together")
|
|
}
|
|
podmanConfig := registry.PodmanConfig()
|
|
if podmanConfig.ContainersConf.Engine.Namespace != "" {
|
|
if c.Flag("storage").Changed && listOpts.External {
|
|
return errors.New("--namespace and --external flags can not both be set")
|
|
}
|
|
listOpts.External = false
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func jsonOut(responses []entities.ListContainer) error {
|
|
type jsonFormat struct {
|
|
entities.ListContainer
|
|
Created int64
|
|
}
|
|
r := make([]jsonFormat, 0)
|
|
for _, con := range responses {
|
|
con.CreatedAt = units.HumanDuration(time.Since(con.Created)) + " ago"
|
|
con.Status = psReporter{con}.Status()
|
|
jf := jsonFormat{
|
|
ListContainer: con,
|
|
Created: con.Created.Unix(),
|
|
}
|
|
r = append(r, jf)
|
|
}
|
|
b, err := json.MarshalIndent(r, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(b))
|
|
return nil
|
|
}
|
|
|
|
func quietOut(responses []entities.ListContainer) {
|
|
for _, r := range responses {
|
|
id := r.ID
|
|
if !noTrunc {
|
|
id = id[0:12]
|
|
}
|
|
fmt.Println(id)
|
|
}
|
|
}
|
|
|
|
func getResponses() ([]entities.ListContainer, error) {
|
|
responses, err := registry.ContainerEngine().ContainerList(registry.Context(), listOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(listOpts.Sort) > 0 {
|
|
responses, err = entities.SortPsOutput(listOpts.Sort, responses)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return responses, nil
|
|
}
|
|
|
|
func ps(cmd *cobra.Command, _ []string) error {
|
|
if err := checkFlags(cmd); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !listOpts.Pod {
|
|
listOpts.Pod = strings.Contains(listOpts.Format, ".PodName")
|
|
}
|
|
|
|
for _, f := range filters {
|
|
fname, filter, hasFilter := strings.Cut(f, "=")
|
|
if !hasFilter {
|
|
return fmt.Errorf("invalid filter %q", f)
|
|
}
|
|
listOpts.Filters[fname] = append(listOpts.Filters[fname], filter)
|
|
}
|
|
listContainers, err := getResponses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(listOpts.Sort) > 0 {
|
|
listContainers, err = entities.SortPsOutput(listOpts.Sort, listContainers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case report.IsJSON(listOpts.Format):
|
|
return jsonOut(listContainers)
|
|
case listOpts.Quiet && !cmd.Flags().Changed("format"):
|
|
quietOut(listContainers)
|
|
return nil
|
|
}
|
|
|
|
responses := make([]psReporter, 0, len(listContainers))
|
|
for _, r := range listContainers {
|
|
responses = append(responses, psReporter{r})
|
|
}
|
|
|
|
hdrs, format := createPsOut()
|
|
|
|
var origin report.Origin
|
|
noHeading, _ := cmd.Flags().GetBool("noheading")
|
|
if cmd.Flags().Changed("format") {
|
|
noHeading = noHeading || !report.HasTable(listOpts.Format)
|
|
format = listOpts.Format
|
|
origin = report.OriginUser
|
|
} else {
|
|
origin = report.OriginPodman
|
|
}
|
|
ns := strings.NewReplacer(".Namespaces.", ".")
|
|
format = ns.Replace(format)
|
|
|
|
rpt, err := report.New(os.Stdout, cmd.Name()).Parse(origin, format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rpt.Flush()
|
|
|
|
headers := func() error { return nil }
|
|
if !noHeading {
|
|
headers = func() error {
|
|
return rpt.Execute(hdrs)
|
|
}
|
|
}
|
|
|
|
switch {
|
|
// Output table Watch > 0 will refresh screen
|
|
case listOpts.Watch > 0:
|
|
// responses will grow to the largest number of processes reported on, but will not thrash the gc
|
|
var responses []psReporter
|
|
for ; ; responses = responses[:0] {
|
|
ctnrs, err := getResponses()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, r := range ctnrs {
|
|
responses = append(responses, psReporter{r})
|
|
}
|
|
|
|
common.ClearScreen()
|
|
|
|
if err := headers(); err != nil {
|
|
return err
|
|
}
|
|
if err := rpt.Execute(responses); err != nil {
|
|
return err
|
|
}
|
|
if err := rpt.Flush(); err != nil {
|
|
// we usually do not care about Flush() failures but here do not loop if Flush() has failed
|
|
return err
|
|
}
|
|
|
|
time.Sleep(time.Duration(listOpts.Watch) * time.Second)
|
|
}
|
|
default:
|
|
if err := headers(); err != nil {
|
|
return err
|
|
}
|
|
if err := rpt.Execute(responses); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cannot use report.Headers() as it doesn't support structures as fields
|
|
func createPsOut() ([]map[string]string, string) {
|
|
hdrs := report.Headers(psReporter{}, map[string]string{
|
|
"Cgroup": "cgroupns",
|
|
"CreatedHuman": "created",
|
|
"ID": "container id",
|
|
"IPC": "ipc",
|
|
"MNT": "mnt",
|
|
"NET": "net",
|
|
"Networks": "networks",
|
|
"PIDNS": "pidns",
|
|
"Pod": "pod id",
|
|
"PodName": "podname", // undo camelcase space break
|
|
"Restarts": "restarts",
|
|
"RunningFor": "running for",
|
|
"UTS": "uts",
|
|
"User": "userns",
|
|
})
|
|
|
|
var row string
|
|
if listOpts.Namespace {
|
|
row = "{{.ID}}\t{{.Names}}\t{{.Pid}}\t{{.Namespaces.Cgroup}}\t{{.Namespaces.IPC}}\t{{.Namespaces.MNT}}\t{{.Namespaces.NET}}\t{{.Namespaces.PIDNS}}\t{{.Namespaces.User}}\t{{.Namespaces.UTS}}"
|
|
} else {
|
|
row = "{{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
|
|
|
if listOpts.Pod {
|
|
row += "\t{{.Pod}}\t{{.PodName}}"
|
|
}
|
|
|
|
if listOpts.Size {
|
|
row += "\t{{.Size}}"
|
|
}
|
|
}
|
|
return hdrs, "{{range .}}" + row + "\n{{end -}}"
|
|
}
|
|
|
|
type psReporter struct {
|
|
entities.ListContainer
|
|
}
|
|
|
|
// ImageID returns the ID of the container
|
|
func (l psReporter) ImageID() string {
|
|
if !noTrunc {
|
|
return l.ListContainer.ImageID[0:12]
|
|
}
|
|
return l.ListContainer.ImageID
|
|
}
|
|
|
|
// Label returns a map of the pod's labels
|
|
func (l psReporter) Label(name string) string {
|
|
return l.ListContainer.Labels[name]
|
|
}
|
|
|
|
// ID returns the ID of the container
|
|
func (l psReporter) ID() string {
|
|
if !noTrunc {
|
|
return l.ListContainer.ID[0:12]
|
|
}
|
|
return l.ListContainer.ID
|
|
}
|
|
|
|
// Pod returns the ID of the pod the container
|
|
// belongs to and appropriately truncates the ID
|
|
func (l psReporter) Pod() string {
|
|
if !noTrunc && len(l.ListContainer.Pod) > 0 {
|
|
return l.ListContainer.Pod[0:12]
|
|
}
|
|
return l.ListContainer.Pod
|
|
}
|
|
|
|
// Status returns the container status in the default ps output format.
|
|
func (l psReporter) Status() string {
|
|
var state string
|
|
switch l.ListContainer.State {
|
|
case "running":
|
|
t := units.HumanDuration(time.Since(time.Unix(l.StartedAt, 0)))
|
|
state = "Up " + t
|
|
case "exited", "stopped":
|
|
t := units.HumanDuration(time.Since(time.Unix(l.ExitedAt, 0)))
|
|
state = fmt.Sprintf("Exited (%d) %s ago", l.ExitCode, t)
|
|
default:
|
|
// Need to capitalize the first letter to match Docker.
|
|
|
|
// strings.Title is deprecated since go 1.18
|
|
// However for our use case it is still fine. The recommended replacement
|
|
// is adding about 400kb binary size so let's keep using this for now.
|
|
//nolint:staticcheck
|
|
state = strings.Title(l.ListContainer.State)
|
|
}
|
|
hc := l.ListContainer.Status
|
|
if hc != "" {
|
|
state += " (" + hc + ")"
|
|
}
|
|
return state
|
|
}
|
|
|
|
func (l psReporter) Restarts() string {
|
|
return strconv.Itoa(int(l.ListContainer.Restarts))
|
|
}
|
|
|
|
func (l psReporter) RunningFor() string {
|
|
return l.CreatedHuman()
|
|
}
|
|
|
|
// Command returns the container command in string format
|
|
func (l psReporter) Command() string {
|
|
command := strings.Join(l.ListContainer.Command, " ")
|
|
if !noTrunc {
|
|
if len(command) > 17 {
|
|
return command[0:17] + "..."
|
|
}
|
|
}
|
|
return command
|
|
}
|
|
|
|
// Size returns the rootfs and virtual sizes in human duration in
|
|
// and output form (string) suitable for ps
|
|
func (l psReporter) Size() string {
|
|
if l.ListContainer.Size == nil {
|
|
logrus.Errorf("Size format requires --size option")
|
|
return ""
|
|
}
|
|
|
|
virt := units.HumanSizeWithPrecision(float64(l.ListContainer.Size.RootFsSize), 3)
|
|
s := units.HumanSizeWithPrecision(float64(l.ListContainer.Size.RwSize), 3)
|
|
return fmt.Sprintf("%s (virtual %s)", s, virt)
|
|
}
|
|
|
|
// Names returns the container name in string format
|
|
func (l psReporter) Names() string {
|
|
return l.ListContainer.Names[0]
|
|
}
|
|
|
|
// Networks returns the container network names in string format
|
|
func (l psReporter) Networks() string {
|
|
return strings.Join(l.ListContainer.Networks, ",")
|
|
}
|
|
|
|
// Ports converts from Portmappings to the string form
|
|
// required by ps
|
|
func (l psReporter) Ports() string {
|
|
return portsToString(l.ListContainer.Ports, l.ListContainer.ExposedPorts)
|
|
}
|
|
|
|
// CreatedAt returns the container creation time in string format. podman
|
|
// and docker both return a timestamped value for createdat
|
|
func (l psReporter) CreatedAt() string {
|
|
return l.Created.String()
|
|
}
|
|
|
|
// CreatedHuman allows us to output the created time in human readable format
|
|
func (l psReporter) CreatedHuman() string {
|
|
return units.HumanDuration(time.Since(l.Created)) + " ago"
|
|
}
|
|
|
|
// Cgroup exposes .Namespaces.Cgroup
|
|
func (l psReporter) Cgroup() string {
|
|
return l.Namespaces.Cgroup
|
|
}
|
|
|
|
// IPC exposes .Namespaces.IPC
|
|
func (l psReporter) IPC() string {
|
|
return l.Namespaces.IPC
|
|
}
|
|
|
|
// MNT exposes .Namespaces.MNT
|
|
func (l psReporter) MNT() string {
|
|
return l.Namespaces.MNT
|
|
}
|
|
|
|
// NET exposes .Namespaces.NET
|
|
func (l psReporter) NET() string {
|
|
return l.Namespaces.NET
|
|
}
|
|
|
|
// PIDNS exposes .Namespaces.PIDNS
|
|
func (l psReporter) PIDNS() string {
|
|
return l.Namespaces.PIDNS
|
|
}
|
|
|
|
// User exposes .Namespaces.User
|
|
func (l psReporter) User() string {
|
|
return l.Namespaces.User
|
|
}
|
|
|
|
// UTS exposes .Namespaces.UTS
|
|
func (l psReporter) UTS() string {
|
|
return l.Namespaces.UTS
|
|
}
|
|
|
|
// portsToString converts the ports used to a string of the from "port1, port2"
|
|
// and also groups a continuous list of ports into a readable format.
|
|
// The format is IP:HostPort(-Range)->ContainerPort(-Range)/Proto
|
|
func portsToString(ports []types.PortMapping, exposedPorts map[uint16][]string) string {
|
|
if len(ports) == 0 && len(exposedPorts) == 0 {
|
|
return ""
|
|
}
|
|
portMap := make(map[string]struct{})
|
|
|
|
sb := &strings.Builder{}
|
|
for _, port := range ports {
|
|
hostIP := port.HostIP
|
|
if hostIP == "" {
|
|
hostIP = "0.0.0.0"
|
|
}
|
|
if port.Range > 1 {
|
|
fmt.Fprintf(sb, "%s:%d-%d->%d-%d/%s, ",
|
|
hostIP, port.HostPort, port.HostPort+port.Range-1,
|
|
port.ContainerPort, port.ContainerPort+port.Range-1, port.Protocol)
|
|
for i := range port.Range {
|
|
portMap[fmt.Sprintf("%d/%s", port.ContainerPort+i, port.Protocol)] = struct{}{}
|
|
}
|
|
} else {
|
|
fmt.Fprintf(sb, "%s:%d->%d/%s, ",
|
|
hostIP, port.HostPort,
|
|
port.ContainerPort, port.Protocol)
|
|
portMap[fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// iterating a map is not deterministic so let's convert slice first and sort by protocol and port to make it deterministic
|
|
sortedPorts := make([]exposedPort, 0, len(exposedPorts))
|
|
for port, protocols := range exposedPorts {
|
|
for _, proto := range protocols {
|
|
sortedPorts = append(sortedPorts, exposedPort{num: port, protocol: proto})
|
|
}
|
|
}
|
|
slices.SortFunc(sortedPorts, func(a, b exposedPort) int {
|
|
protoCmp := cmp.Compare(a.protocol, b.protocol)
|
|
if protoCmp != 0 {
|
|
return protoCmp
|
|
}
|
|
return cmp.Compare(a.num, b.num)
|
|
})
|
|
|
|
var prevPort *exposedPort
|
|
for _, port := range sortedPorts {
|
|
// only if it was not published already so we do not have duplicates
|
|
if _, ok := portMap[fmt.Sprintf("%d/%s", port.num, port.protocol)]; ok {
|
|
continue
|
|
}
|
|
|
|
if prevPort != nil {
|
|
// if the prevPort is one below us we know it is a range, do not print it and just increase the range by one
|
|
if prevPort.protocol == port.protocol && prevPort.num == port.num-prevPort.portRange-1 {
|
|
prevPort.portRange++
|
|
continue
|
|
}
|
|
// the new port is not a range with the previous one so print it
|
|
printExposedPort(prevPort, sb)
|
|
}
|
|
prevPort = &port
|
|
}
|
|
// do not forget to print the last port
|
|
if prevPort != nil {
|
|
printExposedPort(prevPort, sb)
|
|
}
|
|
|
|
display := sb.String()
|
|
// make sure to trim the last ", " of the string
|
|
return display[:len(display)-2]
|
|
}
|
|
|
|
type exposedPort struct {
|
|
num uint16
|
|
protocol string
|
|
// portRange is 0 indexed
|
|
portRange uint16
|
|
}
|
|
|
|
func printExposedPort(port *exposedPort, sb *strings.Builder) {
|
|
// exposed ports do not have a host part and are just written as "NUM[-RANGE]/PROTO"
|
|
if port.portRange > 0 {
|
|
fmt.Fprintf(sb, "%d-%d/%s, ", port.num, port.num+port.portRange, port.protocol)
|
|
} else {
|
|
fmt.Fprintf(sb, "%d/%s, ", port.num, port.protocol)
|
|
}
|
|
}
|