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 }