github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/commands/prune.go (about) 1 package commands 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 "text/tabwriter" 9 "time" 10 11 "github.com/docker/buildx/builder" 12 "github.com/docker/buildx/util/cobrautil/completion" 13 "github.com/docker/cli/cli" 14 "github.com/docker/cli/cli/command" 15 "github.com/docker/cli/opts" 16 "github.com/docker/docker/api/types/filters" 17 "github.com/docker/go-units" 18 "github.com/moby/buildkit/client" 19 "github.com/pkg/errors" 20 "github.com/spf13/cobra" 21 "golang.org/x/sync/errgroup" 22 ) 23 24 type pruneOptions struct { 25 builder string 26 all bool 27 filter opts.FilterOpt 28 keepStorage opts.MemBytes 29 force bool 30 verbose bool 31 } 32 33 const ( 34 normalWarning = `WARNING! This will remove all dangling build cache. Are you sure you want to continue?` 35 allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?` 36 ) 37 38 func runPrune(ctx context.Context, dockerCli command.Cli, opts pruneOptions) error { 39 pruneFilters := opts.filter.Value() 40 pruneFilters = command.PruneFilters(dockerCli, pruneFilters) 41 42 pi, err := toBuildkitPruneInfo(pruneFilters) 43 if err != nil { 44 return err 45 } 46 47 warning := normalWarning 48 if opts.all { 49 warning = allCacheWarning 50 } 51 52 if !opts.force { 53 if ok, err := prompt(ctx, dockerCli.In(), dockerCli.Out(), warning); err != nil { 54 return err 55 } else if !ok { 56 return nil 57 } 58 } 59 60 b, err := builder.New(dockerCli, builder.WithName(opts.builder)) 61 if err != nil { 62 return err 63 } 64 65 nodes, err := b.LoadNodes(ctx) 66 if err != nil { 67 return err 68 } 69 for _, node := range nodes { 70 if node.Err != nil { 71 return node.Err 72 } 73 } 74 75 ch := make(chan client.UsageInfo) 76 printed := make(chan struct{}) 77 78 tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 79 first := true 80 total := int64(0) 81 82 go func() { 83 defer close(printed) 84 for du := range ch { 85 total += du.Size 86 if opts.verbose { 87 printVerbose(tw, []*client.UsageInfo{&du}) 88 } else { 89 if first { 90 printTableHeader(tw) 91 first = false 92 } 93 printTableRow(tw, &du) 94 tw.Flush() 95 } 96 } 97 }() 98 99 eg, ctx := errgroup.WithContext(ctx) 100 for _, node := range nodes { 101 func(node builder.Node) { 102 eg.Go(func() error { 103 if node.Driver != nil { 104 c, err := node.Driver.Client(ctx) 105 if err != nil { 106 return err 107 } 108 popts := []client.PruneOption{ 109 client.WithKeepOpt(pi.KeepDuration, opts.keepStorage.Value()), 110 client.WithFilter(pi.Filter), 111 } 112 if opts.all { 113 popts = append(popts, client.PruneAll) 114 } 115 return c.Prune(ctx, ch, popts...) 116 } 117 return nil 118 }) 119 }(node) 120 } 121 122 if err := eg.Wait(); err != nil { 123 return err 124 } 125 close(ch) 126 <-printed 127 128 tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 129 fmt.Fprintf(tw, "Total:\t%s\n", units.HumanSize(float64(total))) 130 tw.Flush() 131 return nil 132 } 133 134 func pruneCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command { 135 options := pruneOptions{filter: opts.NewFilterOpt()} 136 137 cmd := &cobra.Command{ 138 Use: "prune", 139 Short: "Remove build cache", 140 Args: cli.NoArgs, 141 RunE: func(cmd *cobra.Command, args []string) error { 142 options.builder = rootOpts.builder 143 return runPrune(cmd.Context(), dockerCli, options) 144 }, 145 ValidArgsFunction: completion.Disable, 146 } 147 148 flags := cmd.Flags() 149 flags.BoolVarP(&options.all, "all", "a", false, "Include internal/frontend images") 150 flags.Var(&options.filter, "filter", `Provide filter values (e.g., "until=24h")`) 151 flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache") 152 flags.BoolVar(&options.verbose, "verbose", false, "Provide a more verbose output") 153 flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation") 154 155 return cmd 156 } 157 158 func toBuildkitPruneInfo(f filters.Args) (*client.PruneInfo, error) { 159 var until time.Duration 160 untilValues := f.Get("until") // canonical 161 unusedForValues := f.Get("unused-for") // deprecated synonym for "until" filter 162 163 if len(untilValues) > 0 && len(unusedForValues) > 0 { 164 return nil, errors.Errorf("conflicting filters %q and %q", "until", "unused-for") 165 } 166 untilKey := "until" 167 if len(unusedForValues) > 0 { 168 untilKey = "unused-for" 169 } 170 untilValues = append(untilValues, unusedForValues...) 171 172 switch len(untilValues) { 173 case 0: 174 // nothing to do 175 case 1: 176 var err error 177 until, err = time.ParseDuration(untilValues[0]) 178 if err != nil { 179 return nil, errors.Wrapf(err, "%q filter expects a duration (e.g., '24h')", untilKey) 180 } 181 default: 182 return nil, errors.Errorf("filters expect only one value") 183 } 184 185 filters := make([]string, 0, f.Len()) 186 for _, filterKey := range f.Keys() { 187 if filterKey == untilKey { 188 continue 189 } 190 191 values := f.Get(filterKey) 192 switch len(values) { 193 case 0: 194 filters = append(filters, filterKey) 195 case 1: 196 if filterKey == "id" { 197 filters = append(filters, filterKey+"~="+values[0]) 198 } else { 199 filters = append(filters, filterKey+"=="+values[0]) 200 } 201 default: 202 return nil, errors.Errorf("filters expect only one value") 203 } 204 } 205 return &client.PruneInfo{ 206 KeepDuration: until, 207 Filter: []string{strings.Join(filters, ",")}, 208 }, nil 209 }