diff options
33 files changed, 562 insertions, 61 deletions
diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 37e2eff59..82434e9cc 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -227,7 +227,7 @@ func createPsOut() (string, string) { } headers := defaultHeaders row += "{{.ID}}" - row += "\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.State}}\t{{.Ports}}\t{{.Names}}" + row += "\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" if listOpts.Pod { headers += "\tPOD ID\tPODNAME" @@ -294,6 +294,11 @@ func (l psReporter) State() string { return state } +// Status is a synonym for State() +func (l psReporter) Status() string { + return l.State() +} + // Command returns the container command in string format func (l psReporter) Command() string { return strings.Join(l.ListContainer.Command, " ") diff --git a/cmd/podman/images/load.go b/cmd/podman/images/load.go index 23c657b59..f49f95002 100644 --- a/cmd/podman/images/load.go +++ b/cmd/podman/images/load.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" "os" + "strings" "github.com/containers/image/v5/docker/reference" "github.com/containers/libpod/cmd/podman/parse" @@ -89,6 +90,6 @@ func load(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println("Loaded image: " + response.Name) + fmt.Println("Loaded image(s): " + strings.Join(response.Names, ",")) return nil } diff --git a/cmd/podman/pods/stats.go b/cmd/podman/pods/stats.go new file mode 100644 index 000000000..7c3597d9a --- /dev/null +++ b/cmd/podman/pods/stats.go @@ -0,0 +1,189 @@ +package pods + +import ( + "context" + "fmt" + "os" + "reflect" + "strings" + "text/tabwriter" + "text/template" + "time" + + "github.com/buger/goterm" + "github.com/containers/buildah/pkg/formats" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/util/camelcase" + "github.com/spf13/cobra" +) + +type podStatsOptionsWrapper struct { + entities.PodStatsOptions + + // Format - pretty-print to JSON or a go template. + Format string + // NoReset - do not reset the screen when streaming. + NoReset bool + // NoStream - do not stream stats but write them once. + NoStream bool +} + +var ( + statsOptions = podStatsOptionsWrapper{} + statsDescription = `Display the containers' resource-usage statistics of one or more running pod` + // Command: podman pod _pod_ + statsCmd = &cobra.Command{ + Use: "stats [flags] [POD...]", + Short: "Display resource-usage statistics of pods", + Long: statsDescription, + RunE: stats, + Example: `podman pod stats + podman pod stats a69b23034235 named-pod + podman pod stats --latest + podman pod stats --all`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: statsCmd, + Parent: podCmd, + }) + + flags := statsCmd.Flags() + flags.BoolVarP(&statsOptions.All, "all", "a", false, "Provide stats for all pods") + flags.StringVar(&statsOptions.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template") + flags.BoolVarP(&statsOptions.Latest, "latest", "l", false, "Provide stats on the latest pod Podman is aware of") + flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen when streaming") + flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result") + + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func stats(cmd *cobra.Command, args []string) error { + // Validate input. + if err := entities.ValidatePodStatsOptions(args, &statsOptions.PodStatsOptions); err != nil { + return err + } + + format := statsOptions.Format + doJson := strings.ToLower(format) == formats.JSONString + header := getPodStatsHeader(format) + + for { + reports, err := registry.ContainerEngine().PodStats(context.Background(), args, statsOptions.PodStatsOptions) + if err != nil { + return err + } + // Print the stats in the requested format and configuration. + if doJson { + if err := printJSONPodStats(reports); err != nil { + return err + } + } else { + if !statsOptions.NoReset { + goterm.Clear() + goterm.MoveCursor(1, 1) + goterm.Flush() + } + if len(format) == 0 { + printPodStatsLines(reports) + } else if err := printFormattedPodStatsLines(format, reports, header); err != nil { + return err + } + } + if statsOptions.NoStream { + break + } + time.Sleep(time.Second) + } + + return nil +} + +func printJSONPodStats(stats []*entities.PodStatsReport) error { + b, err := json.MarshalIndent(&stats, "", " ") + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "%s\n", string(b)) + return nil +} + +func printPodStatsLines(stats []*entities.PodStatsReport) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + outFormat := "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" + fmt.Fprintf(w, outFormat, "POD", "CID", "NAME", "CPU %", "MEM USAGE/ LIMIT", "MEM %", "NET IO", "BLOCK IO", "PIDS") + for _, i := range stats { + if len(stats) == 0 { + fmt.Fprintf(w, outFormat, i.Pod, "--", "--", "--", "--", "--", "--", "--", "--") + } else { + fmt.Fprintf(w, outFormat, i.Pod, i.CID, i.Name, i.CPU, i.MemUsage, i.Mem, i.NetIO, i.BlockIO, i.PIDS) + } + } + w.Flush() +} + +func printFormattedPodStatsLines(format string, stats []*entities.PodStatsReport, headerNames map[string]string) error { + if len(stats) == 0 { + return nil + } + + // Use a tabwriter to align column format + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + // Spit out the header if "table" is present in the format + if strings.HasPrefix(format, "table") { + hformat := strings.Replace(strings.TrimSpace(format[5:]), " ", "\t", -1) + format = hformat + headerTmpl, err := template.New("header").Parse(hformat) + if err != nil { + return err + } + if err := headerTmpl.Execute(w, headerNames); err != nil { + return err + } + fmt.Fprintln(w, "") + } + + // Spit out the data rows now + dataTmpl, err := template.New("data").Parse(format) + if err != nil { + return err + } + for _, s := range stats { + if err := dataTmpl.Execute(w, s); err != nil { + return err + } + fmt.Fprintln(w, "") + } + // Flush the writer + return w.Flush() + +} + +// getPodStatsHeader returns the stats header for the specified options. +func getPodStatsHeader(format string) map[string]string { + headerNames := make(map[string]string) + if format == "" { + return headerNames + } + // Make a map of the field names for the headers + v := reflect.ValueOf(entities.PodStatsReport{}) + t := v.Type() + for i := 0; i < t.NumField(); i++ { + split := camelcase.Split(t.Field(i).Name) + value := strings.ToUpper(strings.Join(split, " ")) + switch value { + case "CPU", "MEM": + value += " %" + case "MEM USAGE": + value = "MEM USAGE / LIMIT" + } + headerNames[t.Field(i).Name] = value + } + return headerNames +} diff --git a/docs/source/markdown/podman-pod-stats.1.md b/docs/source/markdown/podman-pod-stats.1.md index 962edbda0..f70a5a919 100644 --- a/docs/source/markdown/podman-pod-stats.1.md +++ b/docs/source/markdown/podman-pod-stats.1.md @@ -7,7 +7,7 @@ podman\-pod\-stats - Display a live stream of resource usage stats for container **podman pod stats** [*options*] [*pod*] ## DESCRIPTION -Display a live stream of containers in one or more pods resource usage statistics +Display a live stream of containers in one or more pods resource usage statistics. Running rootless is only supported on cgroups v2. ## OPTIONS diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 38dfa7ab7..8ee0fb456 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -385,6 +385,16 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) } } + + for _, i := range c.config.Spec.Linux.Namespaces { + if i.Type == spec.UTSNamespace { + hostname := c.Hostname() + g.SetHostname(hostname) + g.AddProcessEnv("HOSTNAME", hostname) + break + } + } + if c.config.UTSNsCtr != "" { if err := c.addNamespaceContainer(&g, UTSNS, c.config.UTSNsCtr, spec.UTSNamespace); err != nil { return nil, err @@ -418,15 +428,6 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { g.AddAnnotation(annotations.ContainerManager, annotations.ContainerManagerLibpod) } - for _, i := range c.config.Spec.Linux.Namespaces { - if i.Type == spec.UTSNamespace { - hostname := c.Hostname() - g.SetHostname(hostname) - g.AddProcessEnv("HOSTNAME", hostname) - break - } - } - // Only add container environment variable if not already present foundContainerEnv := false for _, env := range g.Config.Process.Env { @@ -583,6 +584,12 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return errors.Wrapf(err, "error retrieving dependency %s of container %s from state", ctr, c.ID()) } + if specNS == spec.UTSNamespace { + hostname := nsCtr.Hostname() + g.SetHostname(hostname) + g.AddProcessEnv("HOSTNAME", hostname) + } + // TODO need unlocked version of this for use in pods nsPath, err := nsCtr.NamespacePath(ns) if err != nil { diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 760ab1b7c..f7be5ce9a 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -283,7 +283,7 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) { return } } - utils.WriteResponse(w, http.StatusOK, entities.ImageLoadReport{Name: loadedImage}) + utils.WriteResponse(w, http.StatusOK, entities.ImageLoadReport{Names: split}) } func ImagesImport(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 618d48ac0..0b15ab0d6 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -11,6 +11,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/specgen/generate" "github.com/containers/libpod/pkg/util" @@ -419,3 +420,44 @@ func PodExists(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusNoContent, "") } + +func PodStats(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + NamesOrIDs []string `schema:"namesOrIDs"` + All bool `schema:"all"` + }{ + // default would go here + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + + // Validate input. + options := entities.PodStatsOptions{All: query.All} + if err := entities.ValidatePodStatsOptions(query.NamesOrIDs, &options); err != nil { + utils.InternalServerError(w, err) + } + + // Collect the stats and send them over the wire. + containerEngine := abi.ContainerEngine{Libpod: runtime} + reports, err := containerEngine.PodStats(r.Context(), query.NamesOrIDs, options) + + // Error checks as documented in swagger. + switch errors.Cause(err) { + case define.ErrNoSuchPod: + utils.Error(w, "one or more pods not found", http.StatusNotFound, err) + return + case nil: + // Nothing to do. + default: + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, reports) +} diff --git a/pkg/api/handlers/swagger/swagger.go b/pkg/api/handlers/swagger/swagger.go index 7d8932f56..0aceaf5f6 100644 --- a/pkg/api/handlers/swagger/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -122,6 +122,13 @@ type swagPodTopResponse struct { } } +// List processes in pod +// swagger:response DocsPodStatsResponse +type swagPodStatsResponse struct { + // in:body + Body []*entities.PodStatsReport +} + // Inspect container // swagger:response LibpodInspectContainerResponse type swagLibpodInspectContainerResponse struct { diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index aafc64353..3253a9be3 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -14,6 +14,9 @@ var ( ErrLinkNotSupport = errors.New("Link is not supported") ) +// TODO: document the exported functions in this file and make them more +// generic (e.g., not tied to one ctr/pod). + // Error formats an API response to an error // // apiMessage and code must match the container API, and are sent to client diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 63060af41..4156dd86b 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -286,9 +286,36 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // 200: // $ref: "#/responses/DocsPodTopResponse" // 404: - // $ref: "#/responses/NoSuchContainer" + // $ref: "#/responses/NoSuchPod" // 500: // $ref: "#/responses/InternalError" r.Handle(VersionedPath("/libpod/pods/{name}/top"), s.APIHandler(libpod.PodTop)).Methods(http.MethodGet) + // swagger:operation GET /libpod/pods/stats pods statsPod + // --- + // tags: + // - pods + // summary: Get stats for one or more pods + // description: Display a live stream of resource usage statistics for the containers in one or more pods + // parameters: + // - in: query + // name: all + // description: Provide statistics for all running pods. + // type: boolean + // - in: query + // name: namesOrIDs + // description: Names or IDs of pods. + // type: array + // items: + // type: string + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/DocsPodTopResponse" + // 404: + // $ref: "#/responses/NoSuchPod" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/stats"), s.APIHandler(libpod.PodStats)).Methods(http.MethodGet) return nil } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index 3c60fa2a0..b213c8c73 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -2,6 +2,7 @@ package pods import ( "context" + "errors" "net/http" "net/url" "strconv" @@ -189,11 +190,6 @@ func Start(ctx context.Context, nameOrID string) (*entities.PodStartReport, erro return &report, response.Process(&report) } -func Stats() error { - // TODO - return bindings.ErrNotImplemented -} - // Stop stops all containers in a Pod. The optional timeout parameter can be // used to override the timeout before the container is killed. func Stop(ctx context.Context, nameOrID string, timeout *int) (*entities.PodStopReport, error) { @@ -264,3 +260,26 @@ func Unpause(ctx context.Context, nameOrID string) (*entities.PodUnpauseReport, } return &report, response.Process(&report) } + +// Stats display resource-usage statistics of one or more pods. +func Stats(ctx context.Context, namesOrIDs []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + if options.Latest { + return nil, errors.New("latest is not supported") + } + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + for _, i := range namesOrIDs { + params.Add("namesOrIDs", i) + } + params.Set("all", strconv.FormatBool(options.All)) + + var reports []*entities.PodStatsReport + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/stats", params) + if err != nil { + return nil, err + } + return reports, response.Process(&reports) +} diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 6b8d6788c..f33e42440 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -3,13 +3,13 @@ package test_bindings import ( "context" "fmt" - "github.com/containers/libpod/libpod/define" "io/ioutil" "os" "os/exec" "path/filepath" "strings" + "github.com/containers/libpod/libpod/define" . "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/specgen" @@ -189,7 +189,7 @@ func (b *bindingTest) restoreImageFromCache(i testImage) { // Run a container within or without a pod // and add or append the alpine image to it func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) (string, error) { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) s.Terminal = false s.Command = []string{"top"} if containerName != nil { diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index e288dc368..c79d89b73 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -360,7 +360,7 @@ var _ = Describe("Podman containers ", func() { It("logging", func() { stdoutChan := make(chan string, 10) - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) s.Terminal = true s.Command = []string{"date", "-R"} r, err := containers.CreateWithSpec(bt.conn, s) @@ -521,7 +521,7 @@ var _ = Describe("Podman containers ", func() { }) It("container init", func() { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) ctr, err := containers.CreateWithSpec(bt.conn, s) Expect(err).To(BeNil()) err = containers.ContainerInit(bt.conn, ctr.ID) diff --git a/pkg/bindings/test/create_test.go b/pkg/bindings/test/create_test.go index f83a9b14d..a63aa79cf 100644 --- a/pkg/bindings/test/create_test.go +++ b/pkg/bindings/test/create_test.go @@ -31,7 +31,7 @@ var _ = Describe("Create containers ", func() { }) It("create a container running top", func() { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) s.Command = []string{"top"} s.Terminal = true s.Name = "top" diff --git a/pkg/bindings/test/info_test.go b/pkg/bindings/test/info_test.go index d0e651134..64f2b458f 100644 --- a/pkg/bindings/test/info_test.go +++ b/pkg/bindings/test/info_test.go @@ -45,7 +45,7 @@ var _ = Describe("Podman info", func() { }) It("podman info container counts", func() { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) _, err := containers.CreateWithSpec(bt.conn, s) Expect(err).To(BeNil()) diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index 7f69eec0c..fd94d93be 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -161,3 +161,31 @@ func SortPsOutput(sortBy string, psOutput SortListContainers) (SortListContainer } return psOutput, nil } + +func (l ListContainer) CGROUPNS() string { + return l.Namespaces.Cgroup +} + +func (l ListContainer) IPC() string { + return l.Namespaces.IPC +} + +func (l ListContainer) MNT() string { + return l.Namespaces.MNT +} + +func (l ListContainer) NET() string { + return l.Namespaces.NET +} + +func (l ListContainer) PIDNS() string { + return l.Namespaces.PIDNS +} + +func (l ListContainer) USERNS() string { + return l.Namespaces.User +} + +func (l ListContainer) UTS() string { + return l.Namespaces.UTS +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 8c5bc3058..502279bcf 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -53,6 +53,7 @@ type ContainerEngine interface { PodRestart(ctx context.Context, namesOrIds []string, options PodRestartOptions) ([]*PodRestartReport, error) PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) PodStart(ctx context.Context, namesOrIds []string, options PodStartOptions) ([]*PodStartReport, error) + PodStats(ctx context.Context, namesOrIds []string, options PodStatsOptions) ([]*PodStatsReport, error) PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error) PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 773cd90b4..460965b34 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -256,7 +256,7 @@ type ImageLoadOptions struct { } type ImageLoadReport struct { - Name string + Names []string } type ImageImportOptions struct { diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index aa1445a6a..a4896ce4d 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -1,6 +1,7 @@ package entities import ( + "errors" "strings" "time" @@ -188,3 +189,50 @@ type PodInspectOptions struct { type PodInspectReport struct { *define.InspectPodData } + +// PodStatsOptions are options for the pod stats command. +type PodStatsOptions struct { + // All - provide stats for all running pods. + All bool + // Latest - provide stats for the latest pod. + Latest bool +} + +// PodStatsReport includes pod-resource statistics data. +type PodStatsReport struct { + CPU string + MemUsage string + Mem string + NetIO string + BlockIO string + PIDS string + Pod string + CID string + Name string +} + +// ValidatePodStatsOptions validates the specified slice and options. Allows +// for sharing code in the front- and the back-end. +func ValidatePodStatsOptions(args []string, options *PodStatsOptions) error { + num := 0 + if len(args) > 0 { + num++ + } + if options.All { + num++ + } + if options.Latest { + num++ + } + switch num { + case 0: + // Podman v1 compat: if nothing's specified get all running + // pods. + options.All = true + return nil + case 1: + return nil + default: + return errors.New("--all, --latest and arguments cannot be used together") + } +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 9844d1d96..e09891760 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -217,12 +217,23 @@ func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []strin } func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { var ( + ctrs []*libpod.Container + err error reports []*entities.RestartReport ) - ctrs, err := getContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) - if err != nil { - return nil, err + + if options.Running { + ctrs, err = ic.Libpod.GetRunningContainers() + if err != nil { + return nil, err + } + } else { + ctrs, err = getContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } } + for _, con := range ctrs { timeout := con.StopTimeout() if options.Timeout != nil { diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 64d9c9f12..724bc5343 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -326,16 +326,19 @@ func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) if err != nil { return nil, err } - newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name) - if err != nil { - return nil, errors.Wrap(err, "image loaded but no additional tags were created") - } - if len(opts.Name) > 0 { - if err := newImage.TagImage(fmt.Sprintf("%s:%s", opts.Name, opts.Tag)); err != nil { - return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + names := strings.Split(name, ",") + if len(names) <= 1 { + newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name) + if err != nil { + return nil, errors.Wrap(err, "image loaded but no additional tags were created") + } + if len(opts.Name) > 0 { + if err := newImage.TagImage(fmt.Sprintf("%s:%s", opts.Name, opts.Tag)); err != nil { + return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + } } } - return &entities.ImageLoadReport{Name: name}, nil + return &entities.ImageLoadReport{Names: names}, nil } func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOptions) (*entities.ImageImportReport, error) { diff --git a/pkg/domain/infra/abi/pods_stats.go b/pkg/domain/infra/abi/pods_stats.go new file mode 100644 index 000000000..a41c01da0 --- /dev/null +++ b/pkg/domain/infra/abi/pods_stats.go @@ -0,0 +1,85 @@ +package abi + +import ( + "context" + "fmt" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/docker/go-units" + "github.com/pkg/errors" +) + +// PodStats implements printing stats about pods. +func (ic *ContainerEngine) PodStats(ctx context.Context, namesOrIds []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + // Cgroups v2 check for rootless. + if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return nil, err + } + if !unified { + return nil, errors.New("pod stats is not supported in rootless mode without cgroups v2") + } + } + // Get the (running) pods and convert them to the entities format. + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, errors.Wrap(err, "unable to get list of pods") + } + return ic.podsToStatsReport(pods) +} + +// podsToStatsReport converts a slice of pods into a corresponding slice of stats reports. +func (ic *ContainerEngine) podsToStatsReport(pods []*libpod.Pod) ([]*entities.PodStatsReport, error) { + reports := []*entities.PodStatsReport{} + for i := range pods { // Access by index to prevent potential loop-variable leaks. + podStats, err := pods[i].GetPodStats(nil) + if err != nil { + return nil, err + } + podID := pods[i].ID()[:12] + for j := range podStats { + r := entities.PodStatsReport{ + CPU: floatToPercentString(podStats[j].CPU), + MemUsage: combineHumanValues(podStats[j].MemUsage, podStats[j].MemLimit), + Mem: floatToPercentString(podStats[j].MemPerc), + NetIO: combineHumanValues(podStats[j].NetInput, podStats[j].NetOutput), + BlockIO: combineHumanValues(podStats[j].BlockInput, podStats[j].BlockOutput), + PIDS: pidsToString(podStats[j].PIDs), + CID: podStats[j].ContainerID[:12], + Name: podStats[j].Name, + Pod: podID, + } + reports = append(reports, &r) + } + } + + return reports, nil +} + +func combineHumanValues(a, b uint64) string { + if a == 0 && b == 0 { + return "-- / --" + } + return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) +} + +func floatToPercentString(f float64) string { + strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f) + if err != nil || strippedFloat == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%.2f", strippedFloat) + "%" +} + +func pidsToString(pid uint64) string { + if pid == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%d", pid) +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 18d6613f4..32f9c4e36 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -115,11 +115,15 @@ func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []st t := int(*options.Timeout) timeout = &t } + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) if err != nil { return nil, err } for _, c := range ctrs { + if options.Running && c.State != define.ContainerStateRunning.String() { + continue + } reports = append(reports, &entities.RestartReport{ Id: c.ID, Err: containers.Restart(ic.ClientCxt, c.ID, timeout), diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go index e7641c077..c193c6752 100644 --- a/pkg/domain/infra/tunnel/pods.go +++ b/pkg/domain/infra/tunnel/pods.go @@ -211,3 +211,7 @@ func (ic *ContainerEngine) PodInspect(ctx context.Context, options entities.PodI } return pods.Inspect(ic.ClientCxt, options.NameOrID) } + +func (ic *ContainerEngine) PodStats(ctx context.Context, namesOrIds []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + return pods.Stats(ic.ClientCxt, namesOrIds, options) +} diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index cee49ff51..f0161a793 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -159,6 +159,8 @@ func (n *Namespace) validate() error { func ParseNamespace(ns string) (Namespace, error) { toReturn := Namespace{} switch { + case ns == "pod": + toReturn.NSMode = FromPod case ns == "host": toReturn.NSMode = Host case ns == "private": @@ -214,6 +216,8 @@ func ParseNetworkNamespace(ns string) (Namespace, []string, error) { toReturn := Namespace{} var cniNetworks []string switch { + case ns == "pod": + toReturn.NSMode = FromPod case ns == "bridge": toReturn.NSMode = Bridge case ns == "none": diff --git a/test/e2e/load_test.go b/test/e2e/load_test.go index 6b6d3820a..9a2cee9e1 100644 --- a/test/e2e/load_test.go +++ b/test/e2e/load_test.go @@ -20,7 +20,6 @@ var _ = Describe("Podman load", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pause_test.go b/test/e2e/pause_test.go index 39e08e2e8..149a2e28a 100644 --- a/test/e2e/pause_test.go +++ b/test/e2e/pause_test.go @@ -2,7 +2,10 @@ package integration import ( "fmt" + "io/ioutil" "os" + "path/filepath" + "strings" "github.com/containers/libpod/pkg/cgroups" . "github.com/containers/libpod/test/utils" @@ -17,8 +20,8 @@ var _ = Describe("Podman pause", func() { podmanTest *PodmanTestIntegration ) - pausedState := "Paused" - createdState := "Created" + pausedState := "paused" + createdState := "created" BeforeEach(func() { SkipIfRootless() @@ -31,7 +34,13 @@ var _ = Describe("Podman pause", func() { Expect(err).To(BeNil()) if cgroupsv2 { - _, err := os.Stat("/sys/fs/cgroup/cgroup.freeze") + b, err := ioutil.ReadFile("/proc/self/cgroup") + if err != nil { + Skip("cannot read self cgroup") + } + + path := filepath.Join("/sys/fs/cgroup", strings.TrimSuffix(strings.Replace(string(b), "0::", "", 1), "\n"), "cgroup.freeze") + _, err = os.Stat(path) if err != nil { Skip("freezer controller not available on the current kernel") } @@ -72,7 +81,7 @@ var _ = Describe("Podman pause", func() { Expect(result).To(ExitWithError()) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(createdState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(createdState)) }) It("podman pause a running container by id", func() { @@ -85,7 +94,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"unpause", cid}) result.WaitWithDefaultTimeout() @@ -102,7 +111,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"container", "unpause", cid}) result.WaitWithDefaultTimeout() @@ -133,14 +142,14 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"rm", cid}) result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(2)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) }) @@ -155,7 +164,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"rm", "--force", cid}) result.WaitWithDefaultTimeout() @@ -175,14 +184,14 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"stop", cid}) result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(125)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(ContainSubstring(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(ContainSubstring(pausedState)) result = podmanTest.Podman([]string{"unpause", cid}) result.WaitWithDefaultTimeout() @@ -211,7 +220,7 @@ var _ = Describe("Podman pause", func() { Expect(result.ExitCode()).To(Equal(0)) Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0)) - Expect(podmanTest.GetContainerStatus()).To(Equal(pausedState)) + Expect(strings.ToLower(podmanTest.GetContainerStatus())).To(Equal(pausedState)) result = podmanTest.Podman([]string{"unpause", "test1"}) result.WaitWithDefaultTimeout() diff --git a/test/e2e/pod_infra_container_test.go b/test/e2e/pod_infra_container_test.go index 88644188b..3cc6fa9e8 100644 --- a/test/e2e/pod_infra_container_test.go +++ b/test/e2e/pod_infra_container_test.go @@ -20,7 +20,6 @@ var _ = Describe("Podman pod create", func() { BeforeEach(func() { tempdir, err = CreateTempDirInTempDir() - Skip(v2fail) if err != nil { os.Exit(1) } @@ -95,12 +94,17 @@ var _ = Describe("Podman pod create", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - check := podmanTest.Podman([]string{"ps", "-a", "--no-trunc", "--ns", "--format", "{{.IPC}} {{.NET}}"}) + check := podmanTest.Podman([]string{"ps", "-a", "--no-trunc", "--ns", "--format", "{{.Namespaces.IPC}} {{.Namespaces.NET}}"}) check.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) Expect(len(check.OutputToStringArray())).To(Equal(2)) Expect(check.OutputToStringArray()[0]).To(Equal(check.OutputToStringArray()[1])) + check = podmanTest.Podman([]string{"ps", "-a", "--no-trunc", "--ns", "--format", "{{.IPC}} {{.NET}}"}) + check.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(check.OutputToStringArray())).To(Equal(2)) + Expect(check.OutputToStringArray()[0]).To(Equal(check.OutputToStringArray()[1])) }) It("podman pod correctly sets up NetNS", func() { @@ -236,12 +240,18 @@ var _ = Describe("Podman pod create", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - check := podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.PIDNS}}"}) + check := podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.Namespaces.PIDNS}}"}) check.WaitWithDefaultTimeout() Expect(check.ExitCode()).To(Equal(0)) outputArray := check.OutputToStringArray() Expect(len(outputArray)).To(Equal(2)) + check = podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.PIDNS}}"}) + check.WaitWithDefaultTimeout() + Expect(check.ExitCode()).To(Equal(0)) + outputArray = check.OutputToStringArray() + Expect(len(outputArray)).To(Equal(2)) + PID1 := outputArray[0] PID2 := outputArray[1] Expect(PID1).To(Not(Equal(PID2))) diff --git a/test/e2e/pod_pause_test.go b/test/e2e/pod_pause_test.go index bb1719203..7067c9a87 100644 --- a/test/e2e/pod_pause_test.go +++ b/test/e2e/pod_pause_test.go @@ -15,10 +15,9 @@ var _ = Describe("Podman pod pause", func() { podmanTest *PodmanTestIntegration ) - pausedState := "Paused" + pausedState := "paused" BeforeEach(func() { - Skip(v2fail) SkipIfRootless() tempdir, err = CreateTempDirInTempDir() if err != nil { diff --git a/test/e2e/pod_pod_namespaces.go b/test/e2e/pod_pod_namespaces.go index 7acdfd356..09f716806 100644 --- a/test/e2e/pod_pod_namespaces.go +++ b/test/e2e/pod_pod_namespaces.go @@ -19,7 +19,6 @@ var _ = Describe("Podman pod create", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) @@ -50,7 +49,7 @@ var _ = Describe("Podman pod create", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - check := podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.IPC}} {{.UTS}} {{.NET}}"}) + check := podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.Namespaces.IPC}} {{.Namespaces.UTS}} {{.Namespaces.NET}}"}) check.WaitWithDefaultTimeout() Expect(check.ExitCode()).To(Equal(0)) outputArray := check.OutputToStringArray() @@ -77,7 +76,7 @@ var _ = Describe("Podman pod create", func() { session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - check := podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.PIDNS}}"}) + check := podmanTest.Podman([]string{"ps", "-a", "--ns", "--format", "{{.Namespaces.PIDNS}}"}) check.WaitWithDefaultTimeout() Expect(check.ExitCode()).To(Equal(0)) outputArray := check.OutputToStringArray() diff --git a/test/e2e/pod_start_test.go b/test/e2e/pod_start_test.go index 52ba03dae..2722cb5b3 100644 --- a/test/e2e/pod_start_test.go +++ b/test/e2e/pod_start_test.go @@ -16,7 +16,6 @@ var _ = Describe("Podman pod start", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) diff --git a/test/e2e/pod_stats_test.go b/test/e2e/pod_stats_test.go index bb3610a27..347f33e62 100644 --- a/test/e2e/pod_stats_test.go +++ b/test/e2e/pod_stats_test.go @@ -18,7 +18,6 @@ var _ = Describe("Podman pod stats", func() { ) BeforeEach(func() { - Skip(v2fail) cgroupsv2, err := cgroups.IsCgroup2UnifiedMode() Expect(err).To(BeNil()) diff --git a/test/e2e/restart_test.go b/test/e2e/restart_test.go index 9bbeb4f68..2b515f53b 100644 --- a/test/e2e/restart_test.go +++ b/test/e2e/restart_test.go @@ -17,7 +17,6 @@ var _ = Describe("Podman restart", func() { ) BeforeEach(func() { - Skip(v2fail) tempdir, err = CreateTempDirInTempDir() if err != nil { os.Exit(1) |