diff --git a/bake/bake.go b/bake/bake.go index 9a507cd6..2f0c0fa6 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -18,7 +18,6 @@ import ( controllerapi "github.com/docker/buildx/controller/pb" "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/platformutil" - "github.com/docker/cli/cli/config" hcl "github.com/hashicorp/hcl/v2" "github.com/moby/buildkit/client/llb" @@ -248,7 +247,7 @@ func ParseFiles(files []File, defaults map[string]string) (_ *Config, err error) } if len(hclFiles) > 0 { - renamed, err := hclparser.Parse(hcl.MergeFiles(hclFiles), hclparser.Opt{ + renamed, err := hclparser.Parse(hclparser.MergeFiles(hclFiles), hclparser.Opt{ LookupVar: os.LookupEnv, Vars: defaults, ValidateLabel: validateTargetName, diff --git a/bake/hclparser/merged.go b/bake/hclparser/merged.go new file mode 100644 index 00000000..004b0a63 --- /dev/null +++ b/bake/hclparser/merged.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Forked from https://github.com/hashicorp/hcl/blob/7208bce57fadb72db3a328ebc9aa86489cd06fce/merged.go + +package hclparser + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" +) + +// MergeFiles combines the given files to produce a single body that contains +// configuration from all of the given files. +// +// The ordering of the given files decides the order in which contained +// elements will be returned. If any top-level attributes are defined with +// the same name across multiple files, a diagnostic will be produced from +// the Content and PartialContent methods describing this error in a +// user-friendly way. +func MergeFiles(files []*hcl.File) hcl.Body { + var bodies []hcl.Body + for _, file := range files { + bodies = append(bodies, file.Body) + } + return MergeBodies(bodies) +} + +// MergeBodies is like MergeFiles except it deals directly with bodies, rather +// than with entire files. +func MergeBodies(bodies []hcl.Body) hcl.Body { + if len(bodies) == 0 { + // Swap out for our singleton empty body, to reduce the number of + // empty slices we have hanging around. + return emptyBody + } + + // If any of the given bodies are already merged bodies, we'll unpack + // to flatten to a single mergedBodies, since that's conceptually simpler. + // This also, as a side-effect, eliminates any empty bodies, since + // empties are merged bodies with no inner bodies. + var newLen int + var flatten bool + for _, body := range bodies { + if children, merged := body.(mergedBodies); merged { + newLen += len(children) + flatten = true + } else { + newLen++ + } + } + + if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside + return mergedBodies(bodies) + } + + if newLen == 0 { + // Don't allocate a new empty when we already have one + return emptyBody + } + + n := make([]hcl.Body, 0, newLen) + for _, body := range bodies { + if children, merged := body.(mergedBodies); merged { + n = append(n, children...) + } else { + n = append(n, body) + } + } + return mergedBodies(n) +} + +var emptyBody = mergedBodies([]hcl.Body{}) + +// EmptyBody returns a body with no content. This body can be used as a +// placeholder when a body is required but no body content is available. +func EmptyBody() hcl.Body { + return emptyBody +} + +type mergedBodies []hcl.Body + +// Content returns the content produced by applying the given schema to all +// of the merged bodies and merging the result. +// +// Although required attributes _are_ supported, they should be used sparingly +// with merged bodies since in this case there is no contextual information +// with which to return good diagnostics. Applications working with merged +// bodies may wish to mark all attributes as optional and then check for +// required attributes afterwards, to produce better diagnostics. +func (mb mergedBodies) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { + // the returned body will always be empty in this case, because mergedContent + // will only ever call Content on the child bodies. + content, _, diags := mb.mergedContent(schema, false) + return content, diags +} + +func (mb mergedBodies) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + return mb.mergedContent(schema, true) +} + +func (mb mergedBodies) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { + attrs := make(map[string]*hcl.Attribute) + var diags hcl.Diagnostics + + for _, body := range mb { + thisAttrs, thisDiags := body.JustAttributes() + + if len(thisDiags) != 0 { + diags = append(diags, thisDiags...) + } + + if thisAttrs != nil { + for name, attr := range thisAttrs { + if existing := attrs[name]; existing != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate argument", + Detail: fmt.Sprintf( + "Argument %q was already set at %s", + name, existing.NameRange.String(), + ), + Subject: &attr.NameRange, + }) + continue + } + + attrs[name] = attr + } + } + } + + return attrs, diags +} + +func (mb mergedBodies) MissingItemRange() hcl.Range { + if len(mb) == 0 { + // Nothing useful to return here, so we'll return some garbage. + return hcl.Range{ + Filename: "", + } + } + + // arbitrarily use the first body's missing item range + return mb[0].MissingItemRange() +} + +func (mb mergedBodies) mergedContent(schema *hcl.BodySchema, partial bool) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + // We need to produce a new schema with none of the attributes marked as + // required, since _any one_ of our bodies can contribute an attribute value. + // We'll separately check that all required attributes are present at + // the end. + mergedSchema := &hcl.BodySchema{ + Blocks: schema.Blocks, + } + for _, attrS := range schema.Attributes { + mergedAttrS := attrS + mergedAttrS.Required = false + mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS) + } + + var mergedLeftovers []hcl.Body + content := &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + } + + var diags hcl.Diagnostics + for _, body := range mb { + var thisContent *hcl.BodyContent + var thisLeftovers hcl.Body + var thisDiags hcl.Diagnostics + + if partial { + thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema) + } else { + thisContent, thisDiags = body.Content(mergedSchema) + } + + if thisLeftovers != nil { + mergedLeftovers = append(mergedLeftovers, thisLeftovers) + } + if len(thisDiags) != 0 { + diags = append(diags, thisDiags...) + } + + if thisContent.Attributes != nil { + for name, attr := range thisContent.Attributes { + if existing := content.Attributes[name]; existing != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate argument", + Detail: fmt.Sprintf( + "Argument %q was already set at %s", + name, existing.NameRange.String(), + ), + Subject: &attr.NameRange, + }) + continue + } + content.Attributes[name] = attr + } + } + + if len(thisContent.Blocks) != 0 { + content.Blocks = append(content.Blocks, thisContent.Blocks...) + } + } + + // Finally, we check for required attributes. + for _, attrS := range schema.Attributes { + if !attrS.Required { + continue + } + + if content.Attributes[attrS.Name] == nil { + // We don't have any context here to produce a good diagnostic, + // which is why we warn in the Content docstring to minimize the + // use of required attributes on merged bodies. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required argument", + Detail: fmt.Sprintf( + "The argument %q is required, but was not set.", + attrS.Name, + ), + }) + } + } + + leftoverBody := MergeBodies(mergedLeftovers) + return content, leftoverBody, diags +} diff --git a/bake/hclparser/merged_test.go b/bake/hclparser/merged_test.go new file mode 100644 index 00000000..614687a7 --- /dev/null +++ b/bake/hclparser/merged_test.go @@ -0,0 +1,690 @@ +package hclparser + +// Forked from https://github.com/hashicorp/hcl/blob/v2.8.2/merged_test.go + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import ( + "fmt" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/hcl/v2" +) + +func TestMergedBodiesContent(t *testing.T) { + tests := []struct { + Bodies []hcl.Body + Schema *hcl.BodySchema + Want *hcl.BodyContent + DiagCount int + }{ + { + []hcl.Body{}, + &hcl.BodySchema{}, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + { + []hcl.Body{}, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + { + []hcl.Body{}, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + Required: true, + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 1, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + HasAttributes: []string{"name"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"name"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + }, + }, + 1, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"age"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + { + Name: "age", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + "age": { + Name: "age", + NameRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{}, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + HasBlocks: map[string]int{ + "pizza": 2, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + }, + { + Type: "pizza", + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + &testMergedBodiesVictim{ + Name: "second", + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + }, + &testMergedBodiesVictim{ + Name: "second", + HasBlocks: map[string]int{ + "pizza": 2, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasBlocks: map[string]int{ + "pizza": 2, + }, + }, + &testMergedBodiesVictim{ + Name: "second", + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + }, + &testMergedBodiesVictim{ + Name: "second", + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + 0, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + merged := MergeBodies(test.Bodies) + got, diags := merged.Content(test.Schema) + + if len(diags) != test.DiagCount { + t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) + for _, diag := range diags { + t.Logf(" - %s", diag) + } + } + + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.Want)) + } + }) + } +} + +func TestMergeBodiesPartialContent(t *testing.T) { + tests := []struct { + Bodies []hcl.Body + Schema *hcl.BodySchema + WantContent *hcl.BodyContent + WantRemain hcl.Body + DiagCount int + }{ + { + []hcl.Body{}, + &hcl.BodySchema{}, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + }, + mergedBodies{}, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name", "age"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"age"}, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name", "age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"name", "pizza"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"pizza"}, + }, + }, + 1, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"name", "age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"pizza", "soda"}, + }, + }, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + }, + { + Name: "soda", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{ + "name": { + Name: "name", + NameRange: hcl.Range{Filename: "first"}, + }, + "soda": { + Name: "soda", + NameRange: hcl.Range{Filename: "second"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{"age"}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{"pizza"}, + }, + }, + 0, + }, + { + []hcl.Body{ + &testMergedBodiesVictim{ + Name: "first", + HasBlocks: map[string]int{ + "pizza": 1, + }, + }, + &testMergedBodiesVictim{ + Name: "second", + HasBlocks: map[string]int{ + "pizza": 1, + "soda": 2, + }, + }, + }, + &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "pizza", + }, + }, + }, + &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + Blocks: hcl.Blocks{ + { + Type: "pizza", + DefRange: hcl.Range{Filename: "first"}, + }, + { + Type: "pizza", + DefRange: hcl.Range{Filename: "second"}, + }, + }, + }, + mergedBodies{ + &testMergedBodiesVictim{ + Name: "first", + HasAttributes: []string{}, + HasBlocks: map[string]int{}, + }, + &testMergedBodiesVictim{ + Name: "second", + HasAttributes: []string{}, + HasBlocks: map[string]int{ + "soda": 2, + }, + }, + }, + 0, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { + merged := MergeBodies(test.Bodies) + got, gotRemain, diags := merged.PartialContent(test.Schema) + + if len(diags) != test.DiagCount { + t.Errorf("Wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) + for _, diag := range diags { + t.Logf(" - %s", diag) + } + } + + if !reflect.DeepEqual(got, test.WantContent) { + t.Errorf("wrong content result\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(test.WantContent)) + } + + if !reflect.DeepEqual(gotRemain, test.WantRemain) { + t.Errorf("wrong remaining result\ngot: %s\nwant: %s", spew.Sdump(gotRemain), spew.Sdump(test.WantRemain)) + } + }) + } +} + +type testMergedBodiesVictim struct { + Name string + HasAttributes []string + HasBlocks map[string]int + DiagCount int +} + +func (v *testMergedBodiesVictim) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { + c, _, d := v.PartialContent(schema) + return c, d +} + +func (v *testMergedBodiesVictim) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + remain := &testMergedBodiesVictim{ + Name: v.Name, + HasAttributes: []string{}, + } + + hasAttrs := map[string]struct{}{} + for _, n := range v.HasAttributes { + hasAttrs[n] = struct{}{} + + var found bool + for _, attrS := range schema.Attributes { + if n == attrS.Name { + found = true + break + } + } + if !found { + remain.HasAttributes = append(remain.HasAttributes, n) + } + } + + content := &hcl.BodyContent{ + Attributes: map[string]*hcl.Attribute{}, + } + + rng := hcl.Range{ + Filename: v.Name, + } + + for _, attrS := range schema.Attributes { + _, has := hasAttrs[attrS.Name] + if has { + content.Attributes[attrS.Name] = &hcl.Attribute{ + Name: attrS.Name, + NameRange: rng, + } + } + } + + if v.HasBlocks != nil { + for _, blockS := range schema.Blocks { + num := v.HasBlocks[blockS.Type] + for i := 0; i < num; i++ { + content.Blocks = append(content.Blocks, &hcl.Block{ + Type: blockS.Type, + DefRange: rng, + }) + } + } + + remain.HasBlocks = map[string]int{} + for n := range v.HasBlocks { + var found bool + for _, blockS := range schema.Blocks { + if blockS.Type == n { + found = true + break + } + } + if !found { + remain.HasBlocks[n] = v.HasBlocks[n] + } + } + } + + diags := make(hcl.Diagnostics, v.DiagCount) + for i := range diags { + diags[i] = &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Fake diagnostic %d", i), + Detail: "For testing only.", + Context: &rng, + } + } + + return content, remain, diags +} + +func (v *testMergedBodiesVictim) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { + attrs := make(map[string]*hcl.Attribute) + + rng := hcl.Range{ + Filename: v.Name, + } + + for _, name := range v.HasAttributes { + attrs[name] = &hcl.Attribute{ + Name: name, + NameRange: rng, + } + } + + diags := make(hcl.Diagnostics, v.DiagCount) + for i := range diags { + diags[i] = &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Fake diagnostic %d", i), + Detail: "For testing only.", + Context: &rng, + } + } + + return attrs, diags +} + +func (v *testMergedBodiesVictim) MissingItemRange() hcl.Range { + return hcl.Range{ + Filename: v.Name, + } +} diff --git a/go.mod b/go.mod index d2e370d0..1b908cff 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/containerd/containerd v1.7.1 github.com/containerd/continuity v0.4.1 github.com/containerd/typeurl/v2 v2.1.1 + github.com/davecgh/go-spew v1.1.1 github.com/docker/cli v24.0.1+incompatible github.com/docker/cli-docs-tool v0.5.1 github.com/docker/distribution v2.8.2+incompatible @@ -77,7 +78,6 @@ require ( github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e // indirect github.com/containerd/ttrpc v1.2.2 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/distribution/v3 v3.0.0-20230214150026-36d8c594d7aa // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect