Browse Source

Add /{containers,volumes,images}/prune API endpoint

These new endpoints request the daemon to delete all resources
considered "unused" in their respective category:
  - all stopped containers
  - all volumes not attached to any containers
  - images with no associated containers

Signed-off-by: Kenfe-Mickael Laventure <mickael.laventure@gmail.com>
Kenfe-Mickael Laventure 9 years ago
parent
commit
33f4d68f4d

+ 6 - 0
api/server/router/container/backend.go

@@ -62,6 +62,11 @@ type attachBackend interface {
 	ContainerAttach(name string, c *backend.ContainerAttachConfig) error
 }
 
+// systemBackend includes functions to implement to provide system wide containers functionality
+type systemBackend interface {
+	ContainersPrune(config *types.ContainersPruneConfig) (*types.ContainersPruneReport, error)
+}
+
 // Backend is all the methods that need to be implemented to provide container specific functionality.
 type Backend interface {
 	execBackend
@@ -69,4 +74,5 @@ type Backend interface {
 	stateBackend
 	monitorBackend
 	attachBackend
+	systemBackend
 }

+ 1 - 0
api/server/router/container/container.go

@@ -68,6 +68,7 @@ func (r *containerRouter) initRoutes() {
 		router.NewPostRoute("/exec/{name:.*}/resize", r.postContainerExecResize),
 		router.NewPostRoute("/containers/{name:.*}/rename", r.postContainerRename),
 		router.NewPostRoute("/containers/{name:.*}/update", r.postContainerUpdate),
+		router.NewPostRoute("/containers/prune", r.postContainersPrune),
 		// PUT
 		router.NewPutRoute("/containers/{name:.*}/archive", r.putContainersArchive),
 		// DELETE

+ 21 - 0
api/server/router/container/container_routes.go

@@ -524,3 +524,24 @@ func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.Respons
 	}
 	return err
 }
+
+func (s *containerRouter) postContainersPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+
+	if err := httputils.CheckForJSON(r); err != nil {
+		return err
+	}
+
+	var cfg types.ContainersPruneConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		return err
+	}
+
+	pruneReport, err := s.backend.ContainersPrune(&cfg)
+	if err != nil {
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
+}

+ 1 - 0
api/server/router/image/backend.go

@@ -28,6 +28,7 @@ type imageBackend interface {
 	Images(filterArgs string, filter string, all bool, withExtraAttrs bool) ([]*types.Image, error)
 	LookupImage(name string) (*types.ImageInspect, error)
 	TagImage(imageName, repository, tag string) error
+	ImagesPrune(config *types.ImagesPruneConfig) (*types.ImagesPruneReport, error)
 }
 
 type importExportBackend interface {

+ 1 - 0
api/server/router/image/image.go

@@ -43,6 +43,7 @@ func (r *imageRouter) initRoutes() {
 		router.Cancellable(router.NewPostRoute("/images/create", r.postImagesCreate)),
 		router.Cancellable(router.NewPostRoute("/images/{name:.*}/push", r.postImagesPush)),
 		router.NewPostRoute("/images/{name:.*}/tag", r.postImagesTag),
+		router.NewPostRoute("/images/prune", r.postImagesPrune),
 		// DELETE
 		router.NewDeleteRoute("/images/{name:.*}", r.deleteImages),
 	}

+ 21 - 0
api/server/router/image/image_routes.go

@@ -314,3 +314,24 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
 	}
 	return httputils.WriteJSON(w, http.StatusOK, query.Results)
 }
+
+func (s *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+
+	if err := httputils.CheckForJSON(r); err != nil {
+		return err
+	}
+
+	var cfg types.ImagesPruneConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		return err
+	}
+
+	pruneReport, err := s.backend.ImagesPrune(&cfg)
+	if err != nil {
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
+}

+ 1 - 0
api/server/router/volume/backend.go

@@ -12,4 +12,5 @@ type Backend interface {
 	VolumeInspect(name string) (*types.Volume, error)
 	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
 	VolumeRm(name string, force bool) error
+	VolumesPrune(config *types.VolumesPruneConfig) (*types.VolumesPruneReport, error)
 }

+ 1 - 0
api/server/router/volume/volume.go

@@ -29,6 +29,7 @@ func (r *volumeRouter) initRoutes() {
 		router.NewGetRoute("/volumes/{name:.*}", r.getVolumeByName),
 		// POST
 		router.NewPostRoute("/volumes/create", r.postVolumesCreate),
+		router.NewPostRoute("/volumes/prune", r.postVolumesPrune),
 		// DELETE
 		router.NewDeleteRoute("/volumes/{name:.*}", r.deleteVolumes),
 	}

+ 21 - 0
api/server/router/volume/volume_routes.go

@@ -65,3 +65,24 @@ func (v *volumeRouter) deleteVolumes(ctx context.Context, w http.ResponseWriter,
 	w.WriteHeader(http.StatusNoContent)
 	return nil
 }
+
+func (v *volumeRouter) postVolumesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+
+	if err := httputils.CheckForJSON(r); err != nil {
+		return err
+	}
+
+	var cfg types.VolumesPruneConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		return err
+	}
+
+	pruneReport, err := v.backend.VolumesPrune(&cfg)
+	if err != nil {
+		return err
+	}
+	return httputils.WriteJSON(w, http.StatusOK, pruneReport)
+}

+ 37 - 0
api/types/types.go

@@ -539,3 +539,40 @@ type DiskUsage struct {
 	Containers []*Container
 	Volumes    []*Volume
 }
+
+// ImagesPruneConfig contains the configuration for Remote API:
+// POST "/image/prune"
+type ImagesPruneConfig struct {
+	DanglingOnly bool
+}
+
+// ContainersPruneConfig contains the configuration for Remote API:
+// POST "/image/prune"
+type ContainersPruneConfig struct {
+}
+
+// VolumesPruneConfig contains the configuration for Remote API:
+// POST "/images/prune"
+type VolumesPruneConfig struct {
+}
+
+// ContainersPruneReport contains the response for Remote API:
+// POST "/containers/prune"
+type ContainersPruneReport struct {
+	ContainersDeleted []string
+	SpaceReclaimed    uint64
+}
+
+// VolumesPruneReport contains the response for Remote API:
+// POST "/volumes/prune"
+type VolumesPruneReport struct {
+	VolumesDeleted []string
+	SpaceReclaimed uint64
+}
+
+// ImagesPruneReport contains the response for Remote API:
+// POST "/image/prune"
+type ImagesPruneReport struct {
+	ImagesDeleted  []ImageDelete
+	SpaceReclaimed uint64
+}

+ 152 - 0
daemon/prune.go

@@ -0,0 +1,152 @@
+package daemon
+
+import (
+	"github.com/Sirupsen/logrus"
+	"github.com/docker/distribution/digest"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/image"
+	"github.com/docker/docker/layer"
+	"github.com/docker/docker/pkg/directory"
+	"github.com/docker/docker/reference"
+	"github.com/docker/docker/volume"
+)
+
+// ContainersPrune remove unused containers
+func (daemon *Daemon) ContainersPrune(config *types.ContainersPruneConfig) (*types.ContainersPruneReport, error) {
+	rep := &types.ContainersPruneReport{}
+
+	allContainers := daemon.List()
+	for _, c := range allContainers {
+		if !c.IsRunning() {
+			cSize, _ := daemon.getSize(c)
+			// TODO: sets RmLink to true?
+			err := daemon.ContainerRm(c.ID, &types.ContainerRmConfig{})
+			if err != nil {
+				logrus.Warnf("failed to prune container %s: %v", c.ID)
+				continue
+			}
+			if cSize > 0 {
+				rep.SpaceReclaimed += uint64(cSize)
+			}
+			rep.ContainersDeleted = append(rep.ContainersDeleted, c.ID)
+		}
+	}
+
+	return rep, nil
+}
+
+// VolumesPrune remove unused local volumes
+func (daemon *Daemon) VolumesPrune(config *types.VolumesPruneConfig) (*types.VolumesPruneReport, error) {
+	rep := &types.VolumesPruneReport{}
+
+	pruneVols := func(v volume.Volume) error {
+		name := v.Name()
+		refs := daemon.volumes.Refs(v)
+
+		if len(refs) == 0 {
+			vSize, err := directory.Size(v.Path())
+			if err != nil {
+				logrus.Warnf("could not determine size of volume %s: %v", name, err)
+			}
+			err = daemon.volumes.Remove(v)
+			if err != nil {
+				logrus.Warnf("could not remove volume %s: %v", name, err)
+				return nil
+			}
+			rep.SpaceReclaimed += uint64(vSize)
+			rep.VolumesDeleted = append(rep.VolumesDeleted, name)
+		}
+
+		return nil
+	}
+
+	err := daemon.traverseLocalVolumes(pruneVols)
+
+	return rep, err
+}
+
+// ImagesPrune remove unused images
+func (daemon *Daemon) ImagesPrune(config *types.ImagesPruneConfig) (*types.ImagesPruneReport, error) {
+	rep := &types.ImagesPruneReport{}
+
+	var allImages map[image.ID]*image.Image
+	if config.DanglingOnly {
+		allImages = daemon.imageStore.Heads()
+	} else {
+		allImages = daemon.imageStore.Map()
+	}
+	allContainers := daemon.List()
+	imageRefs := map[string]bool{}
+	for _, c := range allContainers {
+		imageRefs[c.ID] = true
+	}
+
+	// Filter intermediary images and get their unique size
+	allLayers := daemon.layerStore.Map()
+	topImages := map[image.ID]*image.Image{}
+	for id, img := range allImages {
+		dgst := digest.Digest(id)
+		if len(daemon.referenceStore.References(dgst)) == 0 && len(daemon.imageStore.Children(id)) != 0 {
+			continue
+		}
+		topImages[id] = img
+	}
+
+	for id := range topImages {
+		dgst := digest.Digest(id)
+		hex := dgst.Hex()
+		if _, ok := imageRefs[hex]; ok {
+			continue
+		}
+
+		deletedImages := []types.ImageDelete{}
+		refs := daemon.referenceStore.References(dgst)
+		if len(refs) > 0 {
+			if config.DanglingOnly {
+				// Not a dangling image
+				continue
+			}
+
+			nrRefs := len(refs)
+			for _, ref := range refs {
+				// If nrRefs == 1, we have an image marked as myreponame:<none>
+				// i.e. the tag content was changed
+				if _, ok := ref.(reference.Canonical); ok && nrRefs > 1 {
+					continue
+				}
+				imgDel, err := daemon.ImageDelete(ref.String(), false, true)
+				if err != nil {
+					logrus.Warnf("could not delete reference %s: %v", ref.String(), err)
+					continue
+				}
+				deletedImages = append(deletedImages, imgDel...)
+			}
+		} else {
+			imgDel, err := daemon.ImageDelete(hex, false, true)
+			if err != nil {
+				logrus.Warnf("could not delete image %s: %v", hex, err)
+				continue
+			}
+			deletedImages = append(deletedImages, imgDel...)
+		}
+
+		rep.ImagesDeleted = append(rep.ImagesDeleted, deletedImages...)
+	}
+
+	// Compute how much space was freed
+	for _, d := range rep.ImagesDeleted {
+		if d.Deleted != "" {
+			chid := layer.ChainID(d.Deleted)
+			if l, ok := allLayers[chid]; ok {
+				diffSize, err := l.DiffSize()
+				if err != nil {
+					logrus.Warnf("failed to get layer %s size: %v", chid, err)
+					continue
+				}
+				rep.SpaceReclaimed += uint64(diffSize)
+			}
+		}
+	}
+
+	return rep, nil
+}