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:
Kenfe-Mickael Laventure 2016-08-23 16:25:43 -07:00
Родитель f2e11fb8d1
Коммит 33f4d68f4d
11 изменённых файлов: 263 добавлений и 0 удалений

Просмотреть файл

@ -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
Просмотреть файл

@ -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
}