diff --git a/api/server/router/container/backend.go b/api/server/router/container/backend.go index 4e98f72d9c..55f2c9aa23 100644 --- a/api/server/router/container/backend.go +++ b/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 } diff --git a/api/server/router/container/container.go b/api/server/router/container/container.go index d6fea4c353..bbed7e9944 100644 --- a/api/server/router/container/container.go +++ b/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 diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index 24dc658ad3..481745082d 100644 --- a/api/server/router/container/container_routes.go +++ b/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) +} diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go index b03234ab75..aed2ded22a 100644 --- a/api/server/router/image/backend.go +++ b/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 { diff --git a/api/server/router/image/image.go b/api/server/router/image/image.go index 71c95e1e3d..54a4d51482 100644 --- a/api/server/router/image/image.go +++ b/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), } diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index d28892042a..961c547aab 100644 --- a/api/server/router/image/image_routes.go +++ b/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) +} diff --git a/api/server/router/volume/backend.go b/api/server/router/volume/backend.go index 80aebee7c4..3a7608e0bd 100644 --- a/api/server/router/volume/backend.go +++ b/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) } diff --git a/api/server/router/volume/volume.go b/api/server/router/volume/volume.go index 2683dcec52..4e9f972a69 100644 --- a/api/server/router/volume/volume.go +++ b/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), } diff --git a/api/server/router/volume/volume_routes.go b/api/server/router/volume/volume_routes.go index 1ab3fc253a..02cf53fd55 100644 --- a/api/server/router/volume/volume_routes.go +++ b/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) +} diff --git a/api/types/types.go b/api/types/types.go index 69a0ed5486..c15afd8049 100644 --- a/api/types/types.go +++ b/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 +} diff --git a/daemon/prune.go b/daemon/prune.go new file mode 100644 index 0000000000..3790cb8d36 --- /dev/null +++ b/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: + // 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 +}