diff --git a/bake/bake.go b/bake/bake.go index 0563ca1c..9dfde537 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -210,6 +210,7 @@ type Target struct { Labels map[string]string `json:"labels,omitempty"` Tags []string `json:"tags,omitempty"` CacheFrom []string `json:"cache-from,omitempty"` + CacheTo []string `json:"cache-to,omitempty"` Target *string `json:"target,omitempty"` Secrets []string `json:"secret,omitempty"` SSH []string `json:"ssh,omitempty"` @@ -251,7 +252,6 @@ func toBuildOpt(t Target) (*build.Options, error) { Tags: t.Tags, BuildArgs: t.Args, Labels: t.Labels, - // CacheFrom: t.CacheFrom, } platforms, err := build.ParsePlatformSpecs(t.Platforms) @@ -278,6 +278,18 @@ func toBuildOpt(t Target) (*build.Options, error) { bo.Target = *t.Target } + cacheImports, err := build.ParseCacheEntry(t.CacheFrom) + if err != nil { + return nil, err + } + bo.CacheFrom = cacheImports + + cacheExports, err := build.ParseCacheEntry(t.CacheTo) + if err != nil { + return nil, err + } + bo.CacheTo = cacheExports + return bo, nil } @@ -325,6 +337,12 @@ func merge(t1, t2 Target) Target { if t2.Platforms != nil { // no merge t1.Platforms = t2.Platforms } + if len(t2.CacheFrom) > 0 { // no merge + t1.CacheFrom = t2.CacheFrom + } + if len(t2.CacheTo) > 0 { // no merge + t1.CacheTo = t2.CacheTo + } t1.Inherits = append(t1.Inherits, t2.Inherits...) return t1 } diff --git a/build/build.go b/build/build.go index 8c6d743f..7eb4093f 100644 --- a/build/build.go +++ b/build/build.go @@ -47,6 +47,9 @@ type Options struct { Exports []client.ExportEntry Session []session.Attachable + CacheFrom []client.CacheOptionsEntry + CacheTo []client.CacheOptionsEntry + // DockerTarget } @@ -133,10 +136,27 @@ func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, do } } + if v, ok := opt.BuildArgs["BUILDKIT_INLINE_CACHE"]; ok { + if v, _ := strconv.ParseBool(v); v { + opt.CacheTo = append(opt.CacheTo, client.CacheOptionsEntry{ + Type: "inline", + Attrs: map[string]string{}, + }) + } + } + + for _, e := range opt.CacheTo { + if e.Type != "inline" && !d.Features()[driver.CacheExport] { + return nil, notSupported(d, driver.CacheExport) + } + } + so := client.SolveOpt{ Frontend: "dockerfile.v0", FrontendAttrs: map[string]string{}, LocalDirs: map[string]string{}, + CacheExports: opt.CacheTo, + CacheImports: opt.CacheFrom, } switch len(opt.Exports) { diff --git a/build/cache.go b/build/cache.go new file mode 100644 index 00000000..6d9b7f04 --- /dev/null +++ b/build/cache.go @@ -0,0 +1,60 @@ +package build + +import ( + "encoding/csv" + "strings" + + "github.com/moby/buildkit/client" + "github.com/pkg/errors" +) + +func ParseCacheEntry(in []string) ([]client.CacheOptionsEntry, error) { + imports := make([]client.CacheOptionsEntry, 0, len(in)) + for _, in := range in { + csvReader := csv.NewReader(strings.NewReader(in)) + fields, err := csvReader.Read() + if err != nil { + return nil, err + } + if isRefOnlyFormat(fields) { + for _, field := range fields { + imports = append(imports, client.CacheOptionsEntry{ + Type: "registry", + Attrs: map[string]string{"ref": field}, + }) + } + continue + } + im := client.CacheOptionsEntry{ + Attrs: map[string]string{}, + } + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + if len(parts) != 2 { + return nil, errors.Errorf("invalid value %s", field) + } + key := strings.ToLower(parts[0]) + value := parts[1] + switch key { + case "type": + im.Type = value + default: + im.Attrs[key] = value + } + } + if im.Type == "" { + return nil, errors.Errorf("type required form> %q", in) + } + imports = append(imports, im) + } + return imports, nil +} + +func isRefOnlyFormat(in []string) bool { + for _, v := range in { + if strings.Contains(v, "=") { + return false + } + } + return true +} diff --git a/commands/build.go b/commands/build.go index 2193147d..4ea318f0 100644 --- a/commands/build.go +++ b/commands/build.go @@ -26,6 +26,7 @@ type buildOptions struct { buildArgs []string cacheFrom []string + cacheTo []string target string platforms []string secrets []string @@ -153,6 +154,18 @@ func runBuild(dockerCli command.Cli, in buildOptions) error { opts.Exports = outputs + cacheImports, err := build.ParseCacheEntry(in.cacheFrom) + if err != nil { + return err + } + opts.CacheFrom = cacheImports + + cacheExports, err := build.ParseCacheEntry(in.cacheTo) + if err != nil { + return err + } + opts.CacheTo = cacheExports + return buildTargets(ctx, dockerCli, map[string]build.Options{"default": opts}, in.progress) } @@ -195,7 +208,8 @@ func buildCmd(dockerCli command.Cli) *cobra.Command { flags.StringArrayVar(&options.labels, "label", []string{}, "Set metadata for an image") - flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") + flags.StringArrayVar(&options.cacheFrom, "cache-from", []string{}, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)") + flags.StringArrayVar(&options.cacheTo, "cache-to", []string{}, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)") flags.StringVar(&options.target, "target", "", "Set the target build stage to build.")