From 5436e314417e4822d105d1efdbea4e5442d6f42d Mon Sep 17 00:00:00 2001
From: Sujil02 <sushah@redhat.com>
Date: Mon, 20 Apr 2020 10:10:41 -0400
Subject: Adding system prune for podman v2

Register system prune route, handler to support system prune,
Adds testcase to validate the system prune flow.

Signed-off-by: Sujil02 <sushah@redhat.com>
---
 pkg/api/handlers/compat/containers_prune.go |  35 ++++++---
 pkg/api/handlers/libpod/pods.go             |  14 +++-
 pkg/api/handlers/libpod/system.go           |  71 +++++++++++++++++++
 pkg/api/handlers/libpod/volumes.go          |  15 ++--
 pkg/api/server/register_system.go           |  17 +++++
 pkg/bindings/system/system.go               |  25 +++++++
 pkg/bindings/test/system_test.go            | 106 +++++++++++++++++++++++++++-
 pkg/domain/entities/system.go               |  14 ++++
 pkg/domain/infra/abi/images.go              |   1 -
 9 files changed, 280 insertions(+), 18 deletions(-)
 create mode 100644 pkg/api/handlers/libpod/system.go

diff --git a/pkg/api/handlers/compat/containers_prune.go b/pkg/api/handlers/compat/containers_prune.go
index b4e98ac1f..9d77f612b 100644
--- a/pkg/api/handlers/compat/containers_prune.go
+++ b/pkg/api/handlers/compat/containers_prune.go
@@ -38,21 +38,24 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) {
 			filterFuncs = append(filterFuncs, generatedFunc)
 		}
 	}
-	prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs)
-	if err != nil {
-		utils.InternalServerError(w, err)
-		return
-	}
 
 	// Libpod response differs
 	if utils.IsLibpodRequest(r) {
-		report := &entities.ContainerPruneReport{
-			Err: pruneErrors,
-			ID:  prunedContainers,
+		report, err := PruneContainersHelper(w, r, filterFuncs)
+		if err != nil {
+			utils.InternalServerError(w, err)
+			return
 		}
+
 		utils.WriteResponse(w, http.StatusOK, report)
 		return
 	}
+
+	prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return
+	}
 	for ctrID, size := range prunedContainers {
 		if pruneErrors[ctrID] == nil {
 			space += size
@@ -65,3 +68,19 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) {
 	}
 	utils.WriteResponse(w, http.StatusOK, report)
 }
+
+func PruneContainersHelper(w http.ResponseWriter, r *http.Request, filterFuncs []libpod.ContainerFilter) (
+	*entities.ContainerPruneReport, error) {
+	runtime := r.Context().Value("runtime").(*libpod.Runtime)
+	prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return nil, err
+	}
+
+	report := &entities.ContainerPruneReport{
+		Err: pruneErrors,
+		ID:  prunedContainers,
+	}
+	return report, nil
+}
diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go
index 618d48ac0..4eba4af05 100644
--- a/pkg/api/handlers/libpod/pods.go
+++ b/pkg/api/handlers/libpod/pods.go
@@ -230,14 +230,22 @@ func PodRestart(w http.ResponseWriter, r *http.Request) {
 }
 
 func PodPrune(w http.ResponseWriter, r *http.Request) {
+	reports, err := PodPruneHelper(w, r)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return
+	}
+	utils.WriteResponse(w, http.StatusOK, reports)
+}
+
+func PodPruneHelper(w http.ResponseWriter, r *http.Request) ([]*entities.PodPruneReport, error) {
 	var (
 		runtime = r.Context().Value("runtime").(*libpod.Runtime)
 		reports []*entities.PodPruneReport
 	)
 	responses, err := runtime.PrunePods(r.Context())
 	if err != nil {
-		utils.InternalServerError(w, err)
-		return
+		return nil, err
 	}
 	for k, v := range responses {
 		reports = append(reports, &entities.PodPruneReport{
@@ -245,7 +253,7 @@ func PodPrune(w http.ResponseWriter, r *http.Request) {
 			Id:  k,
 		})
 	}
-	utils.WriteResponse(w, http.StatusOK, reports)
+	return reports, nil
 }
 
 func PodPause(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/api/handlers/libpod/system.go b/pkg/api/handlers/libpod/system.go
new file mode 100644
index 000000000..98e33bf10
--- /dev/null
+++ b/pkg/api/handlers/libpod/system.go
@@ -0,0 +1,71 @@
+package libpod
+
+import (
+	"net/http"
+
+	"github.com/containers/libpod/libpod"
+	"github.com/containers/libpod/pkg/api/handlers/compat"
+	"github.com/containers/libpod/pkg/api/handlers/utils"
+	"github.com/containers/libpod/pkg/domain/entities"
+	"github.com/gorilla/schema"
+	"github.com/pkg/errors"
+)
+
+// SystemPrune removes unused data
+func SystemPrune(w http.ResponseWriter, r *http.Request) {
+	var (
+		decoder           = r.Context().Value("decoder").(*schema.Decoder)
+		runtime           = r.Context().Value("runtime").(*libpod.Runtime)
+		systemPruneReport = new(entities.SystemPruneReport)
+	)
+	query := struct {
+		All     bool `schema:"all"`
+		Volumes bool `schema:"volumes"`
+	}{}
+
+	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
+	}
+
+	podPruneReport, err := PodPruneHelper(w, r)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return
+	}
+	systemPruneReport.PodPruneReport = podPruneReport
+
+	// We could parallelize this, should we?
+	containerPruneReport, err := compat.PruneContainersHelper(w, r, nil)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return
+	}
+	systemPruneReport.ContainerPruneReport = containerPruneReport
+
+	results, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, nil)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return
+	}
+
+	report := entities.ImagePruneReport{
+		Report: entities.Report{
+			Id:  results,
+			Err: nil,
+		},
+	}
+
+	systemPruneReport.ImagePruneReport = &report
+
+	if query.Volumes {
+		volumePruneReport, err := pruneVolumesHelper(w, r)
+		if err != nil {
+			utils.InternalServerError(w, err)
+			return
+		}
+		systemPruneReport.VolumePruneReport = volumePruneReport
+	}
+	utils.WriteResponse(w, http.StatusOK, systemPruneReport)
+}
diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go
index 18c561a0d..c42ca407b 100644
--- a/pkg/api/handlers/libpod/volumes.go
+++ b/pkg/api/handlers/libpod/volumes.go
@@ -147,14 +147,22 @@ func ListVolumes(w http.ResponseWriter, r *http.Request) {
 }
 
 func PruneVolumes(w http.ResponseWriter, r *http.Request) {
+	reports, err := pruneVolumesHelper(w, r)
+	if err != nil {
+		utils.InternalServerError(w, err)
+		return
+	}
+	utils.WriteResponse(w, http.StatusOK, reports)
+}
+
+func pruneVolumesHelper(w http.ResponseWriter, r *http.Request) ([]*entities.VolumePruneReport, error) {
 	var (
 		runtime = r.Context().Value("runtime").(*libpod.Runtime)
 		reports []*entities.VolumePruneReport
 	)
 	pruned, err := runtime.PruneVolumes(r.Context())
 	if err != nil {
-		utils.InternalServerError(w, err)
-		return
+		return nil, err
 	}
 	for k, v := range pruned {
 		reports = append(reports, &entities.VolumePruneReport{
@@ -162,9 +170,8 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) {
 			Id:  k,
 		})
 	}
-	utils.WriteResponse(w, http.StatusOK, reports)
+	return reports, nil
 }
-
 func RemoveVolume(w http.ResponseWriter, r *http.Request) {
 	var (
 		runtime = r.Context().Value("runtime").(*libpod.Runtime)
diff --git a/pkg/api/server/register_system.go b/pkg/api/server/register_system.go
index 708ccd39b..7375a75c1 100644
--- a/pkg/api/server/register_system.go
+++ b/pkg/api/server/register_system.go
@@ -4,6 +4,7 @@ import (
 	"net/http"
 
 	"github.com/containers/libpod/pkg/api/handlers/compat"
+	"github.com/containers/libpod/pkg/api/handlers/libpod"
 	"github.com/gorilla/mux"
 )
 
@@ -11,5 +12,21 @@ func (s *APIServer) registerSystemHandlers(r *mux.Router) error {
 	r.Handle(VersionedPath("/system/df"), s.APIHandler(compat.GetDiskUsage)).Methods(http.MethodGet)
 	// Added non version path to URI to support docker non versioned paths
 	r.Handle("/system/df", s.APIHandler(compat.GetDiskUsage)).Methods(http.MethodGet)
+	// Swagger:operation POST /libpod/system/prune libpod pruneSystem
+	// ---
+	// tags:
+	//   - system
+	// summary: Prune unused data
+	// produces:
+	// - application/json
+	// responses:
+	//   200:
+	//     $ref: '#/responses/SystemPruneReport'
+	//   400:
+	//     $ref: "#/responses/BadParamError"
+	//   500:
+	//     $ref: "#/responses/InternalError"
+	r.Handle(VersionedPath("/libpod/system/prune"), s.APIHandler(libpod.SystemPrune)).Methods(http.MethodPost)
+
 	return nil
 }
diff --git a/pkg/bindings/system/system.go b/pkg/bindings/system/system.go
index fce8bbb8e..aee9a7ffd 100644
--- a/pkg/bindings/system/system.go
+++ b/pkg/bindings/system/system.go
@@ -6,9 +6,11 @@ import (
 	"io"
 	"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/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
@@ -59,3 +61,26 @@ func Events(ctx context.Context, eventChan chan (handlers.Event), cancelChan cha
 	}
 	return nil
 }
+
+// Prune removes all unused system data.
+func Prune(ctx context.Context, all, volumes *bool) (*entities.SystemPruneReport, error) {
+	var (
+		report entities.SystemPruneReport
+	)
+	conn, err := bindings.GetClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	params := url.Values{}
+	if all != nil {
+		params.Set("All", strconv.FormatBool(*all))
+	}
+	if volumes != nil {
+		params.Set("Volumes", strconv.FormatBool(*volumes))
+	}
+	response, err := conn.DoRequest(nil, http.MethodPost, "/system/prune", params)
+	if err != nil {
+		return nil, err
+	}
+	return &report, response.Process(&report)
+}
diff --git a/pkg/bindings/test/system_test.go b/pkg/bindings/test/system_test.go
index 3abc26b34..87e6d56dc 100644
--- a/pkg/bindings/test/system_test.go
+++ b/pkg/bindings/test/system_test.go
@@ -4,7 +4,12 @@ import (
 	"time"
 
 	"github.com/containers/libpod/pkg/api/handlers"
+	"github.com/containers/libpod/pkg/bindings"
+	"github.com/containers/libpod/pkg/bindings/containers"
+	"github.com/containers/libpod/pkg/bindings/pods"
 	"github.com/containers/libpod/pkg/bindings/system"
+	"github.com/containers/libpod/pkg/bindings/volumes"
+	"github.com/containers/libpod/pkg/domain/entities"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 	"github.com/onsi/gomega/gexec"
@@ -12,13 +17,16 @@ import (
 
 var _ = Describe("Podman system", func() {
 	var (
-		bt *bindingTest
-		s  *gexec.Session
+		bt     *bindingTest
+		s      *gexec.Session
+		newpod string
 	)
 
 	BeforeEach(func() {
 		bt = newBindingTest()
 		bt.RestoreImagesFromCache()
+		newpod = "newpod"
+		bt.Podcreate(&newpod)
 		s = bt.startAPIService()
 		time.Sleep(1 * time.Second)
 		err := bt.NewConnection()
@@ -48,4 +56,98 @@ var _ = Describe("Podman system", func() {
 		cancelChan <- true
 		Expect(len(messages)).To(BeNumerically("==", 3))
 	})
+
+	It("podman system prune - pod,container stopped", func() {
+		// Start and stop a pod to enter in exited state.
+		_, err := pods.Start(bt.conn, newpod)
+		Expect(err).To(BeNil())
+		_, err = pods.Stop(bt.conn, newpod, nil)
+		Expect(err).To(BeNil())
+		// 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())
+
+		systemPruneResponse, err := system.Prune(bt.conn, &bindings.PTrue, &bindings.PFalse)
+		Expect(err).To(BeNil())
+		Expect(len(systemPruneResponse.PodPruneReport)).To(Equal(1))
+		Expect(len(systemPruneResponse.ContainerPruneReport.ID)).To(Equal(1))
+		Expect(len(systemPruneResponse.ImagePruneReport.Report.Id)).
+			To(BeNumerically(">", 0))
+		Expect(systemPruneResponse.ImagePruneReport.Report.Id).
+			To(ContainElement("docker.io/library/alpine:latest"))
+		Expect(len(systemPruneResponse.VolumePruneReport)).To(Equal(0))
+	})
+
+	It("podman system prune running alpine container", func() {
+		// Start and stop a pod to enter in exited state.
+		_, err := pods.Start(bt.conn, newpod)
+		Expect(err).To(BeNil())
+		_, err = pods.Stop(bt.conn, newpod, nil)
+		Expect(err).To(BeNil())
+
+		// 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())
+
+		// Start container and leave in running
+		var name2 = "top2"
+		_, err = bt.RunTopContainer(&name2, &bindings.PFalse, nil)
+		Expect(err).To(BeNil())
+
+		// Adding an unused volume
+		_, err = volumes.Create(bt.conn, entities.VolumeCreateOptions{})
+		Expect(err).To(BeNil())
+
+		systemPruneResponse, err := system.Prune(bt.conn, &bindings.PTrue, &bindings.PFalse)
+		Expect(err).To(BeNil())
+		Expect(len(systemPruneResponse.PodPruneReport)).To(Equal(1))
+		Expect(len(systemPruneResponse.ContainerPruneReport.ID)).To(Equal(1))
+		Expect(len(systemPruneResponse.ImagePruneReport.Report.Id)).
+			To(BeNumerically(">", 0))
+		// Alpine image should not be pruned as used by running container
+		Expect(systemPruneResponse.ImagePruneReport.Report.Id).
+			ToNot(ContainElement("docker.io/library/alpine:latest"))
+		// Though unsed volume is available it should not be pruned as flag set to false.
+		Expect(len(systemPruneResponse.VolumePruneReport)).To(Equal(0))
+	})
+
+	It("podman system prune running alpine container volume prune", func() {
+		// Start a pod and leave it running
+		_, err := pods.Start(bt.conn, newpod)
+		Expect(err).To(BeNil())
+
+		// 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())
+
+		// Start second container and leave in running
+		var name2 = "top2"
+		_, err = bt.RunTopContainer(&name2, &bindings.PFalse, nil)
+		Expect(err).To(BeNil())
+
+		// Adding an unused volume should work
+		_, err = volumes.Create(bt.conn, entities.VolumeCreateOptions{})
+		Expect(err).To(BeNil())
+
+		systemPruneResponse, err := system.Prune(bt.conn, &bindings.PTrue, &bindings.PTrue)
+		Expect(err).To(BeNil())
+		Expect(len(systemPruneResponse.PodPruneReport)).To(Equal(0))
+		Expect(len(systemPruneResponse.ContainerPruneReport.ID)).To(Equal(1))
+		Expect(len(systemPruneResponse.ImagePruneReport.Report.Id)).
+			To(BeNumerically(">", 0))
+		// Alpine image should not be pruned as used by running container
+		Expect(systemPruneResponse.ImagePruneReport.Report.Id).
+			ToNot(ContainElement("docker.io/library/alpine:latest"))
+		// Volume should be pruned now as flag set true
+		Expect(len(systemPruneResponse.VolumePruneReport)).To(Equal(1))
+	})
 })
diff --git a/pkg/domain/entities/system.go b/pkg/domain/entities/system.go
index 3ddc04293..de93a382f 100644
--- a/pkg/domain/entities/system.go
+++ b/pkg/domain/entities/system.go
@@ -12,3 +12,17 @@ type ServiceOptions struct {
 	Timeout time.Duration  // duration of inactivity the service should wait before shutting down
 	Command *cobra.Command // CLI command provided. Used in V1 code
 }
+
+// SystemPruneOptions provides options to prune system.
+type SystemPruneOptions struct {
+	All    bool
+	Volume bool
+}
+
+// SystemPruneReport provides report after system prune is executed.
+type SystemPruneReport struct {
+	PodPruneReport []*PodPruneReport
+	*ContainerPruneReport
+	*ImagePruneReport
+	VolumePruneReport []*VolumePruneReport
+}
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index 0f710ad28..68758a71d 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -117,7 +117,6 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption
 			Id:  results,
 			Err: nil,
 		},
-		Size: 0,
 	}
 	return &report, nil
 }
-- 
cgit v1.2.3-54-g00ecf