From ec4060aef6c77c049fc2c3b6438ebb5590f6ab69 Mon Sep 17 00:00:00 2001
From: Sujil02 <sushah@redhat.com>
Date: Wed, 8 Apr 2020 04:49:32 -0400
Subject: Ability to prune container in api V2

Adds ability to prune containers for v2.
Adds client side prompt with force flag and filters options to prune.

Signed-off-by: Sujil02 <sushah@redhat.com>
---
 cmd/podman/containers_prune.go              |  4 +-
 cmd/podmanV2/containers/prune.go            | 86 +++++++++++++++++++++++++++++
 libpod/runtime_ctr.go                       |  3 +-
 pkg/api/handlers/compat/containers_prune.go | 13 ++---
 pkg/bindings/containers/containers.go       | 10 ++--
 pkg/bindings/test/containers_test.go        | 65 ++++++++++++++++++++++
 pkg/domain/entities/containers.go           | 16 +++++-
 pkg/domain/entities/engine_container.go     |  1 +
 pkg/domain/infra/abi/containers.go          | 17 ++++++
 pkg/domain/infra/tunnel/containers.go       |  4 ++
 10 files changed, 201 insertions(+), 18 deletions(-)
 create mode 100644 cmd/podmanV2/containers/prune.go

diff --git a/cmd/podman/containers_prune.go b/cmd/podman/containers_prune.go
index cd9817e7e..3953a489d 100644
--- a/cmd/podman/containers_prune.go
+++ b/cmd/podman/containers_prune.go
@@ -19,12 +19,12 @@ var (
 	pruneContainersDescription = `
 	podman container prune
 
-	Removes all exited containers
+	Removes all stopped | exited containers
 `
 	_pruneContainersCommand = &cobra.Command{
 		Use:   "prune",
 		Args:  noSubArgs,
-		Short: "Remove all stopped containers",
+		Short: "Remove all stopped | exited containers",
 		Long:  pruneContainersDescription,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			pruneContainersCommand.InputArgs = args
diff --git a/cmd/podmanV2/containers/prune.go b/cmd/podmanV2/containers/prune.go
new file mode 100644
index 000000000..2d3af5d1d
--- /dev/null
+++ b/cmd/podmanV2/containers/prune.go
@@ -0,0 +1,86 @@
+package containers
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/containers/libpod/cmd/podmanV2/registry"
+	"github.com/containers/libpod/cmd/podmanV2/utils"
+	"github.com/containers/libpod/pkg/domain/entities"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+)
+
+var (
+	pruneDescription = fmt.Sprintf(`podman container prune
+
+	Removes all stopped | exited containers`)
+	pruneCommand = &cobra.Command{
+		Use:     "prune [flags]",
+		Short:   "Remove all stopped | exited containers",
+		Long:    pruneDescription,
+		RunE:    prune,
+		Example: `podman container prune`,
+	}
+	force  bool
+	filter = []string{}
+)
+
+func init() {
+	registry.Commands = append(registry.Commands, registry.CliCommand{
+		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+		Command: pruneCommand,
+		Parent:  containerCmd,
+	})
+	flags := pruneCommand.Flags()
+	flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation.  The default is false")
+	flags.StringArrayVar(&filter, "filter", []string{}, "Provide filter values (e.g. 'label=<key>=<value>')")
+}
+
+func prune(cmd *cobra.Command, args []string) error {
+	var (
+		errs         utils.OutputErrors
+		pruneOptions = entities.ContainerPruneOptions{}
+	)
+	if len(args) > 0 {
+		return errors.Errorf("`%s` takes no arguments", cmd.CommandPath())
+	}
+	if !force {
+		reader := bufio.NewReader(os.Stdin)
+		fmt.Println("WARNING! This will remove all stopped containers.")
+		fmt.Print("Are you sure you want to continue? [y/N] ")
+		answer, err := reader.ReadString('\n')
+		if err != nil {
+			return errors.Wrapf(err, "error reading input")
+		}
+		if strings.ToLower(answer)[0] != 'y' {
+			return nil
+		}
+	}
+
+	// TODO Remove once filter refactor is finished and url.Values done.
+	for _, f := range filter {
+		t := strings.SplitN(f, "=", 2)
+		pruneOptions.Filters = make(url.Values)
+		if len(t) < 2 {
+			return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
+		}
+		pruneOptions.Filters.Add(t[0], t[1])
+	}
+	responses, err := registry.ContainerEngine().ContainerPrune(context.Background(), pruneOptions)
+
+	if err != nil {
+		return err
+	}
+	for k := range responses.ID {
+		fmt.Println(k)
+	}
+	for _, v := range responses.Err {
+		errs = append(errs, v)
+	}
+	return errs.PrintErrors()
+}
diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go
index 207ac6477..9d3e69d56 100644
--- a/libpod/runtime_ctr.go
+++ b/libpod/runtime_ctr.go
@@ -887,8 +887,9 @@ func (r *Runtime) PruneContainers(filterFuncs []ContainerFilter) (map[string]int
 			continue
 		}
 		err = r.RemoveContainer(context.Background(), ctr, false, false)
-		pruneErrors[ctr.ID()] = err
 		if err != nil {
+			pruneErrors[ctr.ID()] = err
+		} else {
 			prunedContainers[ctr.ID()] = size
 		}
 	}
diff --git a/pkg/api/handlers/compat/containers_prune.go b/pkg/api/handlers/compat/containers_prune.go
index a56c3903d..bf3aecd65 100644
--- a/pkg/api/handlers/compat/containers_prune.go
+++ b/pkg/api/handlers/compat/containers_prune.go
@@ -4,8 +4,8 @@ import (
 	"net/http"
 
 	"github.com/containers/libpod/libpod"
-	"github.com/containers/libpod/pkg/api/handlers"
 	"github.com/containers/libpod/pkg/api/handlers/utils"
+	"github.com/containers/libpod/pkg/domain/entities"
 	"github.com/docker/docker/api/types"
 	"github.com/gorilla/schema"
 	"github.com/pkg/errors"
@@ -40,14 +40,11 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) {
 
 	// Libpod response differs
 	if utils.IsLibpodRequest(r) {
-		var response []handlers.LibpodContainersPruneReport
-		for ctrID, size := range prunedContainers {
-			response = append(response, handlers.LibpodContainersPruneReport{ID: ctrID, SpaceReclaimed: size})
+		report := &entities.ContainerPruneReport{
+			Err: pruneErrors,
+			ID:  prunedContainers,
 		}
-		for ctrID, err := range pruneErrors {
-			response = append(response, handlers.LibpodContainersPruneReport{ID: ctrID, PruneError: err.Error()})
-		}
-		utils.WriteResponse(w, http.StatusOK, response)
+		utils.WriteResponse(w, http.StatusOK, report)
 		return
 	}
 	for ctrID, size := range prunedContainers {
diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go
index 963f0ec57..e74a256c7 100644
--- a/pkg/bindings/containers/containers.go
+++ b/pkg/bindings/containers/containers.go
@@ -60,10 +60,8 @@ func List(ctx context.Context, filters map[string][]string, all *bool, last *int
 // used for more granular selection of containers.  The main error returned indicates if there were runtime
 // errors like finding containers.  Errors specific to the removal of a container are in the PruneContainerResponse
 // structure.
-func Prune(ctx context.Context, filters map[string][]string) ([]string, error) {
-	var (
-		pruneResponse []string
-	)
+func Prune(ctx context.Context, filters map[string][]string) (*entities.ContainerPruneReport, error) {
+	var reports *entities.ContainerPruneReport
 	conn, err := bindings.GetClient(ctx)
 	if err != nil {
 		return nil, err
@@ -78,9 +76,9 @@ func Prune(ctx context.Context, filters map[string][]string) ([]string, error) {
 	}
 	response, err := conn.DoRequest(nil, http.MethodPost, "/containers/prune", params)
 	if err != nil {
-		return pruneResponse, err
+		return nil, err
 	}
-	return pruneResponse, response.Process(pruneResponse)
+	return reports, response.Process(&reports)
 }
 
 // Remove removes a container from local storage.  The force bool designates
diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go
index 0b1b9ecdd..e288dc368 100644
--- a/pkg/bindings/test/containers_test.go
+++ b/pkg/bindings/test/containers_test.go
@@ -531,4 +531,69 @@ var _ = Describe("Podman containers ", func() {
 		Expect(err).ToNot(BeNil())
 	})
 
+	It("podman prune stoped containers", func() {
+		// Start and stop a container to enter in exited state.
+		var name = "top"
+		_, err := bt.RunTopContainer(&name, &bindings.PFalse, nil)
+		Expect(err).To(BeNil())
+		err = containers.Stop(bt.conn, name, nil)
+		Expect(err).To(BeNil())
+
+		// Prune container should return no errors and one pruned container ID.
+		pruneResponse, err := containers.Prune(bt.conn, nil)
+		Expect(err).To(BeNil())
+		Expect(len(pruneResponse.Err)).To(Equal(0))
+		Expect(len(pruneResponse.ID)).To(Equal(1))
+	})
+
+	It("podman prune stoped containers with filters", func() {
+		// Start and stop a container to enter in exited state.
+		var name = "top"
+		_, err := bt.RunTopContainer(&name, &bindings.PFalse, nil)
+		Expect(err).To(BeNil())
+		err = containers.Stop(bt.conn, name, nil)
+		Expect(err).To(BeNil())
+
+		// Invalid filter keys should return error.
+		filtersIncorrect := map[string][]string{
+			"status": {"dummy"},
+		}
+		pruneResponse, err := containers.Prune(bt.conn, filtersIncorrect)
+		Expect(err).ToNot(BeNil())
+
+		// Mismatched filter params no container should be pruned.
+		filtersIncorrect = map[string][]string{
+			"name": {"r"},
+		}
+		pruneResponse, err = containers.Prune(bt.conn, filtersIncorrect)
+		Expect(err).To(BeNil())
+		Expect(len(pruneResponse.Err)).To(Equal(0))
+		Expect(len(pruneResponse.ID)).To(Equal(0))
+
+		// Valid filter params container should be pruned now.
+		filters := map[string][]string{
+			"name": {"top"},
+		}
+		pruneResponse, err = containers.Prune(bt.conn, filters)
+		Expect(err).To(BeNil())
+		Expect(len(pruneResponse.Err)).To(Equal(0))
+		Expect(len(pruneResponse.ID)).To(Equal(1))
+	})
+
+	It("podman prune running containers", func() {
+		// Start the container.
+		var name = "top"
+		_, err := bt.RunTopContainer(&name, &bindings.PFalse, nil)
+		Expect(err).To(BeNil())
+
+		// Check if the container is running.
+		data, err := containers.Inspect(bt.conn, name, nil)
+		Expect(err).To(BeNil())
+		Expect(data.State.Status).To(Equal("running"))
+
+		// Prune. Should return no error no prune response ID.
+		pruneResponse, err := containers.Prune(bt.conn, nil)
+		Expect(err).To(BeNil())
+		Expect(len(pruneResponse.ID)).To(Equal(0))
+	})
 })
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index f21af9ce4..52327a905 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -2,6 +2,7 @@ package entities
 
 import (
 	"io"
+	"net/url"
 	"os"
 	"time"
 
@@ -260,7 +261,7 @@ type ContainerRunOptions struct {
 }
 
 // ContainerRunReport describes the results of running
-//a container
+// a container
 type ContainerRunReport struct {
 	ExitCode int
 	Id       string
@@ -327,3 +328,16 @@ type ContainerUnmountReport struct {
 	Err error
 	Id  string
 }
+
+// ContainerPruneOptions describes the options needed
+// to prune a container from the CLI
+type ContainerPruneOptions struct {
+	Filters url.Values `json:"filters" schema:"filters"`
+}
+
+// ContainerPruneReport describes the results after pruning the
+// stopped containers.
+type ContainerPruneReport struct {
+	ID  map[string]int64
+	Err map[string]error
+}
diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go
index 2001ab49c..24e7995e7 100644
--- a/pkg/domain/entities/engine_container.go
+++ b/pkg/domain/entities/engine_container.go
@@ -13,6 +13,7 @@ type ContainerEngine interface {
 	ContainerAttach(ctx context.Context, nameOrId string, options AttachOptions) error
 	ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error)
 	ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error)
+	ContainerPrune(ctx context.Context, options ContainerPruneOptions) (*ContainerPruneReport, error)
 	ContainerCommit(ctx context.Context, nameOrId string, options CommitOptions) (*CommitReport, error)
 	ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error)
 	ContainerDiff(ctx context.Context, nameOrId string, options DiffOptions) (*DiffReport, error)
diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go
index f464df3ac..fc62a6c29 100644
--- a/pkg/domain/infra/abi/containers.go
+++ b/pkg/domain/infra/abi/containers.go
@@ -19,6 +19,7 @@ import (
 	"github.com/containers/libpod/libpod/events"
 	"github.com/containers/libpod/libpod/image"
 	"github.com/containers/libpod/libpod/logs"
+	"github.com/containers/libpod/pkg/api/handlers/utils"
 	"github.com/containers/libpod/pkg/checkpoint"
 	"github.com/containers/libpod/pkg/domain/entities"
 	"github.com/containers/libpod/pkg/domain/infra/abi/terminal"
@@ -173,6 +174,22 @@ func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []strin
 	return reports, nil
 }
 
+func (ic *ContainerEngine) ContainerPrune(ctx context.Context, options entities.ContainerPruneOptions) (*entities.ContainerPruneReport, error) {
+	filterFuncs, err := utils.GenerateFilterFuncsFromMap(ic.Libpod, options.Filters)
+	if err != nil {
+		return nil, err
+	}
+	prunedContainers, pruneErrors, err := ic.Libpod.PruneContainers(filterFuncs)
+	if err != nil {
+		return nil, err
+	}
+	report := entities.ContainerPruneReport{
+		ID:  prunedContainers,
+		Err: pruneErrors,
+	}
+	return &report, nil
+}
+
 func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []string, options entities.KillOptions) ([]*entities.KillReport, error) {
 	var (
 		reports []*entities.KillReport
diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go
index 05b62efcf..679bb371b 100644
--- a/pkg/domain/infra/tunnel/containers.go
+++ b/pkg/domain/infra/tunnel/containers.go
@@ -146,6 +146,10 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string,
 	return reports, nil
 }
 
+func (ic *ContainerEngine) ContainerPrune(ctx context.Context, options entities.ContainerPruneOptions) (*entities.ContainerPruneReport, error) {
+	return containers.Prune(ic.ClientCxt, options.Filters)
+}
+
 func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.InspectOptions) ([]*entities.ContainerInspectReport, error) {
 	var (
 		reports []*entities.ContainerInspectReport
-- 
cgit v1.2.3-54-g00ecf