github.com/moby/docker@v26.1.3+incompatible/daemon/prune.go (about)

     1  package daemon // import "github.com/docker/docker/daemon"
     2  
     3  import (
     4  	"context"
     5  	"regexp"
     6  	"strconv"
     7  	"sync/atomic"
     8  	"time"
     9  
    10  	"github.com/containerd/log"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/backend"
    13  	"github.com/docker/docker/api/types/events"
    14  	"github.com/docker/docker/api/types/filters"
    15  	timetypes "github.com/docker/docker/api/types/time"
    16  	"github.com/docker/docker/errdefs"
    17  	"github.com/docker/docker/libnetwork"
    18  	"github.com/docker/docker/runconfig"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  var (
    23  	// errPruneRunning is returned when a prune request is received while
    24  	// one is in progress
    25  	errPruneRunning = errdefs.Conflict(errors.New("a prune operation is already running"))
    26  
    27  	containersAcceptedFilters = map[string]bool{
    28  		"label":  true,
    29  		"label!": true,
    30  		"until":  true,
    31  	}
    32  
    33  	networksAcceptedFilters = map[string]bool{
    34  		"label":  true,
    35  		"label!": true,
    36  		"until":  true,
    37  	}
    38  )
    39  
    40  // ContainersPrune removes unused containers
    41  func (daemon *Daemon) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (*types.ContainersPruneReport, error) {
    42  	if !atomic.CompareAndSwapInt32(&daemon.pruneRunning, 0, 1) {
    43  		return nil, errPruneRunning
    44  	}
    45  	defer atomic.StoreInt32(&daemon.pruneRunning, 0)
    46  
    47  	rep := &types.ContainersPruneReport{}
    48  
    49  	// make sure that only accepted filters have been received
    50  	err := pruneFilters.Validate(containersAcceptedFilters)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	until, err := getUntilFromPruneFilters(pruneFilters)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	cfg := &daemon.config().Config
    61  	allContainers := daemon.List()
    62  	for _, c := range allContainers {
    63  		select {
    64  		case <-ctx.Done():
    65  			log.G(ctx).Debugf("ContainersPrune operation cancelled: %#v", *rep)
    66  			return rep, nil
    67  		default:
    68  		}
    69  
    70  		if !c.IsRunning() {
    71  			if !until.IsZero() && c.Created.After(until) {
    72  				continue
    73  			}
    74  			if !matchLabels(pruneFilters, c.Config.Labels) {
    75  				continue
    76  			}
    77  			cSize, _, err := daemon.imageService.GetContainerLayerSize(ctx, c.ID)
    78  			if err != nil {
    79  				return nil, err
    80  			}
    81  			// TODO: sets RmLink to true?
    82  			err = daemon.containerRm(cfg, c.ID, &backend.ContainerRmConfig{})
    83  			if err != nil {
    84  				log.G(ctx).Warnf("failed to prune container %s: %v", c.ID, err)
    85  				continue
    86  			}
    87  			if cSize > 0 {
    88  				rep.SpaceReclaimed += uint64(cSize)
    89  			}
    90  			rep.ContainersDeleted = append(rep.ContainersDeleted, c.ID)
    91  		}
    92  	}
    93  	daemon.EventsService.Log(events.ActionPrune, events.ContainerEventType, events.Actor{
    94  		Attributes: map[string]string{"reclaimed": strconv.FormatUint(rep.SpaceReclaimed, 10)},
    95  	})
    96  	return rep, nil
    97  }
    98  
    99  // localNetworksPrune removes unused local networks
   100  func (daemon *Daemon) localNetworksPrune(ctx context.Context, pruneFilters filters.Args) *types.NetworksPruneReport {
   101  	rep := &types.NetworksPruneReport{}
   102  
   103  	until, _ := getUntilFromPruneFilters(pruneFilters)
   104  
   105  	// When the function returns true, the walk will stop.
   106  	daemon.netController.WalkNetworks(func(nw *libnetwork.Network) bool {
   107  		select {
   108  		case <-ctx.Done():
   109  			// context cancelled
   110  			return true
   111  		default:
   112  		}
   113  		if nw.ConfigOnly() {
   114  			return false
   115  		}
   116  		if !until.IsZero() && nw.Created().After(until) {
   117  			return false
   118  		}
   119  		if !matchLabels(pruneFilters, nw.Labels()) {
   120  			return false
   121  		}
   122  		nwName := nw.Name()
   123  		if runconfig.IsPreDefinedNetwork(nwName) {
   124  			return false
   125  		}
   126  		if len(nw.Endpoints()) > 0 {
   127  			return false
   128  		}
   129  		if err := daemon.DeleteNetwork(nw.ID()); err != nil {
   130  			log.G(ctx).Warnf("could not remove local network %s: %v", nwName, err)
   131  			return false
   132  		}
   133  		rep.NetworksDeleted = append(rep.NetworksDeleted, nwName)
   134  		return false
   135  	})
   136  	return rep
   137  }
   138  
   139  // clusterNetworksPrune removes unused cluster networks
   140  func (daemon *Daemon) clusterNetworksPrune(ctx context.Context, pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
   141  	rep := &types.NetworksPruneReport{}
   142  
   143  	until, _ := getUntilFromPruneFilters(pruneFilters)
   144  
   145  	cluster := daemon.GetCluster()
   146  
   147  	if !cluster.IsManager() {
   148  		return rep, nil
   149  	}
   150  
   151  	networks, err := cluster.GetNetworks(pruneFilters)
   152  	if err != nil {
   153  		return rep, err
   154  	}
   155  	networkIsInUse := regexp.MustCompile(`network ([[:alnum:]]+) is in use`)
   156  	for _, nw := range networks {
   157  		select {
   158  		case <-ctx.Done():
   159  			return rep, nil
   160  		default:
   161  			if nw.Ingress {
   162  				// Routing-mesh network removal has to be explicitly invoked by user
   163  				continue
   164  			}
   165  			if !until.IsZero() && nw.Created.After(until) {
   166  				continue
   167  			}
   168  			if !matchLabels(pruneFilters, nw.Labels) {
   169  				continue
   170  			}
   171  			// https://github.com/docker/docker/issues/24186
   172  			// `docker network inspect` unfortunately displays ONLY those containers that are local to that node.
   173  			// So we try to remove it anyway and check the error
   174  			err = cluster.RemoveNetwork(nw.ID)
   175  			if err != nil {
   176  				// we can safely ignore the "network .. is in use" error
   177  				match := networkIsInUse.FindStringSubmatch(err.Error())
   178  				if len(match) != 2 || match[1] != nw.ID {
   179  					log.G(ctx).Warnf("could not remove cluster network %s: %v", nw.Name, err)
   180  				}
   181  				continue
   182  			}
   183  			rep.NetworksDeleted = append(rep.NetworksDeleted, nw.Name)
   184  		}
   185  	}
   186  	return rep, nil
   187  }
   188  
   189  // NetworksPrune removes unused networks
   190  func (daemon *Daemon) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (*types.NetworksPruneReport, error) {
   191  	if !atomic.CompareAndSwapInt32(&daemon.pruneRunning, 0, 1) {
   192  		return nil, errPruneRunning
   193  	}
   194  	defer atomic.StoreInt32(&daemon.pruneRunning, 0)
   195  
   196  	// make sure that only accepted filters have been received
   197  	err := pruneFilters.Validate(networksAcceptedFilters)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	if _, err := getUntilFromPruneFilters(pruneFilters); err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	rep := &types.NetworksPruneReport{}
   207  	if clusterRep, err := daemon.clusterNetworksPrune(ctx, pruneFilters); err == nil {
   208  		rep.NetworksDeleted = append(rep.NetworksDeleted, clusterRep.NetworksDeleted...)
   209  	}
   210  
   211  	localRep := daemon.localNetworksPrune(ctx, pruneFilters)
   212  	rep.NetworksDeleted = append(rep.NetworksDeleted, localRep.NetworksDeleted...)
   213  
   214  	select {
   215  	case <-ctx.Done():
   216  		log.G(ctx).Debugf("NetworksPrune operation cancelled: %#v", *rep)
   217  		return rep, nil
   218  	default:
   219  	}
   220  	daemon.EventsService.Log(events.ActionPrune, events.NetworkEventType, events.Actor{
   221  		Attributes: map[string]string{"reclaimed": "0"},
   222  	})
   223  	return rep, nil
   224  }
   225  
   226  func getUntilFromPruneFilters(pruneFilters filters.Args) (time.Time, error) {
   227  	until := time.Time{}
   228  	if !pruneFilters.Contains("until") {
   229  		return until, nil
   230  	}
   231  	untilFilters := pruneFilters.Get("until")
   232  	if len(untilFilters) > 1 {
   233  		return until, errdefs.InvalidParameter(errors.New("more than one until filter specified"))
   234  	}
   235  	ts, err := timetypes.GetTimestamp(untilFilters[0], time.Now())
   236  	if err != nil {
   237  		return until, errdefs.InvalidParameter(err)
   238  	}
   239  	seconds, nanoseconds, err := timetypes.ParseTimestamps(ts, 0)
   240  	if err != nil {
   241  		return until, errdefs.InvalidParameter(err)
   242  	}
   243  	until = time.Unix(seconds, nanoseconds)
   244  	return until, nil
   245  }
   246  
   247  func matchLabels(pruneFilters filters.Args, labels map[string]string) bool {
   248  	if !pruneFilters.MatchKVList("label", labels) {
   249  		return false
   250  	}
   251  	// By default MatchKVList will return true if field (like 'label!') does not exist
   252  	// So we have to add additional Contains("label!") check
   253  	if pruneFilters.Contains("label!") {
   254  		if pruneFilters.MatchKVList("label!", labels) {
   255  			return false
   256  		}
   257  	}
   258  	return true
   259  }