mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	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>
This commit is contained in:
		
							parent
							
								
									f2e11fb8d1
								
							
						
					
					
						commit
						33f4d68f4d
					
				
					 11 changed files with 263 additions and 0 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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),
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
									
								
								daemon/prune.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								daemon/prune.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue