github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/local/prune.go (about)

     1  /*
     2  Copyright 2020 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package local
    18  
    19  import (
    20  	"context"
    21  	"sort"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/docker/docker/api/types"
    26  	"github.com/dustin/go-humanize"
    27  
    28  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    30  )
    31  
    32  const (
    33  	usageRetries       = 5
    34  	usageRetryInterval = 500 * time.Millisecond
    35  )
    36  
    37  type pruner struct {
    38  	localDocker   docker.LocalDaemon
    39  	pruneChildren bool
    40  	pruneMutex    sync.Mutex
    41  	prunedImgIDs  map[string]struct{}
    42  }
    43  
    44  func newPruner(dockerAPI docker.LocalDaemon, pruneChildren bool) *pruner {
    45  	return &pruner{
    46  		localDocker:   dockerAPI,
    47  		pruneChildren: pruneChildren,
    48  		prunedImgIDs:  make(map[string]struct{}),
    49  	}
    50  }
    51  
    52  func (p *pruner) listImages(ctx context.Context, name string) ([]types.ImageSummary, error) {
    53  	imgs, err := p.localDocker.ImageList(ctx, name)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	if len(imgs) < 2 {
    58  		// no need to sort
    59  		return imgs, nil
    60  	}
    61  
    62  	sort.Slice(imgs, func(i, j int) bool {
    63  		// reverse sort
    64  		return imgs[i].Created > imgs[j].Created
    65  	})
    66  
    67  	return imgs, nil
    68  }
    69  
    70  func (p *pruner) cleanup(ctx context.Context, sync bool, artifacts []string) {
    71  	toPrune := p.collectImagesToPrune(ctx, artifacts)
    72  	if len(toPrune) == 0 {
    73  		return
    74  	}
    75  
    76  	if sync {
    77  		err := p.runPrune(ctx, toPrune)
    78  		if err != nil {
    79  			log.Entry(ctx).Debugf("Failed to prune: %v", err)
    80  		}
    81  	} else {
    82  		go func() {
    83  			err := p.runPrune(ctx, toPrune)
    84  			if err != nil {
    85  				log.Entry(ctx).Debugf("Failed to prune: %v", err)
    86  			}
    87  		}()
    88  	}
    89  }
    90  
    91  func (p *pruner) asynchronousCleanupOldImages(ctx context.Context, artifacts []string) {
    92  	p.cleanup(ctx, false /*async*/, artifacts)
    93  }
    94  
    95  func (p *pruner) synchronousCleanupOldImages(ctx context.Context, artifacts []string) {
    96  	p.cleanup(ctx, true /*sync*/, artifacts)
    97  }
    98  
    99  func (p *pruner) isPruned(id string) bool {
   100  	p.pruneMutex.Lock()
   101  	defer p.pruneMutex.Unlock()
   102  	_, pruned := p.prunedImgIDs[id]
   103  	return pruned
   104  }
   105  
   106  func (p *pruner) runPrune(ctx context.Context, ids []string) error {
   107  	log.Entry(ctx).Debugf("Going to prune: %v", ids)
   108  	// docker API does not support concurrent prune/utilization info request
   109  	// so let's serialize the access to it
   110  	t0 := time.Now()
   111  	p.pruneMutex.Lock()
   112  	log.Entry(ctx).Tracef("Prune mutex wait time: %v", time.Since(t0))
   113  	defer p.pruneMutex.Unlock()
   114  
   115  	beforeDu, err := p.diskUsage(ctx)
   116  	if err != nil {
   117  		if ctx.Err() != nil {
   118  			return ctx.Err()
   119  		}
   120  		log.Entry(ctx).Debugf("Failed to get docker usage info: %v", err)
   121  	}
   122  
   123  	pruned, err := p.localDocker.Prune(ctx, ids, p.pruneChildren)
   124  	for _, pi := range pruned {
   125  		p.prunedImgIDs[pi] = struct{}{}
   126  	}
   127  	if err != nil {
   128  		return err
   129  	}
   130  	// do not print usage report, if initial 'du' failed
   131  	if beforeDu > 0 {
   132  		afterDu, err := p.diskUsage(ctx)
   133  		if err != nil {
   134  			if ctx.Err() != nil {
   135  				return ctx.Err()
   136  			}
   137  			log.Entry(ctx).Debugf("Failed to get docker usage info: %v", err)
   138  			return nil
   139  		}
   140  		if beforeDu >= afterDu {
   141  			log.Entry(ctx).Infof("%d image(s) pruned. Reclaimed disk space: %s",
   142  				len(ids), humanize.Bytes(beforeDu-afterDu))
   143  		} else {
   144  			log.Entry(ctx).Infof("%d image(s) pruned", len(ids))
   145  		}
   146  	}
   147  	return nil
   148  }
   149  
   150  func (p *pruner) collectImagesToPrune(ctx context.Context, artifacts []string) []string {
   151  	// in case we're trying to build multiple images with the same ref in the same pipeline
   152  	imgNameCount := make(map[string]int)
   153  	for _, a := range artifacts {
   154  		imgNameCount[a]++
   155  	}
   156  	imgProcessed := make(map[string]struct{})
   157  	var rt []string
   158  	for _, a := range artifacts {
   159  		if _, ok := imgProcessed[a]; ok {
   160  			continue
   161  		}
   162  		imgProcessed[a] = struct{}{}
   163  
   164  		imgs, err := p.listImages(ctx, a)
   165  		if err != nil {
   166  			switch err {
   167  			case context.Canceled, context.DeadlineExceeded:
   168  				return []string{}
   169  			}
   170  			log.Entry(ctx).Warnf("failed to list images: %v", err)
   171  			continue
   172  		}
   173  		for i := imgNameCount[a]; i < len(imgs); i++ {
   174  			rt = append(rt, imgs[i].ID)
   175  		}
   176  	}
   177  	return rt
   178  }
   179  
   180  func (p *pruner) diskUsage(ctx context.Context) (uint64, error) {
   181  	for retry := 0; retry < usageRetries-1; retry++ {
   182  		usage, err := p.localDocker.DiskUsage(ctx)
   183  		if err == nil {
   184  			return usage, nil
   185  		}
   186  		if ctx.Err() != nil {
   187  			return 0, ctx.Err()
   188  		}
   189  		// DiskUsage(..) may return "operation in progress" error.
   190  		log.Entry(ctx).Debugf("[%d of %d] failed to get disk usage: %v. Will retry in %v",
   191  			retry, usageRetries, err, usageRetryInterval)
   192  		time.Sleep(usageRetryInterval)
   193  	}
   194  
   195  	usage, err := p.localDocker.DiskUsage(ctx)
   196  	if err == nil {
   197  		return usage, nil
   198  	}
   199  	log.Entry(ctx).Debugf("Failed to get usage after: %v. giving up", err)
   200  	return 0, err
   201  }