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) } }