diff options
36 files changed, 671 insertions, 227 deletions
diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index e9146bda7..4d12d2534 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -206,7 +206,7 @@ func ps(cmd *cobra.Command, args []string) error { return err } if err := tmpl.Execute(w, responses); err != nil { - return nil + return err } if err := w.Flush(); err != nil { return err diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index b13983e37..f72446cb6 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -170,9 +170,9 @@ func run(cmd *cobra.Command, args []string) error { return nil } if runRmi { - _, err := registry.ImageEngine().Remove(registry.GetContext(), []string{args[0]}, entities.ImageRemoveOptions{}) - if err != nil { - logrus.Errorf("%s", errors.Wrapf(err, "failed removing image")) + _, rmErrors := registry.ImageEngine().Remove(registry.GetContext(), []string{args[0]}, entities.ImageRemoveOptions{}) + if len(rmErrors) > 0 { + logrus.Errorf("%s", errors.Wrapf(errorhandling.JoinErrors(rmErrors), "failed removing image")) } } return nil diff --git a/cmd/podman/containers/stats.go b/cmd/podman/containers/stats.go new file mode 100644 index 000000000..3f9db671f --- /dev/null +++ b/cmd/podman/containers/stats.go @@ -0,0 +1,244 @@ +package containers + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + "text/template" + + tm "github.com/buger/goterm" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/utils" + "github.com/docker/go-units" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + statsDescription = "Display percentage of CPU, memory, network I/O, block I/O and PIDs for one or more containers." + statsCommand = &cobra.Command{ + Use: "stats [flags] [CONTAINER...]", + Short: "Display a live stream of container resource usage statistics", + Long: statsDescription, + RunE: stats, + Args: checkStatOptions, + Example: `podman stats --all --no-stream + podman stats ctrID + podman stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`, + } + + containerStatsCommand = &cobra.Command{ + Use: statsCommand.Use, + Short: statsCommand.Short, + Long: statsCommand.Long, + RunE: statsCommand.RunE, + Args: checkStatOptions, + Example: `podman container stats --all --no-stream + podman container stats ctrID + podman container stats --no-stream --format "table {{.ID}} {{.Name}} {{.MemUsage}}" ctrID`, + } +) + +var ( + statsOptions entities.ContainerStatsOptions + defaultStatsRow = "{{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDS}}\n" + defaultStatsHeader = "ID\tNAME\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET IO\tBLOCK IO\tPIDS\n" +) + +func statFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&statsOptions.All, "all", "a", false, "Show all containers. Only running containers are shown by default. The default is false") + flags.StringVar(&statsOptions.Format, "format", "", "Pretty-print container statistics to JSON or using a Go template") + flags.BoolVarP(&statsOptions.Latest, "latest", "l", false, "Act on the latest container Podman is aware of") + flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals") + flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: statsCommand, + }) + flags := statsCommand.Flags() + statFlags(flags) + + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: containerStatsCommand, + Parent: containerCmd, + }) + + containerStatsFlags := containerStatsCommand.Flags() + statFlags(containerStatsFlags) +} + +// stats is different in that it will assume running containers if +// no input is given, so we need to validate differently +func checkStatOptions(cmd *cobra.Command, args []string) error { + opts := 0 + if statsOptions.All { + opts += 1 + } + if statsOptions.Latest { + opts += 1 + } + if len(args) > 0 { + opts += 1 + } + if opts > 1 { + return errors.Errorf("--all, --latest and containers cannot be used together") + } + return nil +} + +func stats(cmd *cobra.Command, args []string) error { + if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return err + } + if !unified { + return errors.New("stats is not supported in rootless mode without cgroups v2") + } + } + statsOptions.StatChan = make(chan []*define.ContainerStats, 1) + go func() { + for reports := range statsOptions.StatChan { + if err := outputStats(reports); err != nil { + logrus.Error(err) + } + } + }() + return registry.ContainerEngine().ContainerStats(registry.Context(), args, statsOptions) +} + +func outputStats(reports []*define.ContainerStats) error { + if len(statsOptions.Format) < 1 && !statsOptions.NoReset { + tm.Clear() + tm.MoveCursor(1, 1) + tm.Flush() + } + var stats []*containerStats + for _, r := range reports { + stats = append(stats, &containerStats{r}) + } + if statsOptions.Format == "json" { + return outputJSON(stats) + } + format := defaultStatsRow + if len(statsOptions.Format) > 0 { + format = statsOptions.Format + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + } + format = "{{range . }}" + format + "{{end}}" + if len(statsOptions.Format) < 1 { + format = defaultStatsHeader + format + } + tmpl, err := template.New("stats").Parse(format) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 8, 2, 2, ' ', 0) + if err := tmpl.Execute(w, stats); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err + } + return nil +} + +type containerStats struct { + *define.ContainerStats +} + +func (s *containerStats) ID() string { + return s.ContainerID[0:12] +} + +func (s *containerStats) CPUPerc() string { + return floatToPercentString(s.CPU) +} + +func (s *containerStats) MemPerc() string { + return floatToPercentString(s.ContainerStats.MemPerc) +} + +func (s *containerStats) NetIO() string { + return combineHumanValues(s.NetInput, s.NetOutput) +} + +func (s *containerStats) BlockIO() string { + return combineHumanValues(s.BlockInput, s.BlockOutput) +} + +func (s *containerStats) PIDS() string { + if s.PIDs == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%d", s.PIDs) +} +func (s *containerStats) MemUsage() string { + return combineHumanValues(s.ContainerStats.MemUsage, s.ContainerStats.MemLimit) +} + +func floatToPercentString(f float64) string { + strippedFloat, err := utils.RemoveScientificNotationFromFloat(f) + if err != nil || strippedFloat == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%.2f", strippedFloat) + "%" +} + +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 outputJSON(stats []*containerStats) error { + type jstat struct { + Id string `json:"id"` + Name string `json:"name"` + CpuPercent string `json:"cpu_percent"` + MemUsage string `json:"mem_usage"` + MemPerc string `json:"mem_percent"` + NetIO string `json:"net_io"` + BlockIO string `json:"block_io"` + Pids string `json:"pids"` + } + var jstats []jstat + for _, j := range stats { + jstats = append(jstats, jstat{ + Id: j.ID(), + Name: j.Name, + CpuPercent: j.CPUPerc(), + MemUsage: j.MemPerc(), + MemPerc: j.MemUsage(), + NetIO: j.NetIO(), + BlockIO: j.BlockIO(), + Pids: j.PIDS(), + }) + } + + b, err := json.MarshalIndent(jstats, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} diff --git a/cmd/podman/containers/unpause.go b/cmd/podman/containers/unpause.go index adf8d12ee..7ea8e13c1 100644 --- a/cmd/podman/containers/unpause.go +++ b/cmd/podman/containers/unpause.go @@ -49,7 +49,7 @@ func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, - Command: unpauseCommand, + Command: containerUnpauseCommand, Parent: containerCmd, }) diff --git a/cmd/podman/images/rm.go b/cmd/podman/images/rm.go index 1cf5fa365..4b9920532 100644 --- a/cmd/podman/images/rm.go +++ b/cmd/podman/images/rm.go @@ -5,6 +5,7 @@ import ( "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/errorhandling" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -48,7 +49,9 @@ func rm(cmd *cobra.Command, args []string) error { return errors.Errorf("when using the --all switch, you may not pass any images names or IDs") } - report, err := registry.ImageEngine().Remove(registry.GetContext(), args, imageOpts) + // Note: certain image-removal errors are non-fatal. Hence, the report + // might be set even if err != nil. + report, rmErrors := registry.ImageEngine().Remove(registry.GetContext(), args, imageOpts) if report != nil { for _, u := range report.Untagged { fmt.Println("Untagged: " + u) @@ -62,5 +65,5 @@ func rm(cmd *cobra.Command, args []string) error { registry.SetExitCode(report.ExitCode) } - return err + return errorhandling.JoinErrors(rmErrors) } diff --git a/libpod/define/containerstate.go b/libpod/define/containerstate.go index 6da49a594..825e77387 100644 --- a/libpod/define/containerstate.go +++ b/libpod/define/containerstate.go @@ -112,3 +112,22 @@ func (s ContainerExecStatus) String() string { return "bad state" } } + +// ContainerStats contains the statistics information for a running container +type ContainerStats struct { + ContainerID string + Name string + PerCPU []uint64 + CPU float64 + CPUNano uint64 + CPUSystemNano uint64 + SystemNano uint64 + MemUsage uint64 + MemLimit uint64 + MemPerc float64 + NetInput uint64 + NetOutput uint64 + BlockInput uint64 + BlockOutput uint64 + PIDs uint64 +} diff --git a/libpod/pod.go b/libpod/pod.go index b5a14c165..8eb06ae2f 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -247,14 +247,14 @@ func (p *Pod) InfraContainerID() (string, error) { // PodContainerStats is an organization struct for pods and their containers type PodContainerStats struct { Pod *Pod - ContainerStats map[string]*ContainerStats + ContainerStats map[string]*define.ContainerStats } // GetPodStats returns the stats for each of its containers -func (p *Pod) GetPodStats(previousContainerStats map[string]*ContainerStats) (map[string]*ContainerStats, error) { +func (p *Pod) GetPodStats(previousContainerStats map[string]*define.ContainerStats) (map[string]*define.ContainerStats, error) { var ( ok bool - prevStat *ContainerStats + prevStat *define.ContainerStats ) p.lock.Lock() defer p.lock.Unlock() @@ -266,10 +266,10 @@ func (p *Pod) GetPodStats(previousContainerStats map[string]*ContainerStats) (ma if err != nil { return nil, err } - newContainerStats := make(map[string]*ContainerStats) + newContainerStats := make(map[string]*define.ContainerStats) for _, c := range containers { if prevStat, ok = previousContainerStats[c.ID()]; !ok { - prevStat = &ContainerStats{} + prevStat = &define.ContainerStats{} } newStats, err := c.GetContainerStats(prevStat) // If the container wasn't running, don't include it diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index 919080c42..cd7f54799 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -56,7 +56,7 @@ func (r *Runtime) RemoveImage(ctx context.Context, img *image.Image, force bool) } } } else { - return nil, fmt.Errorf("could not remove image %s as it is being used by %d containers", img.ID(), len(imageCtrs)) + return nil, errors.Wrapf(define.ErrImageInUse, "could not remove image %s as it is being used by %d containers", img.ID(), len(imageCtrs)) } } diff --git a/libpod/stats.go b/libpod/stats.go index 6f42afd18..9f4986144 100644 --- a/libpod/stats.go +++ b/libpod/stats.go @@ -13,8 +13,8 @@ import ( ) // GetContainerStats gets the running stats for a given container -func (c *Container) GetContainerStats(previousStats *ContainerStats) (*ContainerStats, error) { - stats := new(ContainerStats) +func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) { + stats := new(define.ContainerStats) stats.ContainerID = c.ID() stats.Name = c.Name() diff --git a/libpod/stats_config.go b/libpod/stats_config.go deleted file mode 100644 index 91d3d1493..000000000 --- a/libpod/stats_config.go +++ /dev/null @@ -1,20 +0,0 @@ -package libpod - -// ContainerStats contains the statistics information for a running container -type ContainerStats struct { - ContainerID string - Name string - PerCPU []uint64 - CPU float64 - CPUNano uint64 - CPUSystemNano uint64 - SystemNano uint64 - MemUsage uint64 - MemLimit uint64 - MemPerc float64 - NetInput uint64 - NetOutput uint64 - BlockInput uint64 - BlockOutput uint64 - PIDs uint64 -} diff --git a/libpod/stats_unsupported.go b/libpod/stats_unsupported.go index ec19a89a1..6d21ae8f2 100644 --- a/libpod/stats_unsupported.go +++ b/libpod/stats_unsupported.go @@ -5,6 +5,6 @@ package libpod import "github.com/containers/libpod/libpod/define" // GetContainerStats gets the running stats for a given container -func (c *Container) GetContainerStats(previousStats *ContainerStats) (*ContainerStats, error) { +func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) { return nil, define.ErrOSNotSupported } diff --git a/libpod/util.go b/libpod/util.go index 6457dac1c..bdfd153ed 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -9,12 +9,10 @@ import ( "os/exec" "path/filepath" "sort" - "strconv" "strings" "time" "github.com/containers/common/pkg/config" - "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/utils" "github.com/fsnotify/fsnotify" @@ -36,24 +34,6 @@ func FuncTimer(funcName string) { fmt.Printf("%s executed in %d ms\n", funcName, elapsed) } -// RemoveScientificNotationFromFloat returns a float without any -// scientific notation if the number has any. -// golang does not handle conversion of float64s that have scientific -// notation in them and otherwise stinks. please replace this if you have -// a better implementation. -func RemoveScientificNotationFromFloat(x float64) (float64, error) { - bigNum := strconv.FormatFloat(x, 'g', -1, 64) - breakPoint := strings.IndexAny(bigNum, "Ee") - if breakPoint > 0 { - bigNum = bigNum[:breakPoint] - } - result, err := strconv.ParseFloat(bigNum, 64) - if err != nil { - return x, errors.Wrapf(err, "unable to remove scientific number from calculations") - } - return result, nil -} - // MountExists returns true if dest exists in the list of mounts func MountExists(specMounts []spec.Mount, dest string) bool { for _, m := range specMounts { diff --git a/libpod/util_test.go b/libpod/util_test.go index 227686c2b..4e18a7e4e 100644 --- a/libpod/util_test.go +++ b/libpod/util_test.go @@ -3,6 +3,7 @@ package libpod import ( "testing" + "github.com/containers/libpod/utils" "github.com/stretchr/testify/assert" ) @@ -10,7 +11,7 @@ func TestRemoveScientificNotationFromFloat(t *testing.T) { numbers := []float64{0.0, .5, 1.99999932, 1.04e+10} results := []float64{0.0, .5, 1.99999932, 1.04} for i, x := range numbers { - result, err := RemoveScientificNotationFromFloat(x) + result, err := utils.RemoveScientificNotationFromFloat(x) assert.NoError(t, err) assert.Equal(t, result, results[i]) } diff --git a/pkg/api/handlers/compat/containers_stats.go b/pkg/api/handlers/compat/containers_stats.go index 53ad0a632..62ccd2b93 100644 --- a/pkg/api/handlers/compat/containers_stats.go +++ b/pkg/api/handlers/compat/containers_stats.go @@ -50,7 +50,7 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) { return } - stats, err := ctnr.GetContainerStats(&libpod.ContainerStats{}) + stats, err := ctnr.GetContainerStats(&define.ContainerStats{}) if err != nil { utils.InternalServerError(w, errors.Wrapf(err, "Failed to obtain Container %s stats", name)) return diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index f7be5ce9a..93b4564a1 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -23,6 +23,7 @@ import ( "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/errorhandling" "github.com/containers/libpod/pkg/util" utils2 "github.com/containers/libpod/utils" "github.com/gorilla/schema" @@ -700,8 +701,8 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, reports) } -// ImagesRemove is the endpoint for image removal. -func ImagesRemove(w http.ResponseWriter, r *http.Request) { +// ImagesBatchRemove is the endpoint for batch image removal. +func ImagesBatchRemove(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { @@ -722,7 +723,49 @@ func ImagesRemove(w http.ResponseWriter, r *http.Request) { opts := entities.ImageRemoveOptions{All: query.All, Force: query.Force} imageEngine := abi.ImageEngine{Libpod: runtime} - rmReport, rmError := imageEngine.Remove(r.Context(), query.Images, opts) - report := handlers.LibpodImagesRemoveReport{ImageRemoveReport: *rmReport, Error: rmError.Error()} + rmReport, rmErrors := imageEngine.Remove(r.Context(), query.Images, opts) + + strErrs := errorhandling.ErrorsToStrings(rmErrors) + report := handlers.LibpodImagesRemoveReport{ImageRemoveReport: *rmReport, Errors: strErrs} utils.WriteResponse(w, http.StatusOK, report) } + +// ImagesRemove is the endpoint for removing one image. +func ImagesRemove(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Force bool `schema:"force"` + }{ + Force: false, + } + + 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 + } + + opts := entities.ImageRemoveOptions{Force: query.Force} + imageEngine := abi.ImageEngine{Libpod: runtime} + rmReport, rmErrors := imageEngine.Remove(r.Context(), []string{utils.GetName(r)}, opts) + + // In contrast to batch-removal, where we're only setting the exit + // code, we need to have another closer look at the errors here and set + // the appropriate http status code. + + switch rmReport.ExitCode { + case 0: + report := handlers.LibpodImagesRemoveReport{ImageRemoveReport: *rmReport, Errors: []string{}} + utils.WriteResponse(w, http.StatusOK, report) + case 1: + // 404 - no such image + utils.Error(w, "error removing image", http.StatusNotFound, errorhandling.JoinErrors(rmErrors)) + case 2: + // 409 - conflict error (in use by containers) + utils.Error(w, "error removing image", http.StatusConflict, errorhandling.JoinErrors(rmErrors)) + default: + // 500 - internal error + utils.Error(w, "failed to remove image", http.StatusInternalServerError, errorhandling.JoinErrors(rmErrors)) + } +} diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 58a12ea6a..a7abf59c0 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -41,7 +41,7 @@ type LibpodImagesPullReport struct { type LibpodImagesRemoveReport struct { entities.ImageRemoveReport // Image removal requires is to return data and an error. - Error string + Errors []string } type ContainersPruneReport struct { diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index f59dca6f5..0e8d68b7e 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -822,7 +822,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/import"), s.APIHandler(libpod.ImagesImport)).Methods(http.MethodPost) - // swagger:operation GET /libpod/images/remove libpod libpodImagesRemove + // swagger:operation DELETE /libpod/images/remove libpod libpodImagesRemove // --- // tags: // - images @@ -853,7 +853,37 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // $ref: "#/responses/BadParamError" // 500: // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/remove"), s.APIHandler(libpod.ImagesRemove)).Methods(http.MethodGet) + r.Handle(VersionedPath("/libpod/images/remove"), s.APIHandler(libpod.ImagesBatchRemove)).Methods(http.MethodDelete) + // swagger:operation DELETE /libpod/images/{name:.*}/remove libpod libpodRemoveImage + // --- + // tags: + // - images + // summary: Remove an image from the local storage. + // description: Remove an image from the local storage. + // parameters: + // - in: path + // name: name:.* + // type: string + // required: true + // description: name or ID of image to remove + // - in: query + // name: force + // type: boolean + // description: remove the image even if used by containers or has other tags + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/DocsImageDeleteResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 404: + // $ref: '#/responses/NoSuchImage' + // 409: + // $ref: '#/responses/ConflictError' + // 500: + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/{name:.*}/remove"), s.APIHandler(libpod.ImagesRemove)).Methods(http.MethodDelete) // swagger:operation POST /libpod/images/pull libpod libpodImagesPull // --- // tags: @@ -952,36 +982,6 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(libpod.SearchImages)).Methods(http.MethodGet) - // swagger:operation DELETE /libpod/images/{name:.*} libpod libpodRemoveImage - // --- - // tags: - // - images - // summary: Remove Image - // description: Delete an image from local store - // parameters: - // - in: path - // name: name:.* - // type: string - // required: true - // description: name or ID of image to delete - // - in: query - // name: force - // type: boolean - // description: remove the image even if used by containers or has other tags - // produces: - // - application/json - // responses: - // 200: - // $ref: "#/responses/DocsImageDeleteResponse" - // 400: - // $ref: "#/responses/BadParamError" - // 404: - // $ref: '#/responses/NoSuchImage' - // 409: - // $ref: '#/responses/ConflictError' - // 500: - // $ref: '#/responses/InternalError' - r.Handle(VersionedPath("/libpod/images/{name:.*}"), s.APIHandler(compat.RemoveImage)).Methods(http.MethodDelete) // swagger:operation GET /libpod/images/{name:.*}/get libpod libpodExportImage // --- // tags: diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 4d8ae6a6e..034ade618 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -109,36 +109,6 @@ func Load(ctx context.Context, r io.Reader, name *string) (*entities.ImageLoadRe return &report, response.Process(&report) } -// Remove deletes an image from local storage. The optional force parameter -// will forcibly remove the image by removing all all containers, including -// those that are Running, first. -func Remove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, error) { - var report handlers.LibpodImagesRemoveReport - conn, err := bindings.GetClient(ctx) - if err != nil { - return nil, err - } - params := url.Values{} - params.Set("all", strconv.FormatBool(opts.All)) - params.Set("force", strconv.FormatBool(opts.Force)) - for _, i := range images { - params.Add("images", i) - } - - response, err := conn.DoRequest(nil, http.MethodGet, "/images/remove", params) - if err != nil { - return nil, err - } - if err := response.Process(&report); err != nil { - return nil, err - } - var rmError error - if report.Error != "" { - rmError = errors.New(report.Error) - } - return &report.ImageRemoveReport, rmError -} - // Export saves an image from local storage as a tarball or image archive. The optional format // parameter is used to change the format of the output. func Export(ctx context.Context, nameOrID string, w io.Writer, format *string, compress *bool) error { diff --git a/pkg/bindings/images/rm.go b/pkg/bindings/images/rm.go new file mode 100644 index 000000000..e3b5590df --- /dev/null +++ b/pkg/bindings/images/rm.go @@ -0,0 +1,65 @@ +package images + +import ( + "context" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/errorhandling" +) + +// BachtRemove removes a batch of images from the local storage. +func BatchRemove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, []error) { + // FIXME - bindings tests are missing for this endpoint. Once the CI is + // re-enabled for bindings, we need to add them. At the time of writing, + // the tests don't compile. + var report handlers.LibpodImagesRemoveReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, []error{err} + } + + params := url.Values{} + params.Set("all", strconv.FormatBool(opts.All)) + params.Set("force", strconv.FormatBool(opts.Force)) + for _, i := range images { + params.Add("images", i) + } + + response, err := conn.DoRequest(nil, http.MethodDelete, "/images/remove", params) + if err != nil { + return nil, []error{err} + } + if err := response.Process(&report); err != nil { + return nil, []error{err} + } + + return &report.ImageRemoveReport, errorhandling.StringsToErrors(report.Errors) +} + +// Remove removes an image from the local storage. Use force to remove an +// image, even if it's used by containers. +func Remove(ctx context.Context, nameOrID string, force bool) (*entities.ImageRemoveReport, error) { + var report handlers.LibpodImagesRemoveReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + + params := url.Values{} + params.Set("force", strconv.FormatBool(force)) + response, err := conn.DoRequest(nil, http.MethodDelete, "/images/%s/remove", params, nameOrID) + if err != nil { + return nil, err + } + if err := response.Process(&report); err != nil { + return nil, err + } + + errs := errorhandling.StringsToErrors(report.Errors) + return &report.ImageRemoveReport, errorhandling.JoinErrors(errs) +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 58210efd0..9c8e82149 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -84,17 +84,20 @@ var _ = Describe("Podman images", func() { // Test to validate the remove image api It("remove image", func() { // Remove invalid image should be a 404 - _, err = images.Remove(bt.conn, "foobar5000", &bindings.PFalse) + response, err := images.Remove(bt.conn, "foobar5000", false) Expect(err).ToNot(BeNil()) + Expect(response).To(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Remove an image by name, validate image is removed and error is nil inspectData, err := images.GetImage(bt.conn, busybox.shortName, nil) Expect(err).To(BeNil()) - response, err := images.Remove(bt.conn, busybox.shortName, nil) + response, err = images.Remove(bt.conn, busybox.shortName, false) Expect(err).To(BeNil()) - Expect(inspectData.ID).To(Equal(response[0]["Deleted"])) + code, _ = bindings.CheckResponseCode(err) + + Expect(inspectData.ID).To(Equal(response.Deleted[0])) inspectData, err = images.GetImage(bt.conn, busybox.shortName, nil) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) @@ -104,30 +107,31 @@ var _ = Describe("Podman images", func() { _, err = bt.RunTopContainer(&top, &bindings.PFalse, nil) Expect(err).To(BeNil()) // we should now have a container called "top" running - containerResponse, err := containers.Inspect(bt.conn, "top", &bindings.PFalse) + containerResponse, err := containers.Inspect(bt.conn, "top", nil) Expect(err).To(BeNil()) Expect(containerResponse.Name).To(Equal("top")) // try to remove the image "alpine". This should fail since we are not force // deleting hence image cannot be deleted until the container is deleted. - response, err = images.Remove(bt.conn, alpine.shortName, &bindings.PFalse) + response, err = images.Remove(bt.conn, alpine.shortName, false) code, _ = bindings.CheckResponseCode(err) - Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + Expect(code).To(BeNumerically("==", http.StatusConflict)) // Removing the image "alpine" where force = true - response, err = images.Remove(bt.conn, alpine.shortName, &bindings.PTrue) + response, err = images.Remove(bt.conn, alpine.shortName, true) Expect(err).To(BeNil()) - - // Checking if both the images are gone as well as the container is deleted - inspectData, err = images.GetImage(bt.conn, busybox.shortName, nil) + // To be extra sure, check if the previously created container + // is gone as well. + _, err = containers.Inspect(bt.conn, "top", &bindings.PFalse) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) - inspectData, err = images.GetImage(bt.conn, alpine.shortName, nil) + // Now make sure both images are gone. + inspectData, err = images.GetImage(bt.conn, busybox.shortName, nil) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) - _, err = containers.Inspect(bt.conn, "top", &bindings.PFalse) + inspectData, err = images.GetImage(bt.conn, alpine.shortName, nil) code, _ = bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) }) @@ -209,7 +213,7 @@ var _ = Describe("Podman images", func() { It("Load|Import Image", func() { // load an image - _, err := images.Remove(bt.conn, alpine.name, nil) + _, err := images.Remove(bt.conn, alpine.name, false) Expect(err).To(BeNil()) exists, err := images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) @@ -219,7 +223,7 @@ var _ = Describe("Podman images", func() { Expect(err).To(BeNil()) names, err := images.Load(bt.conn, f, nil) Expect(err).To(BeNil()) - Expect(names.Name).To(Equal(alpine.name)) + Expect(names.Names[0]).To(Equal(alpine.name)) exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) @@ -227,7 +231,7 @@ var _ = Describe("Podman images", func() { // load with a repo name f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) Expect(err).To(BeNil()) - _, err = images.Remove(bt.conn, alpine.name, nil) + _, err = images.Remove(bt.conn, alpine.name, false) Expect(err).To(BeNil()) exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) @@ -235,7 +239,7 @@ var _ = Describe("Podman images", func() { newName := "quay.io/newname:fizzle" names, err = images.Load(bt.conn, f, &newName) Expect(err).To(BeNil()) - Expect(names.Name).To(Equal(alpine.name)) + Expect(names.Names[0]).To(Equal(alpine.name)) exists, err = images.Exists(bt.conn, newName) Expect(err).To(BeNil()) Expect(exists).To(BeTrue()) @@ -243,7 +247,7 @@ var _ = Describe("Podman images", func() { // load with a bad repo name should trigger a 500 f, err = os.Open(filepath.Join(ImageCacheDir, alpine.tarballName)) Expect(err).To(BeNil()) - _, err = images.Remove(bt.conn, alpine.name, nil) + _, err = images.Remove(bt.conn, alpine.name, false) Expect(err).To(BeNil()) exists, err = images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) @@ -271,7 +275,7 @@ var _ = Describe("Podman images", func() { It("Import Image", func() { // load an image - _, err = images.Remove(bt.conn, alpine.name, nil) + _, err = images.Remove(bt.conn, alpine.name, false) Expect(err).To(BeNil()) exists, err := images.Exists(bt.conn, alpine.name) Expect(err).To(BeNil()) diff --git a/pkg/bindings/test/manifests_test.go b/pkg/bindings/test/manifests_test.go index 23c3d8194..4987dfe5b 100644 --- a/pkg/bindings/test/manifests_test.go +++ b/pkg/bindings/test/manifests_test.go @@ -47,7 +47,7 @@ var _ = Describe("Podman containers ", func() { code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) - _, err = images.Remove(bt.conn, id, nil) + _, err = images.Remove(bt.conn, id, false) Expect(err).To(BeNil()) // create manifest list with images diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 622e8eb5b..071eff2fc 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -367,3 +367,14 @@ type ContainerCpOptions struct { // ContainerCpReport describes the output from a cp operation type ContainerCpReport struct { } + +// ContainerStatsOptions describes input options for getting +// stats on containers +type ContainerStatsOptions struct { + All bool + Format string + Latest bool + NoReset bool + NoStream bool + StatChan chan []*define.ContainerStats +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 98e4d841f..2e4e486b5 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -35,6 +35,7 @@ type ContainerEngine interface { ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error) ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error) ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error) + ContainerStats(ctx context.Context, namesOrIds []string, options ContainerStatsOptions) error ContainerStop(ctx context.Context, namesOrIds []string, options StopOptions) ([]*StopReport, error) ContainerTop(ctx context.Context, options TopOptions) (*StringSliceReport, error) ContainerUnmount(ctx context.Context, nameOrIds []string, options ContainerUnmountOptions) ([]*ContainerUnmountReport, error) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 46a96ca20..45686ec79 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -19,7 +19,7 @@ type ImageEngine interface { Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) Pull(ctx context.Context, rawImage string, opts ImagePullOptions) (*ImagePullReport, error) Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error - Remove(ctx context.Context, images []string, opts ImageRemoveOptions) (*ImageRemoveReport, error) + Remove(ctx context.Context, images []string, opts ImageRemoveOptions) (*ImageRemoveReport, []error) Save(ctx context.Context, nameOrId string, tags []string, options ImageSaveOptions) error Search(ctx context.Context, term string, opts ImageSearchOptions) ([]ImageSearchReport, error) Shutdown(ctx context.Context) diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 244fbc5cd..249e8147c 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -8,8 +8,7 @@ import ( "strconv" "strings" "sync" - - lpfilters "github.com/containers/libpod/libpod/filters" + "time" "github.com/containers/buildah" "github.com/containers/common/pkg/config" @@ -17,8 +16,10 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/events" + lpfilters "github.com/containers/libpod/libpod/filters" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/logs" + "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/checkpoint" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/infra/abi/terminal" @@ -1003,3 +1004,76 @@ func (ic *ContainerEngine) Shutdown(_ context.Context) { _ = ic.Libpod.Shutdown(false) }) } + +func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) error { + containerFunc := ic.Libpod.GetRunningContainers + switch { + case len(namesOrIds) > 0: + containerFunc = func() ([]*libpod.Container, error) { return ic.Libpod.GetContainersByList(namesOrIds) } + case options.Latest: + containerFunc = func() ([]*libpod.Container, error) { + lastCtr, err := ic.Libpod.GetLatestContainer() + if err != nil { + return nil, err + } + return []*libpod.Container{lastCtr}, nil + } + case options.All: + containerFunc = ic.Libpod.GetAllContainers + } + + ctrs, err := containerFunc() + if err != nil { + return errors.Wrapf(err, "unable to get list of containers") + } + containerStats := map[string]*define.ContainerStats{} + for _, ctr := range ctrs { + initialStats, err := ctr.GetContainerStats(&define.ContainerStats{}) + if err != nil { + // when doing "all", don't worry about containers that are not running + cause := errors.Cause(err) + if options.All && (cause == define.ErrCtrRemoved || cause == define.ErrNoSuchCtr || cause == define.ErrCtrStateInvalid) { + continue + } + if cause == cgroups.ErrCgroupV1Rootless { + err = cause + } + return err + } + containerStats[ctr.ID()] = initialStats + } + for { + reportStats := []*define.ContainerStats{} + for _, ctr := range ctrs { + id := ctr.ID() + if _, ok := containerStats[ctr.ID()]; !ok { + initialStats, err := ctr.GetContainerStats(&define.ContainerStats{}) + if errors.Cause(err) == define.ErrCtrRemoved || errors.Cause(err) == define.ErrNoSuchCtr || errors.Cause(err) == define.ErrCtrStateInvalid { + // skip dealing with a container that is gone + continue + } + if err != nil { + return err + } + containerStats[id] = initialStats + } + stats, err := ctr.GetContainerStats(containerStats[id]) + if err != nil && errors.Cause(err) != define.ErrNoSuchCtr { + return err + } + // replace the previous measurement with the current one + containerStats[id] = stats + reportStats = append(reportStats, stats) + } + ctrs, err = containerFunc() + if err != nil { + return err + } + options.StatChan <- reportStats + if options.NoStream { + break + } + time.Sleep(time.Second) + } + return nil +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 0af06fb89..7ab5131f0 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -21,7 +21,6 @@ import ( domainUtils "github.com/containers/libpod/pkg/domain/utils" "github.com/containers/libpod/pkg/util" "github.com/containers/storage" - "github.com/hashicorp/go-multierror" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -423,8 +422,10 @@ func (ir *ImageEngine) Tree(ctx context.Context, nameOrId string, opts entities. return &entities.ImageTreeReport{Tree: results}, nil } -// Remove removes one or more images from local storage. -func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (report *entities.ImageRemoveReport, finalError error) { +// removeErrorsToExitCode returns an exit code for the specified slice of +// image-removal errors. The error codes are set according to the documented +// behaviour in the Podman man pages. +func removeErrorsToExitCode(rmErrors []error) int { var ( // noSuchImageErrors indicates that at least one image was not found. noSuchImageErrors bool @@ -434,59 +435,53 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie // otherErrors indicates that at least one error other than the two // above occured. otherErrors bool - // deleteError is a multierror to conveniently collect errors during - // removal. We really want to delete as many images as possible and not - // error out immediately. - deleteError *multierror.Error ) - report = &entities.ImageRemoveReport{} + if len(rmErrors) == 0 { + return 0 + } - // Set the removalCode and the error after all work is done. - defer func() { - switch { - // 2 - case inUseErrors: - // One of the specified images has child images or is - // being used by a container. - report.ExitCode = 2 - // 1 - case noSuchImageErrors && !(otherErrors || inUseErrors): - // One of the specified images did not exist, and no other - // failures. - report.ExitCode = 1 - // 0 + for _, e := range rmErrors { + switch errors.Cause(e) { + case define.ErrNoSuchImage: + noSuchImageErrors = true + case define.ErrImageInUse, storage.ErrImageUsedByContainer: + inUseErrors = true default: - // Nothing to do. - } - if deleteError != nil { - // go-multierror has a trailing new line which we need to remove to normalize the string. - finalError = deleteError.ErrorOrNil() - finalError = errors.New(strings.TrimSpace(finalError.Error())) + otherErrors = true } + } + + switch { + case inUseErrors: + // One of the specified images has child images or is + // being used by a container. + return 2 + case noSuchImageErrors && !(otherErrors || inUseErrors): + // One of the specified images did not exist, and no other + // failures. + return 1 + default: + return 125 + } +} + +// Remove removes one or more images from local storage. +func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (report *entities.ImageRemoveReport, rmErrors []error) { + report = &entities.ImageRemoveReport{} + + // Set the exit code at very end. + defer func() { + report.ExitCode = removeErrorsToExitCode(rmErrors) }() // deleteImage is an anonymous function to conveniently delete an image // without having to pass all local data around. deleteImage := func(img *image.Image) error { results, err := ir.Libpod.RemoveImage(ctx, img, opts.Force) - switch errors.Cause(err) { - case nil: - break - case define.ErrNoSuchImage: - inUseErrors = true // ExitCode is expected - case storage.ErrImageUsedByContainer: - inUseErrors = true // Important for exit codes in Podman. - return errors.New( - fmt.Sprintf("A container associated with containers/storage, i.e. via Buildah, CRI-O, etc., may be associated with this image: %-12.12s\n", img.ID())) - case define.ErrImageInUse: - inUseErrors = true - return err - default: - otherErrors = true // Important for exit codes in Podman. + if err != nil { return err } - report.Deleted = append(report.Deleted, results.Deleted) report.Untagged = append(report.Untagged, results.Untagged...) return nil @@ -499,9 +494,7 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie for { storageImages, err := ir.Libpod.ImageRuntime().GetRWImages() if err != nil { - deleteError = multierror.Append(deleteError, - errors.Wrapf(err, "unable to query local images")) - otherErrors = true // Important for exit codes in Podman. + rmErrors = append(rmErrors, err) return } // No images (left) to remove, so we're done. @@ -510,9 +503,7 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie } // Prevent infinity loops by making a delete-progress check. if previousImages == len(storageImages) { - otherErrors = true // Important for exit codes in Podman. - deleteError = multierror.Append(deleteError, - errors.New("unable to delete all images, check errors and re-run image removal if needed")) + rmErrors = append(rmErrors, errors.New("unable to delete all images, check errors and re-run image removal if needed")) break } previousImages = len(storageImages) @@ -520,15 +511,15 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie for _, img := range storageImages { isParent, err := img.IsParent(ctx) if err != nil { - otherErrors = true // Important for exit codes in Podman. - deleteError = multierror.Append(deleteError, err) + rmErrors = append(rmErrors, err) + continue } // Skip parent images. if isParent { continue } if err := deleteImage(img); err != nil { - deleteError = multierror.Append(deleteError, err) + rmErrors = append(rmErrors, err) } } } @@ -539,21 +530,13 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie // Delete only the specified images. for _, id := range images { img, err := ir.Libpod.ImageRuntime().NewFromLocal(id) - switch errors.Cause(err) { - case nil: - break - case image.ErrNoSuchImage: - noSuchImageErrors = true // Important for exit codes in Podman. - fallthrough - default: - deleteError = multierror.Append(deleteError, errors.Wrapf(err, "failed to remove image '%s'", id)) + if err != nil { + rmErrors = append(rmErrors, err) continue } - err = deleteImage(img) if err != nil { - otherErrors = true // Important for exit codes in Podman. - deleteError = multierror.Append(deleteError, err) + rmErrors = append(rmErrors, err) } } diff --git a/pkg/domain/infra/abi/pods_stats.go b/pkg/domain/infra/abi/pods_stats.go index a41c01da0..c6befcf95 100644 --- a/pkg/domain/infra/abi/pods_stats.go +++ b/pkg/domain/infra/abi/pods_stats.go @@ -8,6 +8,7 @@ import ( "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/utils" "github.com/docker/go-units" "github.com/pkg/errors" ) @@ -68,7 +69,7 @@ func combineHumanValues(a, b uint64) string { } func floatToPercentString(f float64) string { - strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f) + strippedFloat, err := utils.RemoveScientificNotationFromFloat(f) if err != nil || strippedFloat == 0 { // If things go bazinga, return a safe value return "--" diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 32f9c4e36..227b660f7 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -387,3 +387,7 @@ func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, // Shutdown Libpod engine func (ic *ContainerEngine) Shutdown(_ context.Context) { } + +func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) error { + return errors.New("not implemented") +} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index dcc5fc3e7..00893194c 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -20,8 +20,8 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.Boo return &entities.BoolReport{Value: found}, err } -func (ir *ImageEngine) Remove(ctx context.Context, imagesArg []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, error) { - return images.Remove(ir.ClientCxt, imagesArg, opts) +func (ir *ImageEngine) Remove(ctx context.Context, imagesArg []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, []error) { + return images.BatchRemove(ir.ClientCxt, imagesArg, opts) } func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) ([]*entities.ImageSummary, error) { diff --git a/pkg/errorhandling/errorhandling.go b/pkg/errorhandling/errorhandling.go index 970d47636..3117b0ca4 100644 --- a/pkg/errorhandling/errorhandling.go +++ b/pkg/errorhandling/errorhandling.go @@ -2,10 +2,46 @@ package errorhandling import ( "os" + "strings" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) +// JoinErrors converts the error slice into a single human-readable error. +func JoinErrors(errs []error) error { + if len(errs) == 0 { + return nil + } + + // `multierror` appends new lines which we need to remove to prevent + // blank lines when printing the error. + var multiE *multierror.Error + multiE = multierror.Append(multiE, errs...) + return errors.New(strings.TrimSpace(multiE.ErrorOrNil().Error())) +} + +// ErrorsToString converts the slice of errors into a slice of corresponding +// error messages. +func ErrorsToStrings(errs []error) []string { + strErrs := make([]string, len(errs)) + for i := range errs { + strErrs[i] = errs[i].Error() + } + return strErrs +} + +// StringsToErrors converts a slice of error messages into a slice of +// corresponding errors. +func StringsToErrors(strErrs []string) []error { + errs := make([]error, len(strErrs)) + for i := range strErrs { + errs[i] = errors.New(strErrs[i]) + } + return errs +} + // SyncQuiet syncs a file and logs any error. Should only be used within // a defer. func SyncQuiet(f *os.File) { diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go index 8fba07c18..258cb8652 100644 --- a/pkg/varlinkapi/containers.go +++ b/pkg/varlinkapi/containers.go @@ -331,7 +331,7 @@ func (i *VarlinkAPI) GetContainerStats(call iopodman.VarlinkCall, name string) e if err != nil { return call.ReplyContainerNotFound(name, err.Error()) } - containerStats, err := ctr.GetContainerStats(&libpod.ContainerStats{}) + containerStats, err := ctr.GetContainerStats(&define.ContainerStats{}) if err != nil { if errors.Cause(err) == define.ErrCtrStateInvalid { return call.ReplyNoContainerRunning() diff --git a/pkg/varlinkapi/pods.go b/pkg/varlinkapi/pods.go index 5a9360447..aeb3cdcb8 100644 --- a/pkg/varlinkapi/pods.go +++ b/pkg/varlinkapi/pods.go @@ -8,12 +8,12 @@ import ( "strconv" "syscall" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + iopodman "github.com/containers/libpod/pkg/varlink" "github.com/cri-o/ocicni/pkg/ocicni" "github.com/docker/go-connections/nat" "github.com/pkg/errors" - - "github.com/containers/libpod/libpod" - iopodman "github.com/containers/libpod/pkg/varlink" ) // CreatePod ... @@ -263,7 +263,7 @@ func (i *VarlinkAPI) GetPodStats(call iopodman.VarlinkCall, name string) error { if err != nil { return call.ReplyPodNotFound(name, err.Error()) } - prevStats := make(map[string]*libpod.ContainerStats) + prevStats := make(map[string]*define.ContainerStats) podStats, err := pod.GetPodStats(prevStats) if err != nil { return call.ReplyErrorOccurred(err.Error()) diff --git a/pkg/varlinkapi/remote_client.go b/pkg/varlinkapi/remote_client.go index a16d11dec..88e410de6 100644 --- a/pkg/varlinkapi/remote_client.go +++ b/pkg/varlinkapi/remote_client.go @@ -3,14 +3,14 @@ package varlinkapi import ( - "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" iopodman "github.com/containers/libpod/pkg/varlink" ) // ContainerStatsToLibpodContainerStats converts the varlink containerstats to a libpod // container stats -func ContainerStatsToLibpodContainerStats(stats iopodman.ContainerStats) libpod.ContainerStats { - cstats := libpod.ContainerStats{ +func ContainerStatsToLibpodContainerStats(stats iopodman.ContainerStats) define.ContainerStats { + cstats := define.ContainerStats{ ContainerID: stats.Id, Name: stats.Name, CPU: stats.Cpu, diff --git a/test/e2e/stats_test.go b/test/e2e/stats_test.go index 32f7cc520..762417a17 100644 --- a/test/e2e/stats_test.go +++ b/test/e2e/stats_test.go @@ -21,7 +21,6 @@ var _ = Describe("Podman stats", func() { ) BeforeEach(func() { - Skip(v2fail) cgroupsv2, err := cgroups.IsCgroup2UnifiedMode() Expect(err).To(BeNil()) diff --git a/test/system/015-help.bats b/test/system/015-help.bats index 6c3d617dc..42d3bd3ec 100644 --- a/test/system/015-help.bats +++ b/test/system/015-help.bats @@ -28,13 +28,20 @@ function check_help() { local subcommands_found=0 for cmd in $(podman_commands "$@"); do - dprint "podman $@ $cmd --help" + # Human-readable podman command string, with multiple spaces collapsed + command_string="podman $* $cmd" + command_string=${command_string// / } # 'podman x' -> 'podman x' + + dprint "$command_string --help" run_podman "$@" $cmd --help # The line immediately after 'Usage:' gives us a 1-line synopsis usage=$(echo "$output" | grep -A1 '^Usage:' | tail -1) [ -n "$usage" ] || die "podman $cmd: no Usage message found" + # e.g. 'podman ps' should not show 'podman container ps' in usage + is "$usage" " $command_string .*" "Usage string matches command" + # If usage ends in '[command]', recurse into subcommands if expr "$usage" : '.*\[command\]$' >/dev/null; then subcommands_found=$(expr $subcommands_found + 1) @@ -46,10 +53,10 @@ function check_help() { # Confirm that by running with 'invalid-arg' and expecting failure. if expr "$usage" : '.*\[flags\]$' >/dev/null; then if [ "$cmd" != "help" ]; then - dprint "podman $@ $cmd invalid-arg" + dprint "$command_string invalid-arg" run_podman 125 "$@" $cmd invalid-arg is "$output" "Error: .* takes no arguments" \ - "'podman $@ $cmd' with extra (invalid) arguments" + "'$command_string' with extra (invalid) arguments" fi fi @@ -69,10 +76,10 @@ function check_help() { # The </dev/null protects us from 'podman login' which will # try to read username/password from stdin. - dprint "podman $@ $cmd (without required args)" + dprint "$command_string (without required args)" run_podman 125 "$@" $cmd </dev/null is "$output" "Error:.* \(require\|specif\|must\|provide\|need\|choose\|accepts\)" \ - "'podman $@ $cmd' without required arg" + "'$command_string' without required arg" fi count=$(expr $count + 1) diff --git a/utils/utils.go b/utils/utils.go index cf58ca3fb..27ce1821d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/exec" + "strconv" "strings" "github.com/containers/storage/pkg/archive" @@ -125,3 +126,21 @@ func Tar(source string) (io.ReadCloser, error) { logrus.Debugf("creating tarball of %s", source) return archive.Tar(source, archive.Uncompressed) } + +// RemoveScientificNotationFromFloat returns a float without any +// scientific notation if the number has any. +// golang does not handle conversion of float64s that have scientific +// notation in them and otherwise stinks. please replace this if you have +// a better implementation. +func RemoveScientificNotationFromFloat(x float64) (float64, error) { + bigNum := strconv.FormatFloat(x, 'g', -1, 64) + breakPoint := strings.IndexAny(bigNum, "Ee") + if breakPoint > 0 { + bigNum = bigNum[:breakPoint] + } + result, err := strconv.ParseFloat(bigNum, 64) + if err != nil { + return x, errors.Wrapf(err, "unable to remove scientific number from calculations") + } + return result, nil +} |