diff --git a/bake/bake.go b/bake/bake.go index 2c2c55f8ec28..d05938fe00b5 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -317,6 +317,8 @@ func (c Config) newOverrides(v []string) (map[string]*Target, error) { return nil, errors.Errorf("invalid value %s for boolean key pull", parts[1]) } t.Pull = &pull + case "metadata-file": + continue default: return nil, errors.Errorf("unknown key: %s", keys[1]) } @@ -434,6 +436,7 @@ type Target struct { Outputs []string `json:"output,omitempty" hcl:"output,optional"` Pull *bool `json:"pull,omitempty" hcl:"pull,optional"` NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional"` + MetadataFile *string `json:"-" hcl:"-"` // IMPORTANT: if you add more fields here, do not forget to update newOverrides and README. } @@ -448,13 +451,16 @@ func (t *Target) normalize() { t.Outputs = removeDupes(t.Outputs) } -func TargetsToBuildOpt(m map[string]*Target, inp *Input) (map[string]build.Options, error) { +func TargetsToBuildOpt(m map[string]*Target, inp *Input, metadataFile string) (map[string]build.Options, error) { m2 := make(map[string]build.Options, len(m)) for k, v := range m { - bo, err := toBuildOpt(v, inp) + bo, err := toBuildOpt(k, v, inp) if err != nil { return nil, err } + if len(metadataFile) > 0 { + bo.MetadataFile = metadataFile + } m2[k] = *bo } return m2, nil @@ -472,7 +478,7 @@ func updateContext(t *build.Inputs, inp *Input) { t.ContextState = &st } -func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { +func toBuildOpt(n string, t *Target, inp *Input) (*build.Options, error) { if v := t.Context; v != nil && *v == "-" { return nil, errors.Errorf("context from stdin not allowed in bake") } @@ -502,6 +508,10 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { if t.Pull != nil { pull = *t.Pull } + metadataFile := "" + if t.MetadataFile != nil { + metadataFile = *t.MetadataFile + } bi := build.Inputs{ ContextPath: contextPath, @@ -513,12 +523,14 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { updateContext(&bi, inp) bo := &build.Options{ - Inputs: bi, - Tags: t.Tags, - BuildArgs: t.Args, - Labels: t.Labels, - NoCache: noCache, - Pull: pull, + Inputs: bi, + Tags: t.Tags, + BuildArgs: t.Args, + Labels: t.Labels, + NoCache: noCache, + Pull: pull, + MetadataFile: metadataFile, + BakeDefinition: t, } platforms, err := platformutil.Parse(t.Platforms) diff --git a/build/build.go b/build/build.go index ba7b77801884..131ae668572e 100644 --- a/build/build.go +++ b/build/build.go @@ -46,14 +46,15 @@ var ( ) type Options struct { - Inputs Inputs - Tags []string - Labels map[string]string - BuildArgs map[string]string - Pull bool - ImageIDFile string - ExtraHosts []string - NetworkMode string + Inputs Inputs + Tags []string + Labels map[string]string + BuildArgs map[string]string + Pull bool + ImageIDFile string + MetadataFile string + ExtraHosts []string + NetworkMode string NoCache bool Target string @@ -65,7 +66,8 @@ type Options struct { CacheTo []client.CacheOptionsEntry Allow []entitlements.Entitlement - // DockerTarget + + BakeDefinition interface{} } type Inputs struct { @@ -342,6 +344,12 @@ func toSolveOpt(ctx context.Context, d driver.Driver, multiDriver bool, opt Opti return nil, nil, errors.Wrap(err, "removing image ID file") } } + if opt.MetadataFile != "" { + // Avoid leaving a stale file if we eventually fail + if err := os.Remove(opt.MetadataFile); err != nil && !os.IsNotExist(err) { + return nil, nil, errors.Wrap(err, "removing metadata file") + } + } // inline cache from build arg if v, ok := opt.BuildArgs["BUILDKIT_INLINE_CACHE"]; ok { @@ -656,6 +664,29 @@ func Build(ctx context.Context, drivers []DriverInfo, opt map[string]Options, do respMu.Lock() resp[k] = res[0] respMu.Unlock() + + if opt.MetadataFile != "" { + metadata := make([]map[string]string, 0, len(res)) + for _, r := range res { + if _, ok := r.ExporterResponse["containerimage.digest"]; ok { + metadata = append(metadata, r.ExporterResponse) + } + } + mdatab, err := json.MarshalIndent(map[string]interface{}{k: struct { + Definition interface{} `json:"definition,omitempty"` + Metadata []map[string]string `json:"metadata,omitempty"` + }{ + Definition: opt.BakeDefinition, + Metadata: metadata, + }}, "", " ") + if err != nil { + return err + } + if err := ioutil.WriteFile(opt.MetadataFile, mdatab, 0644); err != nil { + return err + } + } + if len(res) == 1 { if opt.ImageIDFile != "" { return ioutil.WriteFile(opt.ImageIDFile, []byte(res[0].ExporterResponse["containerimage.digest"]), 0644) diff --git a/commands/bake.go b/commands/bake.go index 0d37b100f93a..764417484fc8 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -53,6 +53,9 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions) (err error if in.pull != nil { overrides = append(overrides, fmt.Sprintf("*.pull=%t", *in.pull)) } + if len(in.metadataFile) > 0 { + overrides = append(overrides, fmt.Sprintf("*.metadata-file=%s", in.metadataFile)) + } contextPathHash, _ := os.Getwd() ctx2, cancel := context.WithCancel(context.TODO()) @@ -103,7 +106,7 @@ func runBake(dockerCli command.Cli, targets []string, in bakeOptions) (err error return nil } - bo, err := bake.TargetsToBuildOpt(m, inp) + bo, err := bake.TargetsToBuildOpt(m, inp, in.metadataFile) if err != nil { return err } diff --git a/commands/build.go b/commands/build.go index 324c4e3ddd05..b2a540111cd0 100644 --- a/commands/build.go +++ b/commands/build.go @@ -64,10 +64,11 @@ type buildOptions struct { } type commonOptions struct { - builder string - noCache *bool - progress string - pull *bool + builder string + noCache *bool + progress string + pull *bool + metadataFile string // golangci-lint#826 // nolint:structcheck exportPush bool @@ -100,15 +101,16 @@ func runBuild(dockerCli command.Cli, in buildOptions) error { DockerfilePath: in.dockerfileName, InStream: os.Stdin, }, - Tags: in.tags, - Labels: listToMap(in.labels, false), - BuildArgs: listToMap(in.buildArgs, true), - Pull: pull, - NoCache: noCache, - Target: in.target, - ImageIDFile: in.imageIDFile, - ExtraHosts: in.extraHosts, - NetworkMode: in.networkMode, + Tags: in.tags, + Labels: listToMap(in.labels, false), + BuildArgs: listToMap(in.buildArgs, true), + Pull: pull, + NoCache: noCache, + Target: in.target, + ImageIDFile: in.imageIDFile, + MetadataFile: in.metadataFile, + ExtraHosts: in.extraHosts, + NetworkMode: in.networkMode, } platforms, err := platformutil.Parse(in.platforms) @@ -333,6 +335,7 @@ func commonBuildFlags(options *commonOptions, flags *pflag.FlagSet) { flags.StringVar(&options.progress, "progress", defaultProgress, "Set type of progress output (auto, plain, tty). Use plain to show container output") options.pull = flags.Bool("pull", false, "Always attempt to pull a newer version of the image") + flags.StringVar(&options.metadataFile, "metadata-file", "", "Write build metadata to a file as JSON") } func listToMap(values []string, defaultEnv bool) map[string]string { diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md index 55821153b705..03e9ee838d1e 100644 --- a/docs/reference/buildx_bake.md +++ b/docs/reference/buildx_bake.md @@ -18,6 +18,7 @@ Build from a file | `--builder string` | Override the configured builder instance | | [`-f`](#file), [`--file stringArray`](#file) | Build definition file | | `--load` | Shorthand for --set=*.output=type=docker | +| `--metadata-file string` | Write build metadata to a file as JSON | | [`--no-cache`](#no-cache) | Do not use cache when building the image | | [`--print`](#print) | Print the options without building | | [`--progress string`](#progress) | Set type of progress output (auto, plain, tty). Use plain to show container output | diff --git a/docs/reference/buildx_build.md b/docs/reference/buildx_build.md index d7711085d953..ec2ce236dfff 100644 --- a/docs/reference/buildx_build.md +++ b/docs/reference/buildx_build.md @@ -25,6 +25,7 @@ Start a build | `--iidfile string` | Write the image ID to the file | | `--label stringArray` | Set metadata for an image | | [`--load`](#load) | Shorthand for --output=type=docker | +| `--metadata-file string` | Write build metadata to a file as JSON | | `--network string` | Set the networking mode for the RUN instructions during build | | `--no-cache` | Do not use cache when building the image | | [`-o`](#output), [`--output stringArray`](#output) | Output destination (format: type=local,dest=path) |