Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bake: merge targets and vars from multiple JSON files #1025

Merged
merged 2 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions bake/hcl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,128 @@ func TestHCLBuiltinVars(t *testing.T) {
require.Equal(t, "foo", *c.Targets[0].Context)
require.Equal(t, "test", *c.Targets[0].Dockerfile)
}

func TestCombineHCLAndJSONTargets(t *testing.T) {
c, err := ParseFiles([]File{
{
Name: "docker-bake.hcl",
Data: []byte(`
group "default" {
targets = ["a"]
}

target "metadata-a" {}
target "metadata-b" {}

target "a" {
inherits = ["metadata-a"]
context = "."
target = "a"
}

target "b" {
inherits = ["metadata-b"]
context = "."
target = "b"
}`),
},
{
Name: "metadata-a.json",
Data: []byte(`
{
"target": [{
"metadata-a": [{
"tags": [
"app/a:1.0.0",
"app/a:latest"
]
}]
}]
}`),
},
{
Name: "metadata-b.json",
Data: []byte(`
{
"target": [{
"metadata-b": [{
"tags": [
"app/b:1.0.0",
"app/b:latest"
]
}]
}]
}`),
},
}, nil)
require.NoError(t, err)

require.Equal(t, 1, len(c.Groups))
require.Equal(t, "default", c.Groups[0].Name)
require.Equal(t, []string{"a"}, c.Groups[0].Targets)

require.Equal(t, 4, len(c.Targets))

require.Equal(t, c.Targets[0].Name, "metadata-a")
require.Equal(t, []string{"app/a:1.0.0", "app/a:latest"}, c.Targets[0].Tags)

require.Equal(t, c.Targets[1].Name, "metadata-b")
require.Equal(t, []string{"app/b:1.0.0", "app/b:latest"}, c.Targets[1].Tags)

require.Equal(t, c.Targets[2].Name, "a")
require.Equal(t, ".", *c.Targets[2].Context)
require.Equal(t, "a", *c.Targets[2].Target)

require.Equal(t, c.Targets[3].Name, "b")
require.Equal(t, ".", *c.Targets[3].Context)
require.Equal(t, "b", *c.Targets[3].Target)
}

func TestCombineHCLAndJSONVars(t *testing.T) {
c, err := ParseFiles([]File{
{
Name: "docker-bake.hcl",
Data: []byte(`
variable "ABC" {
default = "foo"
}
variable "DEF" {
default = ""
}
group "default" {
targets = ["one"]
}
target "one" {
args = {
a = "pre-${ABC}"
}
}
target "two" {
args = {
b = "pre-${DEF}"
}
}`),
},
{
Name: "foo.json",
Data: []byte(`{"variable": {"DEF": {"default": "bar"}}, "target": { "one": { "args": {"a": "pre-${ABC}-${DEF}"}} } }`),
},
{
Name: "bar.json",
Data: []byte(`{"ABC": "ghi", "DEF": "jkl"}`),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it still work when hcl also defines ABC = "old" and foo.json "ABC": "old2"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed I tried with attributes in global scope an doesn't seem to work atm (see last commit)

=== RUN   TestCombineHCLAndJSONAttrs
    hcl_test.go:790: 
        	Error Trace:	hcl_test.go:790
        	Error:      	Not equal: 
        	            	expected: map[string]string{"a":"pre-ghi-jkl"}
        	            	actual  : map[string]string{"a":"pre-foo-jkl"}

I guess it's how merging blocks are handled for attributes in global scope where only the first one is retained atm:

ABC = "foo"
variable "DEF" {
  default = ""
}
group "default" {
  targets = ["one"]
}
target "one" {
  args = {
    a = "pre-${ABC}"
  }
}
target "two" {
  args = {
    b = "pre-${DEF}"
  }
}
{"ABC": "oof", "variable": {"DEF": {"default": "bar"}}, "target": { "one": { "args": {"a": "pre-${ABC}-${DEF}"}} } }
{"ABC": "ghi", "DEF": "jkl"}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it at least work with HCL-only?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with HCL only (see test in last commit) 😞:

=== RUN   TestCombineHCLAttrs
    hcl_test.go:801: 
        	Error Trace:	hcl_test.go:801
        	Error:      	Not equal: 
        	            	expected: map[string]string{"a":"pre-ghi-jkl"}
        	            	actual  : map[string]string{"a":"pre-foo-jkl"}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we missed a case in #541. Changing this test with a global attr FOO = "abc" in first file makes it failed:

func TestHCLMultiFileAttrs(t *testing.T) {
	os.Unsetenv("FOO")
	dt := []byte(`
		FOO = "abc"
		target "app" {
			args = {
				v1 = "pre-${FOO}"
			}
		}
		`)
	dt2 := []byte(`
		FOO="def"
		`)

	c, err := ParseFiles([]File{
		{Data: dt, Name: "c1.hcl"},
		{Data: dt2, Name: "c2.hcl"},
	}, nil)
	require.NoError(t, err)
	require.Equal(t, 1, len(c.Targets))
	require.Equal(t, c.Targets[0].Name, "app")
	require.Equal(t, "pre-def", c.Targets[0].Args["v1"])

	os.Setenv("FOO", "ghi")

	c, err = ParseFiles([]File{
		{Data: dt, Name: "c1.hcl"},
		{Data: dt2, Name: "c2.hcl"},
	}, nil)
	require.NoError(t, err)

	require.Equal(t, 1, len(c.Targets))
	require.Equal(t, c.Targets[0].Name, "app")
	require.Equal(t, "pre-ghi", c.Targets[0].Args["v1"])
}
=== RUN   TestHCLMultiFileAttrs
    hcl_test.go:488: 
        	Error Trace:	hcl_test.go:488
        	Error:      	Not equal: 
        	            	expected: "pre-def"
        	            	actual  : "pre-abc"

Copy link
Member Author

@crazy-max crazy-max Apr 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah JustAttributes skips repeated attributes: https://github.com/hashicorp/hcl/blob/c7ee8b78101c33b4dfed2641d78cf5e9651eabb8/merged.go#L109-L120

We might need to bring in this func in the hclparser to fix that for us wdyt?

},
}, nil)
require.NoError(t, err)

require.Equal(t, 1, len(c.Groups))
require.Equal(t, "default", c.Groups[0].Name)
require.Equal(t, []string{"one"}, c.Groups[0].Targets)

require.Equal(t, 2, len(c.Targets))

require.Equal(t, c.Targets[0].Name, "one")
require.Equal(t, map[string]string{"a": "pre-ghi-jkl"}, c.Targets[0].Args)

require.Equal(t, c.Targets[1].Name, "two")
require.Equal(t, map[string]string{"b": "pre-jkl"}, c.Targets[1].Args)
}
36 changes: 32 additions & 4 deletions bake/hclparser/hclparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,8 @@ func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics {

attrs, diags := b.JustAttributes()
if diags.HasErrors() {
for _, d := range diags {
if d.Detail != "Blocks are not allowed here." {
return diags
}
if d := removeAttributesDiags(diags, reserved, p.vars); len(d) > 0 {
return d
}
}

Expand Down Expand Up @@ -514,3 +512,33 @@ func setLabel(v reflect.Value, lbl string) int {
}
return -1
}

func removeAttributesDiags(diags hcl.Diagnostics, reserved map[string]struct{}, vars map[string]*variable) hcl.Diagnostics {
var fdiags hcl.Diagnostics
for _, d := range diags {
if fout := func(d *hcl.Diagnostic) bool {
// https://github.com/docker/buildx/pull/541
if d.Detail == "Blocks are not allowed here." {
return true
}
for r := range reserved {
// JSON body objects don't handle repeated blocks like HCL but
// reserved name attributes should be allowed when multi bodies are merged.
// https://github.com/hashicorp/hcl/blob/main/json/spec.md#blocks
if strings.HasPrefix(d.Detail, fmt.Sprintf(`Argument "%s" was already set at `, r)) {
crazy-max marked this conversation as resolved.
Show resolved Hide resolved
return true
}
}
for v := range vars {
// Do the same for global variables
if strings.HasPrefix(d.Detail, fmt.Sprintf(`Argument "%s" was already set at `, v)) {
return true
}
}
return false
}(d); !fout {
fdiags = append(fdiags, d)
}
}
return fdiags
}