A variable with a type but no default value or override resulted in an empty string. This matches the legacy behavior of untyped variables, but does not make sense when using types (an empty string is itself a type violation for everything except `string`). All variables defined with a type but with no value are now a typed `null`. A variable explicitly typed `any` was previously treated as if the typing was omitted; with no defined value or override, that resulted in an empty string. The `any` type is now distinguished from an omitted type; these variables, with no default or override, are also `null`. In other respects, the behavior of `any` is unchanged and largely behaves as if the type was omitted. It's not clear whether it should be supported, let alone how it should behave, so these tests were removed. It's being treated as undefined behavior. Signed-off-by: Roberto Villarreal <rrjjvv@yahoo.com>
2378 lines
54 KiB
Go
2378 lines
54 KiB
Go
package bake
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"testing"
|
|
|
|
hcl "github.com/hashicorp/hcl/v2"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHCLBasic(t *testing.T) {
|
|
t.Parallel()
|
|
dt := []byte(`
|
|
group "default" {
|
|
targets = ["db", "webapp"]
|
|
}
|
|
|
|
target "db" {
|
|
context = "./db"
|
|
tags = ["docker.io/tonistiigi/db"]
|
|
output = ["type=image"]
|
|
}
|
|
|
|
target "webapp" {
|
|
context = "./dir"
|
|
dockerfile = "Dockerfile-alternate"
|
|
args = {
|
|
buildno = "123"
|
|
}
|
|
output = [
|
|
{ type = "image" }
|
|
]
|
|
}
|
|
|
|
target "cross" {
|
|
platforms = [
|
|
"linux/amd64",
|
|
"linux/arm64"
|
|
]
|
|
}
|
|
|
|
target "webapp-plus" {
|
|
inherits = ["webapp", "cross"]
|
|
args = {
|
|
IAMCROSS = "true"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets)
|
|
|
|
require.Equal(t, 4, len(c.Targets))
|
|
require.Equal(t, "db", c.Targets[0].Name)
|
|
require.Equal(t, "./db", *c.Targets[0].Context)
|
|
|
|
require.Equal(t, "webapp", c.Targets[1].Name)
|
|
require.Equal(t, 1, len(c.Targets[1].Args))
|
|
require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"])
|
|
|
|
require.Equal(t, "cross", c.Targets[2].Name)
|
|
require.Equal(t, 2, len(c.Targets[2].Platforms))
|
|
require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[2].Platforms)
|
|
|
|
require.Equal(t, "webapp-plus", c.Targets[3].Name)
|
|
require.Equal(t, 1, len(c.Targets[3].Args))
|
|
require.Equal(t, map[string]*string{"IAMCROSS": ptrstr("true")}, c.Targets[3].Args)
|
|
}
|
|
|
|
func TestHCLBasicInJSON(t *testing.T) {
|
|
dt := []byte(`
|
|
{
|
|
"group": {
|
|
"default": {
|
|
"targets": ["db", "webapp"]
|
|
}
|
|
},
|
|
"target": {
|
|
"db": {
|
|
"context": "./db",
|
|
"tags": ["docker.io/tonistiigi/db"]
|
|
},
|
|
"webapp": {
|
|
"context": "./dir",
|
|
"dockerfile": "Dockerfile-alternate",
|
|
"args": {
|
|
"buildno": "123"
|
|
}
|
|
},
|
|
"cross": {
|
|
"platforms": [
|
|
"linux/amd64",
|
|
"linux/arm64"
|
|
]
|
|
},
|
|
"webapp-plus": {
|
|
"inherits": ["webapp", "cross"],
|
|
"args": {
|
|
"IAMCROSS": "true"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets)
|
|
|
|
require.Equal(t, 4, len(c.Targets))
|
|
require.Equal(t, "db", c.Targets[0].Name)
|
|
require.Equal(t, "./db", *c.Targets[0].Context)
|
|
|
|
require.Equal(t, "webapp", c.Targets[1].Name)
|
|
require.Equal(t, 1, len(c.Targets[1].Args))
|
|
require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"])
|
|
|
|
require.Equal(t, "cross", c.Targets[2].Name)
|
|
require.Equal(t, 2, len(c.Targets[2].Platforms))
|
|
require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[2].Platforms)
|
|
|
|
require.Equal(t, "webapp-plus", c.Targets[3].Name)
|
|
require.Equal(t, 1, len(c.Targets[3].Args))
|
|
require.Equal(t, map[string]*string{"IAMCROSS": ptrstr("true")}, c.Targets[3].Args)
|
|
}
|
|
|
|
func TestHCLWithFunctions(t *testing.T) {
|
|
dt := []byte(`
|
|
group "default" {
|
|
targets = ["webapp"]
|
|
}
|
|
|
|
target "webapp" {
|
|
args = {
|
|
buildno = "${add(123, 1)}"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "webapp", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("124"), c.Targets[0].Args["buildno"])
|
|
}
|
|
|
|
func TestHCLWithUserDefinedFunctions(t *testing.T) {
|
|
dt := []byte(`
|
|
function "increment" {
|
|
params = [number]
|
|
result = number + 1
|
|
}
|
|
|
|
group "default" {
|
|
targets = ["webapp"]
|
|
}
|
|
|
|
target "webapp" {
|
|
args = {
|
|
buildno = "${increment(123)}"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "webapp", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("124"), c.Targets[0].Args["buildno"])
|
|
}
|
|
|
|
func TestHCLWithVariables(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "BUILD_NUMBER" {
|
|
default = "123"
|
|
}
|
|
|
|
group "default" {
|
|
targets = ["webapp"]
|
|
}
|
|
|
|
target "webapp" {
|
|
args = {
|
|
buildno = "${BUILD_NUMBER}"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "webapp", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("123"), c.Targets[0].Args["buildno"])
|
|
|
|
t.Setenv("BUILD_NUMBER", "456")
|
|
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"webapp"}, c.Groups[0].Targets)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "webapp", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("456"), c.Targets[0].Args["buildno"])
|
|
}
|
|
|
|
func TestHCLWithVariablesInFunctions(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "REPO" {
|
|
default = "user/repo"
|
|
}
|
|
function "tag" {
|
|
params = [tag]
|
|
result = ["${REPO}:${tag}"]
|
|
}
|
|
|
|
target "webapp" {
|
|
tags = tag("v1")
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "webapp", c.Targets[0].Name)
|
|
require.Equal(t, []string{"user/repo:v1"}, c.Targets[0].Tags)
|
|
|
|
t.Setenv("REPO", "docker/buildx")
|
|
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "webapp", c.Targets[0].Name)
|
|
require.Equal(t, []string{"docker/buildx:v1"}, c.Targets[0].Tags)
|
|
}
|
|
|
|
func TestHCLMultiFileSharedVariables(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = "abc"
|
|
}
|
|
target "app" {
|
|
args = {
|
|
v1 = "pre-${FOO}"
|
|
}
|
|
}
|
|
`)
|
|
dt2 := []byte(`
|
|
target "app" {
|
|
args = {
|
|
v2 = "${FOO}-post"
|
|
}
|
|
}
|
|
`)
|
|
|
|
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, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre-abc"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("abc-post"), c.Targets[0].Args["v2"])
|
|
|
|
t.Setenv("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, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre-def"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("def-post"), c.Targets[0].Args["v2"])
|
|
}
|
|
|
|
func TestHCLVarsWithVars(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = upper("${BASE}def")
|
|
}
|
|
variable "BAR" {
|
|
default = "-${FOO}-"
|
|
}
|
|
target "app" {
|
|
args = {
|
|
v1 = "pre-${BAR}"
|
|
}
|
|
}
|
|
`)
|
|
dt2 := []byte(`
|
|
variable "BASE" {
|
|
default = "abc"
|
|
}
|
|
target "app" {
|
|
args = {
|
|
v2 = "${FOO}-post"
|
|
}
|
|
}
|
|
`)
|
|
|
|
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, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre--ABCDEF-"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("ABCDEF-post"), c.Targets[0].Args["v2"])
|
|
|
|
t.Setenv("BASE", "new")
|
|
|
|
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, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre--NEWDEF-"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("NEWDEF-post"), c.Targets[0].Args["v2"])
|
|
}
|
|
|
|
func TestHCLTypedVariables(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = 3
|
|
}
|
|
variable "IS_FOO" {
|
|
default = true
|
|
}
|
|
target "app" {
|
|
args = {
|
|
v1 = FOO > 5 ? "higher" : "lower"
|
|
v2 = IS_FOO ? "yes" : "no"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("lower"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("yes"), c.Targets[0].Args["v2"])
|
|
|
|
t.Setenv("FOO", "5.1")
|
|
t.Setenv("IS_FOO", "0")
|
|
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("higher"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("no"), c.Targets[0].Args["v2"])
|
|
|
|
t.Setenv("FOO", "NaN")
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "failed to parse FOO as number")
|
|
|
|
t.Setenv("FOO", "0")
|
|
t.Setenv("IS_FOO", "maybe")
|
|
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "failed to parse IS_FOO as bool")
|
|
}
|
|
|
|
func TestHCLNullVariables(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = null
|
|
}
|
|
target "default" {
|
|
args = {
|
|
foo = FOO
|
|
}
|
|
}`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, ptrstr(nil), c.Targets[0].Args["foo"])
|
|
|
|
t.Setenv("FOO", "bar")
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, ptrstr("bar"), c.Targets[0].Args["foo"])
|
|
}
|
|
|
|
func TestHCLTypedNullVariables(t *testing.T) {
|
|
types := []string{
|
|
"any",
|
|
"string", "number", "bool",
|
|
"list(string)", "set(string)", "map(string)",
|
|
"tuple([string])", "object({val: string})",
|
|
}
|
|
for _, varType := range types {
|
|
tName := fmt.Sprintf("variable typed %q with null default remains null", varType)
|
|
t.Run(tName, func(t *testing.T) {
|
|
dt := fmt.Sprintf(`
|
|
variable "FOO" {
|
|
type = %s
|
|
default = null
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = equal(FOO, null)
|
|
}
|
|
}`, varType)
|
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "true", *c.Targets[0].Args["foo"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHCLTypedValuelessVariables(t *testing.T) {
|
|
types := []string{
|
|
"any",
|
|
"string", "number", "bool",
|
|
"list(string)", "set(string)", "map(string)",
|
|
"tuple([string])", "object({val: string})",
|
|
}
|
|
for _, varType := range types {
|
|
tName := fmt.Sprintf("variable typed %q with no default is null", varType)
|
|
t.Run(tName, func(t *testing.T) {
|
|
dt := fmt.Sprintf(`
|
|
variable "FOO" {
|
|
type = %s
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = equal(FOO, null)
|
|
}
|
|
}`, varType)
|
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "true", *c.Targets[0].Args["foo"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSONNullVariables(t *testing.T) {
|
|
dt := []byte(`{
|
|
"variable": {
|
|
"FOO": {
|
|
"default": null
|
|
}
|
|
},
|
|
"target": {
|
|
"default": {
|
|
"args": {
|
|
"foo": "${FOO}"
|
|
}
|
|
}
|
|
}
|
|
}`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
require.Equal(t, ptrstr(nil), c.Targets[0].Args["foo"])
|
|
|
|
t.Setenv("FOO", "bar")
|
|
c, err = ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
require.Equal(t, ptrstr("bar"), c.Targets[0].Args["foo"])
|
|
}
|
|
|
|
func TestHCLVariableCycle(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = BAR
|
|
}
|
|
variable "FOO2" {
|
|
default = FOO
|
|
}
|
|
variable "BAR" {
|
|
default = FOO
|
|
}
|
|
target "app" {}
|
|
`)
|
|
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "variable cycle not allowed")
|
|
}
|
|
|
|
func TestHCLAttrs(t *testing.T) {
|
|
dt := []byte(`
|
|
FOO="abc"
|
|
BAR="attr-${FOO}def"
|
|
target "app" {
|
|
args = {
|
|
"v1": BAR
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("attr-abcdef"), c.Targets[0].Args["v1"])
|
|
|
|
// env does not apply if no variable
|
|
t.Setenv("FOO", "bar")
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("attr-abcdef"), c.Targets[0].Args["v1"])
|
|
// attr-multifile
|
|
}
|
|
|
|
func TestHCLTargetAttrs(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
dockerfile = "xxx"
|
|
context = target.bar.context
|
|
target = target.foo.dockerfile
|
|
}
|
|
|
|
target "bar" {
|
|
dockerfile = target.foo.dockerfile
|
|
context = "yyy"
|
|
target = target.bar.context
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "foo", c.Targets[0].Name)
|
|
require.Equal(t, "bar", c.Targets[1].Name)
|
|
|
|
require.Equal(t, "xxx", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "yyy", *c.Targets[0].Context)
|
|
require.Equal(t, "xxx", *c.Targets[0].Target)
|
|
|
|
require.Equal(t, "xxx", *c.Targets[1].Dockerfile)
|
|
require.Equal(t, "yyy", *c.Targets[1].Context)
|
|
require.Equal(t, "yyy", *c.Targets[1].Target)
|
|
}
|
|
|
|
func TestHCLTargetGlobal(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
dockerfile = "x"
|
|
}
|
|
x = target.foo.dockerfile
|
|
y = x
|
|
target "bar" {
|
|
dockerfile = y
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "foo", c.Targets[0].Name)
|
|
require.Equal(t, "bar", c.Targets[1].Name)
|
|
|
|
require.Equal(t, "x", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "x", *c.Targets[1].Dockerfile)
|
|
}
|
|
|
|
func TestHCLTargetAttrName(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
dockerfile = target.foo.name
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "foo", c.Targets[0].Name)
|
|
require.Equal(t, "foo", *c.Targets[0].Dockerfile)
|
|
}
|
|
|
|
func TestHCLTargetAttrEmptyChain(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
# dockerfile = Dockerfile
|
|
context = target.foo.dockerfile
|
|
target = target.foo.context
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "foo", c.Targets[0].Name)
|
|
require.Nil(t, c.Targets[0].Dockerfile)
|
|
require.Nil(t, c.Targets[0].Context)
|
|
require.Nil(t, c.Targets[0].Target)
|
|
}
|
|
|
|
func TestHCLAttrsCustomType(t *testing.T) {
|
|
dt := []byte(`
|
|
platforms=["linux/arm64", "linux/amd64"]
|
|
target "app" {
|
|
platforms = platforms
|
|
args = {
|
|
"v1": platforms[0]
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, []string{"linux/arm64", "linux/amd64"}, c.Targets[0].Platforms)
|
|
require.Equal(t, ptrstr("linux/arm64"), c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestHCLAttrsCapsuleType(t *testing.T) {
|
|
dt := []byte(`
|
|
target "app" {
|
|
attest = [
|
|
{ type = "provenance", mode = "max" },
|
|
"type=sbom,disabled=true,generator=foo,\"ENV1=bar,baz\",ENV2=hello",
|
|
]
|
|
|
|
cache-from = [
|
|
{ type = "registry", ref = "user/app:cache" },
|
|
"type=local,src=path/to/cache",
|
|
]
|
|
|
|
cache-to = [
|
|
{ type = "local", dest = "path/to/cache" },
|
|
]
|
|
|
|
output = [
|
|
{ type = "oci", dest = "../out.tar" },
|
|
"type=local,dest=../out",
|
|
]
|
|
|
|
secret = [
|
|
{ id = "mysecret", src = "/local/secret" },
|
|
{ id = "mysecret2", env = "TOKEN" },
|
|
]
|
|
|
|
ssh = [
|
|
{ id = "default" },
|
|
{ id = "key", paths = ["path/to/key"] },
|
|
]
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, []string{"type=provenance,mode=max", "type=sbom,disabled=true,\"ENV1=bar,baz\",ENV2=hello,generator=foo"}, stringify(c.Targets[0].Attest))
|
|
require.Equal(t, []string{"type=local,dest=../out", "type=oci,dest=../out.tar"}, stringify(c.Targets[0].Outputs))
|
|
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom))
|
|
require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[0].CacheTo))
|
|
require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,env=TOKEN"}, stringify(c.Targets[0].Secrets))
|
|
require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[0].SSH))
|
|
}
|
|
|
|
func TestHCLAttrsCapsuleType_ObjectVars(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "foo" {
|
|
default = "bar"
|
|
}
|
|
|
|
target "app" {
|
|
cache-from = [
|
|
{ type = "registry", ref = "user/app:cache" },
|
|
"type=local,src=path/to/cache",
|
|
]
|
|
|
|
cache-to = [ target.app.cache-from[0] ]
|
|
|
|
output = [
|
|
{ type = "oci", dest = "../out.tar" },
|
|
"type=local,dest=../out",
|
|
]
|
|
|
|
secret = [
|
|
{ id = "mysecret", src = "/local/secret" },
|
|
]
|
|
|
|
ssh = [
|
|
{ id = "default" },
|
|
{ id = "key", paths = ["path/to/${target.app.output[0].type}"] },
|
|
]
|
|
}
|
|
|
|
target "web" {
|
|
cache-from = target.app.cache-from
|
|
|
|
output = [ "type=oci,dest=../${foo}.tar" ]
|
|
|
|
secret = [
|
|
{ id = target.app.output[0].type, src = "/${target.app.cache-from[1].type}/secret" },
|
|
]
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
|
|
findTarget := func(t *testing.T, name string) *Target {
|
|
t.Helper()
|
|
for _, tgt := range c.Targets {
|
|
if tgt.Name == name {
|
|
return tgt
|
|
}
|
|
}
|
|
t.Fatalf("could not find target %q", name)
|
|
return nil
|
|
}
|
|
|
|
app := findTarget(t, "app")
|
|
require.Equal(t, []string{"type=local,dest=../out", "type=oci,dest=../out.tar"}, stringify(app.Outputs))
|
|
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(app.CacheFrom))
|
|
require.Equal(t, []string{"user/app:cache"}, stringify(app.CacheTo))
|
|
require.Equal(t, []string{"id=mysecret,src=/local/secret"}, stringify(app.Secrets))
|
|
require.Equal(t, []string{"default", "key=path/to/oci"}, stringify(app.SSH))
|
|
|
|
web := findTarget(t, "web")
|
|
require.Equal(t, []string{"type=oci,dest=../bar.tar"}, stringify(web.Outputs))
|
|
require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(web.CacheFrom))
|
|
require.Equal(t, []string{"id=oci,src=/local/secret"}, stringify(web.Secrets))
|
|
}
|
|
|
|
func TestHCLAttrsCapsuleType_MissingVars(t *testing.T) {
|
|
dt := []byte(`
|
|
target "app" {
|
|
attest = [
|
|
"type=sbom,disabled=${SBOM}",
|
|
]
|
|
|
|
cache-from = [
|
|
{ type = "registry", ref = "user/app:${FOO1}" },
|
|
"type=local,src=path/to/cache:${FOO2}",
|
|
]
|
|
|
|
cache-to = [
|
|
{ type = "local", dest = "path/to/${BAR}" },
|
|
]
|
|
|
|
output = [
|
|
{ type = "oci", dest = "../${OUTPUT}.tar" },
|
|
]
|
|
|
|
secret = [
|
|
{ id = "mysecret", src = "/local/${SECRET}" },
|
|
]
|
|
|
|
ssh = [
|
|
{ id = "key", paths = ["path/to/${SSH_KEY}"] },
|
|
]
|
|
}
|
|
`)
|
|
|
|
var diags hcl.Diagnostics
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorAs(t, err, &diags)
|
|
|
|
re := regexp.MustCompile(`There is no variable named "([\w\d_]+)"`)
|
|
var actual []string
|
|
for _, diag := range diags {
|
|
if m := re.FindStringSubmatch(diag.Error()); m != nil {
|
|
actual = append(actual, m[1])
|
|
}
|
|
}
|
|
require.ElementsMatch(t,
|
|
[]string{"SBOM", "FOO1", "FOO2", "BAR", "OUTPUT", "SECRET", "SSH_KEY"},
|
|
actual)
|
|
}
|
|
|
|
func TestHCLMultiFileAttrs(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = "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, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre-def"), c.Targets[0].Args["v1"])
|
|
|
|
t.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, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre-ghi"), c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestHCLMultiFileGlobalAttrs(t *testing.T) {
|
|
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, "app", c.Targets[0].Name)
|
|
require.Equal(t, "pre-def", *c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestHCLDuplicateTarget(t *testing.T) {
|
|
dt := []byte(`
|
|
target "app" {
|
|
dockerfile = "x"
|
|
}
|
|
target "app" {
|
|
dockerfile = "y"
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, "y", *c.Targets[0].Dockerfile)
|
|
}
|
|
|
|
func TestHCLRenameTarget(t *testing.T) {
|
|
dt := []byte(`
|
|
target "abc" {
|
|
name = "xyz"
|
|
dockerfile = "foo"
|
|
}
|
|
`)
|
|
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "requires matrix")
|
|
}
|
|
|
|
func TestHCLRenameGroup(t *testing.T) {
|
|
dt := []byte(`
|
|
group "foo" {
|
|
name = "bar"
|
|
targets = ["x", "y"]
|
|
}
|
|
`)
|
|
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "not supported")
|
|
|
|
dt = []byte(`
|
|
group "foo" {
|
|
matrix = {
|
|
name = ["x", "y"]
|
|
}
|
|
}
|
|
`)
|
|
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "not supported")
|
|
}
|
|
|
|
func TestHCLRenameTargetAttrs(t *testing.T) {
|
|
dt := []byte(`
|
|
target "abc" {
|
|
name = "xyz"
|
|
matrix = {}
|
|
dockerfile = "foo"
|
|
}
|
|
|
|
target "def" {
|
|
dockerfile = target.xyz.dockerfile
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "xyz", c.Targets[0].Name)
|
|
require.Equal(t, "foo", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "def", c.Targets[1].Name)
|
|
require.Equal(t, "foo", *c.Targets[1].Dockerfile)
|
|
|
|
dt = []byte(`
|
|
target "def" {
|
|
dockerfile = target.xyz.dockerfile
|
|
}
|
|
|
|
target "abc" {
|
|
name = "xyz"
|
|
matrix = {}
|
|
dockerfile = "foo"
|
|
}
|
|
`)
|
|
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "def", c.Targets[0].Name)
|
|
require.Equal(t, "foo", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "xyz", c.Targets[1].Name)
|
|
require.Equal(t, "foo", *c.Targets[1].Dockerfile)
|
|
|
|
dt = []byte(`
|
|
target "abc" {
|
|
name = "xyz"
|
|
matrix = {}
|
|
dockerfile = "foo"
|
|
}
|
|
|
|
target "def" {
|
|
dockerfile = target.abc.dockerfile
|
|
}
|
|
`)
|
|
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "abc")
|
|
|
|
dt = []byte(`
|
|
target "def" {
|
|
dockerfile = target.abc.dockerfile
|
|
}
|
|
|
|
target "abc" {
|
|
name = "xyz"
|
|
matrix = {}
|
|
dockerfile = "foo"
|
|
}
|
|
`)
|
|
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "abc")
|
|
}
|
|
|
|
func TestHCLRenameSplit(t *testing.T) {
|
|
dt := []byte(`
|
|
target "x" {
|
|
name = "y"
|
|
matrix = {}
|
|
dockerfile = "foo"
|
|
}
|
|
|
|
target "x" {
|
|
name = "z"
|
|
matrix = {}
|
|
dockerfile = "bar"
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "y", c.Targets[0].Name)
|
|
require.Equal(t, "foo", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "z", c.Targets[1].Name)
|
|
require.Equal(t, "bar", *c.Targets[1].Dockerfile)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "x", c.Groups[0].Name)
|
|
require.Equal(t, []string{"y", "z"}, c.Groups[0].Targets)
|
|
}
|
|
|
|
func TestHCLRenameMultiFile(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
name = "bar"
|
|
matrix = {}
|
|
dockerfile = "x"
|
|
}
|
|
`)
|
|
dt2 := []byte(`
|
|
target "foo" {
|
|
context = "y"
|
|
}
|
|
`)
|
|
dt3 := []byte(`
|
|
target "bar" {
|
|
target = "z"
|
|
}
|
|
`)
|
|
|
|
c, _, err := ParseFiles([]File{
|
|
{Data: dt, Name: "c1.hcl"},
|
|
{Data: dt2, Name: "c2.hcl"},
|
|
{Data: dt3, Name: "c3.hcl"},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
|
|
require.Equal(t, "bar", c.Targets[0].Name)
|
|
require.Equal(t, "x", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "z", *c.Targets[0].Target)
|
|
|
|
require.Equal(t, "foo", c.Targets[1].Name)
|
|
require.Equal(t, "y", *c.Targets[1].Context)
|
|
}
|
|
|
|
func TestHCLMatrixBasic(t *testing.T) {
|
|
dt := []byte(`
|
|
target "default" {
|
|
matrix = {
|
|
foo = ["x", "y"]
|
|
}
|
|
name = foo
|
|
dockerfile = "${foo}.Dockerfile"
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "x", c.Targets[0].Name)
|
|
require.Equal(t, "y", c.Targets[1].Name)
|
|
require.Equal(t, "x.Dockerfile", *c.Targets[0].Dockerfile)
|
|
require.Equal(t, "y.Dockerfile", *c.Targets[1].Dockerfile)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.Equal(t, []string{"x", "y"}, c.Groups[0].Targets)
|
|
}
|
|
|
|
func TestHCLMatrixMultipleKeys(t *testing.T) {
|
|
dt := []byte(`
|
|
target "default" {
|
|
matrix = {
|
|
foo = ["a"]
|
|
bar = ["b", "c"]
|
|
baz = ["d", "e", "f"]
|
|
}
|
|
name = "${foo}-${bar}-${baz}"
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 6, len(c.Targets))
|
|
names := make([]string, len(c.Targets))
|
|
for i, t := range c.Targets {
|
|
names[i] = t.Name
|
|
}
|
|
require.ElementsMatch(t, []string{"a-b-d", "a-b-e", "a-b-f", "a-c-d", "a-c-e", "a-c-f"}, names)
|
|
|
|
require.Equal(t, 1, len(c.Groups))
|
|
require.Equal(t, "default", c.Groups[0].Name)
|
|
require.ElementsMatch(t, []string{"a-b-d", "a-b-e", "a-b-f", "a-c-d", "a-c-e", "a-c-f"}, c.Groups[0].Targets)
|
|
}
|
|
|
|
func TestHCLMatrixLists(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
matrix = {
|
|
aa = [["aa", "bb"], ["cc", "dd"]]
|
|
}
|
|
name = aa[0]
|
|
args = {
|
|
target = "val${aa[1]}"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "aa", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("valbb"), c.Targets[0].Args["target"])
|
|
require.Equal(t, "cc", c.Targets[1].Name)
|
|
require.Equal(t, ptrstr("valdd"), c.Targets[1].Args["target"])
|
|
}
|
|
|
|
func TestHCLMatrixMaps(t *testing.T) {
|
|
dt := []byte(`
|
|
target "foo" {
|
|
matrix = {
|
|
aa = [
|
|
{
|
|
foo = "aa"
|
|
bar = "bb"
|
|
},
|
|
{
|
|
foo = "cc"
|
|
bar = "dd"
|
|
}
|
|
]
|
|
}
|
|
name = aa.foo
|
|
args = {
|
|
target = "val${aa.bar}"
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "aa", c.Targets[0].Name)
|
|
require.Equal(t, c.Targets[0].Args["target"], ptrstr("valbb"))
|
|
require.Equal(t, "cc", c.Targets[1].Name)
|
|
require.Equal(t, c.Targets[1].Args["target"], ptrstr("valdd"))
|
|
}
|
|
|
|
func TestHCLMatrixMultipleTargets(t *testing.T) {
|
|
dt := []byte(`
|
|
target "x" {
|
|
matrix = {
|
|
foo = ["a", "b"]
|
|
}
|
|
name = foo
|
|
}
|
|
target "y" {
|
|
matrix = {
|
|
bar = ["c", "d"]
|
|
}
|
|
name = bar
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 4, len(c.Targets))
|
|
names := make([]string, len(c.Targets))
|
|
for i, t := range c.Targets {
|
|
names[i] = t.Name
|
|
}
|
|
require.ElementsMatch(t, []string{"a", "b", "c", "d"}, names)
|
|
|
|
require.Equal(t, 2, len(c.Groups))
|
|
names = make([]string, len(c.Groups))
|
|
for i, c := range c.Groups {
|
|
names[i] = c.Name
|
|
}
|
|
require.ElementsMatch(t, []string{"x", "y"}, names)
|
|
|
|
for _, g := range c.Groups {
|
|
switch g.Name {
|
|
case "x":
|
|
require.Equal(t, []string{"a", "b"}, g.Targets)
|
|
case "y":
|
|
require.Equal(t, []string{"c", "d"}, g.Targets)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHCLMatrixDuplicateNames(t *testing.T) {
|
|
dt := []byte(`
|
|
target "default" {
|
|
matrix = {
|
|
foo = ["a", "b"]
|
|
}
|
|
name = "c"
|
|
}
|
|
`)
|
|
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestHCLMatrixArgs(t *testing.T) {
|
|
dt := []byte(`
|
|
a = 1
|
|
variable "b" {
|
|
default = 2
|
|
}
|
|
target "default" {
|
|
matrix = {
|
|
foo = [a, b]
|
|
}
|
|
name = foo
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "1", c.Targets[0].Name)
|
|
require.Equal(t, "2", c.Targets[1].Name)
|
|
}
|
|
|
|
func TestHCLMatrixArgsOverride(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "ABC" {
|
|
default = "def"
|
|
}
|
|
|
|
target "bar" {
|
|
matrix = {
|
|
aa = split(",", ABC)
|
|
}
|
|
name = "bar-${aa}"
|
|
args = {
|
|
foo = aa
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, _, err := ParseFiles([]File{
|
|
{Data: dt, Name: "docker-bake.hcl"},
|
|
}, map[string]string{"ABC": "11,22,33"})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 3, len(c.Targets))
|
|
require.Equal(t, "bar-11", c.Targets[0].Name)
|
|
require.Equal(t, "bar-22", c.Targets[1].Name)
|
|
require.Equal(t, "bar-33", c.Targets[2].Name)
|
|
|
|
require.Equal(t, ptrstr("11"), c.Targets[0].Args["foo"])
|
|
require.Equal(t, ptrstr("22"), c.Targets[1].Args["foo"])
|
|
require.Equal(t, ptrstr("33"), c.Targets[2].Args["foo"])
|
|
}
|
|
|
|
func TestHCLMatrixBadTypes(t *testing.T) {
|
|
dt := []byte(`
|
|
target "default" {
|
|
matrix = "test"
|
|
}
|
|
`)
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
|
|
dt = []byte(`
|
|
target "default" {
|
|
matrix = ["test"]
|
|
}
|
|
`)
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
|
|
dt = []byte(`
|
|
target "default" {
|
|
matrix = {
|
|
["a"] = ["b"]
|
|
}
|
|
}
|
|
`)
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
|
|
dt = []byte(`
|
|
target "default" {
|
|
matrix = {
|
|
1 = 2
|
|
}
|
|
}
|
|
`)
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
|
|
dt = []byte(`
|
|
target "default" {
|
|
matrix = {
|
|
a = "b"
|
|
}
|
|
}
|
|
`)
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestHCLMatrixWithGlobalTarget(t *testing.T) {
|
|
dt := []byte(`
|
|
target "x" {
|
|
tags = ["a", "b"]
|
|
}
|
|
|
|
target "default" {
|
|
tags = target.x.tags
|
|
matrix = {
|
|
dummy = [""]
|
|
}
|
|
}
|
|
`)
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "x", c.Targets[0].Name)
|
|
require.Equal(t, "default", c.Targets[1].Name)
|
|
require.Equal(t, []string{"a", "b"}, c.Targets[1].Tags)
|
|
}
|
|
|
|
func TestJSONAttributes(t *testing.T) {
|
|
dt := []byte(`{"FOO": "abc", "variable": {"BAR": {"default": "def"}}, "target": { "app": { "args": {"v1": "pre-${FOO}-${BAR}"}} } }`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre-abc-def"), c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestJSONFunctions(t *testing.T) {
|
|
dt := []byte(`{
|
|
"FOO": "abc",
|
|
"function": {
|
|
"myfunc": {
|
|
"params": ["inp"],
|
|
"result": "<${upper(inp)}-${FOO}>"
|
|
}
|
|
},
|
|
"target": {
|
|
"app": {
|
|
"args": {
|
|
"v1": "pre-${myfunc(\"foo\")}"
|
|
}
|
|
}
|
|
}}`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("pre-<FOO-abc>"), c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestJSONInvalidFunctions(t *testing.T) {
|
|
dt := []byte(`{
|
|
"target": {
|
|
"app": {
|
|
"args": {
|
|
"v1": "myfunc(\"foo\")"
|
|
}
|
|
}
|
|
}}`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr(`myfunc("foo")`), c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestHCLFunctionInAttr(t *testing.T) {
|
|
dt := []byte(`
|
|
function "brace" {
|
|
params = [inp]
|
|
result = "[${inp}]"
|
|
}
|
|
function "myupper" {
|
|
params = [val]
|
|
result = "${upper(val)} <> ${brace(v2)}"
|
|
}
|
|
|
|
v1=myupper("foo")
|
|
v2=lower("BAZ")
|
|
target "app" {
|
|
args = {
|
|
"v1": v1
|
|
}
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("FOO <> [baz]"), c.Targets[0].Args["v1"])
|
|
}
|
|
|
|
func TestHCLCombineCompose(t *testing.T) {
|
|
dt := []byte(`
|
|
target "app" {
|
|
context = "dir"
|
|
args = {
|
|
v1 = "foo"
|
|
}
|
|
}
|
|
`)
|
|
dt2 := []byte(`
|
|
version: "3"
|
|
|
|
services:
|
|
app:
|
|
build:
|
|
dockerfile: Dockerfile-alternate
|
|
args:
|
|
v2: "bar"
|
|
`)
|
|
|
|
c, _, err := ParseFiles([]File{
|
|
{Data: dt, Name: "c1.hcl"},
|
|
{Data: dt2, Name: "c2.yml"},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
require.Equal(t, ptrstr("foo"), c.Targets[0].Args["v1"])
|
|
require.Equal(t, ptrstr("bar"), c.Targets[0].Args["v2"])
|
|
require.Equal(t, "dir", *c.Targets[0].Context)
|
|
require.Equal(t, "Dockerfile-alternate", *c.Targets[0].Dockerfile)
|
|
}
|
|
|
|
func TestHCLBuiltinVars(t *testing.T) {
|
|
dt := []byte(`
|
|
target "app" {
|
|
context = BAKE_CMD_CONTEXT
|
|
dockerfile = "test"
|
|
}
|
|
`)
|
|
|
|
c, _, err := ParseFiles([]File{
|
|
{Data: dt, Name: "c1.hcl"},
|
|
}, map[string]string{
|
|
"BAKE_CMD_CONTEXT": "foo",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "app", c.Targets[0].Name)
|
|
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, "metadata-a", c.Targets[0].Name)
|
|
require.Equal(t, []string{"app/a:1.0.0", "app/a:latest"}, c.Targets[0].Tags)
|
|
|
|
require.Equal(t, "metadata-b", c.Targets[1].Name)
|
|
require.Equal(t, []string{"app/b:1.0.0", "app/b:latest"}, c.Targets[1].Tags)
|
|
|
|
require.Equal(t, "a", c.Targets[2].Name)
|
|
require.Equal(t, ".", *c.Targets[2].Context)
|
|
require.Equal(t, "a", *c.Targets[2].Target)
|
|
|
|
require.Equal(t, "b", c.Targets[3].Name)
|
|
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"}`),
|
|
},
|
|
}, 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, "one", c.Targets[0].Name)
|
|
require.Equal(t, map[string]*string{"a": ptrstr("pre-ghi-jkl")}, c.Targets[0].Args)
|
|
|
|
require.Equal(t, "two", c.Targets[1].Name)
|
|
require.Equal(t, map[string]*string{"b": ptrstr("pre-jkl")}, c.Targets[1].Args)
|
|
}
|
|
|
|
func TestEmptyVariableJSON(t *testing.T) {
|
|
dt := []byte(`{
|
|
"variable": {
|
|
"VAR": {}
|
|
}
|
|
}`)
|
|
_, err := ParseFile(dt, "docker-bake.json")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFunctionNoParams(t *testing.T) {
|
|
dt := []byte(`
|
|
function "foo" {
|
|
result = "bar"
|
|
}
|
|
target "foo_target" {
|
|
args = {
|
|
test = foo()
|
|
}
|
|
}
|
|
`)
|
|
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestFunctionNoResult(t *testing.T) {
|
|
dt := []byte(`
|
|
function "foo" {
|
|
params = ["a"]
|
|
}
|
|
`)
|
|
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestVarUnsupportedType(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = []
|
|
}
|
|
target "default" {}`)
|
|
|
|
t.Setenv("FOO", "bar")
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestHCLIndexOfFunc(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "APP_VERSIONS" {
|
|
default = [
|
|
"1.42.4",
|
|
"1.42.3"
|
|
]
|
|
}
|
|
target "default" {
|
|
args = {
|
|
APP_VERSION = app_version
|
|
}
|
|
matrix = {
|
|
app_version = APP_VERSIONS
|
|
}
|
|
name="app-${replace(app_version, ".", "-")}"
|
|
tags = [
|
|
"app:${app_version}",
|
|
indexof(APP_VERSIONS, app_version) == 0 ? "app:latest" : "",
|
|
]
|
|
}
|
|
`)
|
|
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(c.Targets))
|
|
require.Equal(t, "app-1-42-4", c.Targets[0].Name)
|
|
require.Equal(t, "app:latest", c.Targets[0].Tags[1])
|
|
require.Equal(t, "app-1-42-3", c.Targets[1].Name)
|
|
require.Empty(t, c.Targets[1].Tags[1])
|
|
}
|
|
|
|
func TestVarTypingSpec(t *testing.T) {
|
|
templ := `
|
|
variable "FOO" {
|
|
type = %s
|
|
}
|
|
target "default" {
|
|
}`
|
|
|
|
// not exhaustive, but the common ones
|
|
for _, s := range []string{
|
|
"bool", "number", "string", "any",
|
|
"list(string)", "set(string)", "tuple([string, number])",
|
|
} {
|
|
dt := fmt.Sprintf(templ, s)
|
|
_, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
for _, s := range []string{
|
|
"boolean", // no synonyms/aliases
|
|
"BOOL", // case matters
|
|
`lower("bool")`, // must be literals
|
|
} {
|
|
dt := fmt.Sprintf(templ, s)
|
|
_, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "not a valid type")
|
|
}
|
|
}
|
|
|
|
func TestDefaultVarTypeEnforcement(t *testing.T) {
|
|
// To help prove a given default doesn't just pass the type check, but *is* that type,
|
|
// we use argValue to provide an expression that would work only on that type.
|
|
tests := []struct {
|
|
name string
|
|
varType string
|
|
varDefault any
|
|
argValue string
|
|
wantValue string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "number (happy)",
|
|
varType: "number",
|
|
varDefault: 99,
|
|
argValue: "FOO + 1",
|
|
wantValue: "100",
|
|
},
|
|
{
|
|
name: "numeric string compatible with number",
|
|
varType: "number",
|
|
varDefault: `"99"`,
|
|
argValue: "FOO + 1",
|
|
wantValue: "100",
|
|
},
|
|
{
|
|
name: "boolean (happy)",
|
|
varType: "bool",
|
|
varDefault: true,
|
|
argValue: "and(FOO, true)",
|
|
wantValue: "true",
|
|
},
|
|
{
|
|
name: "numeric boolean compatible with boolean",
|
|
varType: "bool",
|
|
varDefault: `"true"`,
|
|
argValue: "and(FOO, true)",
|
|
wantValue: "true",
|
|
},
|
|
// should be representative of flagrant primitive type mismatches; not worth listing all possibilities?
|
|
{
|
|
name: "non-numeric string default incompatible with number",
|
|
varType: "number",
|
|
varDefault: `"oops"`,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "list of numbers (happy)",
|
|
varType: "list(number)",
|
|
varDefault: "[2,3]",
|
|
argValue: `join("", [for v in FOO: v + 1])`,
|
|
wantValue: "34",
|
|
},
|
|
{
|
|
name: "list of numbers with numeric strings okay",
|
|
varType: "list(number)",
|
|
varDefault: `["2","3"]`,
|
|
argValue: `join("", [for v in FOO: v + 1])`,
|
|
wantValue: "34",
|
|
},
|
|
// represent flagrant mismatches for list types
|
|
{
|
|
name: "non-numeric strings in numeric list rejected",
|
|
varType: "list(number)",
|
|
varDefault: `["oops"]`,
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
argValue := tt.argValue
|
|
if argValue == "" {
|
|
argValue = "FOO"
|
|
}
|
|
dt := fmt.Sprintf(`
|
|
variable "FOO" {
|
|
type = %s
|
|
default = %v
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = %s
|
|
}
|
|
}`, tt.varType, tt.varDefault, argValue)
|
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
if tt.wantError {
|
|
require.ErrorContains(t, err, "invalid type")
|
|
} else {
|
|
require.NoError(t, err)
|
|
if tt.wantValue != "" {
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, ptrstr(tt.wantValue), c.Targets[0].Args["foo"])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDefaultVarTypeWithAttrValuesEnforcement(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
attrValue any
|
|
varType string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "attribute literal which matches var type",
|
|
attrValue: `"hello"`,
|
|
varType: "string",
|
|
},
|
|
{
|
|
name: "attribute literal which coerces to var type",
|
|
attrValue: `"99"`,
|
|
varType: "number",
|
|
},
|
|
{
|
|
name: "attribute from function which coerces to var type",
|
|
attrValue: `substr("99 bottles", 0, 2)`,
|
|
varType: "number",
|
|
},
|
|
{
|
|
name: "attribute from function returning non-coercible value",
|
|
attrValue: `split(",", "1,2,3foo")`,
|
|
varType: "list(number)",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "mismatch",
|
|
attrValue: 99,
|
|
varType: "bool",
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "attribute correctly typed via function",
|
|
attrValue: `split(",", "1,2,3")`,
|
|
varType: "list(number)",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dt := fmt.Sprintf(`
|
|
BAR = %v
|
|
variable "FOO" {
|
|
type = %s
|
|
default = BAR
|
|
}
|
|
|
|
target "default" {
|
|
}`, tt.attrValue, tt.varType)
|
|
_, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
if tt.wantError {
|
|
require.ErrorContains(t, err, "invalid type")
|
|
require.ErrorContains(t, err, "FOO default value")
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypedVarOverrides(t *testing.T) {
|
|
const unsuitableValueType = "Unsuitable value type"
|
|
const unsupportedType = "unsupported type"
|
|
const failedToParseElement = "failed to parse element"
|
|
tests := []struct {
|
|
name string
|
|
varType string
|
|
override string
|
|
argValue string
|
|
wantValue string
|
|
wantErrorMsg string
|
|
}{
|
|
{
|
|
name: "boolean",
|
|
varType: "bool",
|
|
override: "true",
|
|
wantValue: "true",
|
|
},
|
|
{
|
|
name: "number",
|
|
varType: "number",
|
|
override: "99",
|
|
wantValue: "99",
|
|
},
|
|
{
|
|
name: "unquoted string accepted",
|
|
varType: "string",
|
|
override: "hello",
|
|
wantValue: "hello",
|
|
},
|
|
// an environment variable with a quoted string would most likely be intended
|
|
// to be a string whose first and last characters are quotes
|
|
{
|
|
name: "quoted string keeps quotes in value",
|
|
varType: "string",
|
|
override: `"hello"`,
|
|
wantValue: `"hello"`,
|
|
},
|
|
{
|
|
name: "proper CSV list of strings",
|
|
varType: "list(string)",
|
|
override: "hi,there",
|
|
argValue: `join("-", FOO)`,
|
|
wantValue: "hi-there",
|
|
},
|
|
{
|
|
name: "CSV of unquoted strings okay",
|
|
varType: "list(string)",
|
|
override: `hi,there`,
|
|
argValue: `join("-", FOO)`,
|
|
wantValue: "hi-there",
|
|
},
|
|
{
|
|
name: "CSV list of numbers",
|
|
varType: "list(number)",
|
|
override: "3,1,4",
|
|
argValue: `join("-", [for v in FOO: v + 1])`,
|
|
wantValue: "4-2-5",
|
|
},
|
|
{
|
|
name: "CSV set of numbers",
|
|
varType: "set(number)",
|
|
override: "3,1,4",
|
|
// anecdotally sets are sorted but may not be guaranteed
|
|
argValue: `join("-", [for v in sort(FOO): v + 1])`,
|
|
wantValue: "2-4-5",
|
|
},
|
|
{
|
|
name: "CSV map of numbers",
|
|
varType: "map(number)",
|
|
override: "foo:1,bar:2",
|
|
argValue: `join("-", sort(values(FOO)))`,
|
|
wantValue: "1-2",
|
|
},
|
|
{
|
|
name: "CSV tuple",
|
|
varType: "tuple([number,string])",
|
|
override: `99,bottles`,
|
|
argValue: `format("%d %s", FOO[0], FOO[1])`,
|
|
wantValue: "99 bottles",
|
|
},
|
|
{
|
|
name: "CSV tuple elements with wrong type",
|
|
varType: "tuple([number,string])",
|
|
override: `99,100`,
|
|
wantErrorMsg: unsuitableValueType,
|
|
},
|
|
{
|
|
name: "invalid CSV value",
|
|
varType: "list(string)",
|
|
override: `"hello,world`,
|
|
wantErrorMsg: "from CSV",
|
|
},
|
|
{
|
|
name: "object not supported",
|
|
varType: "object({message: string})",
|
|
override: "does not matter",
|
|
wantErrorMsg: unsupportedType,
|
|
},
|
|
{
|
|
name: "list of non-primitives not supported",
|
|
varType: "list(list(number))",
|
|
override: "1,2",
|
|
wantErrorMsg: unsupportedType,
|
|
},
|
|
{
|
|
name: "set of non-primitives not supported",
|
|
varType: "set(set(number))",
|
|
override: "1,2",
|
|
wantErrorMsg: unsupportedType,
|
|
},
|
|
{
|
|
name: "tuple of non-primitives not supported",
|
|
varType: "tuple([list(number)])",
|
|
// Intentionally a different override than other similar tests; tuple is unique in that
|
|
// multiple types are involved and length matters. In the real world, it's probably more
|
|
// likely a user would accidentally omit or add an item than trying to use non-primitives,
|
|
// so the length check comes first.
|
|
override: "1",
|
|
wantErrorMsg: unsupportedType,
|
|
},
|
|
{
|
|
name: "map of non-primitives not supported",
|
|
varType: "map(list(number))",
|
|
override: "foo:1,2",
|
|
wantErrorMsg: unsupportedType,
|
|
},
|
|
{
|
|
name: "invalid map k/v parsing",
|
|
varType: "map(string)",
|
|
// TODO fragile; will fail in a different manner without first k/v pair
|
|
override: `a:b,foo:"bar`,
|
|
wantErrorMsg: "as CSV",
|
|
},
|
|
{
|
|
name: "list with invalidly parsed elements",
|
|
varType: "list(number)",
|
|
override: "1,1z",
|
|
wantErrorMsg: failedToParseElement,
|
|
},
|
|
{
|
|
name: "set with invalidly parsed elements",
|
|
varType: "set(number)",
|
|
override: "1,1z",
|
|
wantErrorMsg: failedToParseElement,
|
|
},
|
|
{
|
|
name: "tuple with invalidly parsed elements",
|
|
varType: "tuple([number])",
|
|
override: "1z",
|
|
wantErrorMsg: failedToParseElement,
|
|
},
|
|
{
|
|
name: "map with invalidly parsed elements",
|
|
varType: "map(number)",
|
|
override: "foo:1z",
|
|
wantErrorMsg: failedToParseElement,
|
|
},
|
|
{
|
|
name: "map with bad value format",
|
|
varType: "map(number)",
|
|
override: "foo:1:1",
|
|
wantErrorMsg: "expected one k/v pair",
|
|
},
|
|
{
|
|
name: "primitive with bad value format",
|
|
varType: "number",
|
|
override: "1z",
|
|
wantErrorMsg: "failed to parse",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
argValue := tt.argValue
|
|
if argValue == "" {
|
|
argValue = "FOO"
|
|
}
|
|
dt := fmt.Sprintf(`
|
|
variable "FOO" {
|
|
type = %s
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = %s
|
|
}
|
|
}`, tt.varType, argValue)
|
|
t.Setenv("FOO", tt.override)
|
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
if tt.wantErrorMsg != "" {
|
|
require.ErrorContains(t, err, tt.wantErrorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
if tt.wantValue != "" {
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, tt.wantValue, *c.Targets[0].Args["foo"])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypedVarOverrides_JSON(t *testing.T) {
|
|
const unsuitableValueType = "Unsuitable value type"
|
|
tests := []struct {
|
|
name string
|
|
varType string
|
|
override string
|
|
argValue string
|
|
wantValue string
|
|
wantErrorMsg string
|
|
}{
|
|
{
|
|
name: "boolean",
|
|
varType: "bool",
|
|
override: "true",
|
|
wantValue: "true",
|
|
},
|
|
{
|
|
name: "number",
|
|
varType: "number",
|
|
override: "99",
|
|
wantValue: "99",
|
|
},
|
|
// no shortcuts in JSON mode
|
|
{
|
|
name: "unquoted string is error",
|
|
varType: "string",
|
|
override: "hello",
|
|
wantErrorMsg: "from JSON",
|
|
},
|
|
{
|
|
name: "string",
|
|
varType: "string",
|
|
override: `"hello"`,
|
|
wantValue: "hello",
|
|
},
|
|
{
|
|
name: "list of strings",
|
|
varType: "list(string)",
|
|
override: `["hi","there"]`,
|
|
argValue: `join("-", FOO)`,
|
|
wantValue: "hi-there",
|
|
},
|
|
{
|
|
name: "list of numbers",
|
|
varType: "list(number)",
|
|
override: "[3, 1, 4]",
|
|
argValue: `join("-", [for v in FOO: v + 1])`,
|
|
wantValue: "4-2-5",
|
|
},
|
|
{
|
|
name: "map of numbers",
|
|
varType: "map(number)",
|
|
override: `{"foo": 1, "bar": 2}`,
|
|
argValue: `join("-", sort(values(FOO)))`,
|
|
wantValue: "1-2",
|
|
},
|
|
{
|
|
name: "invalid JSON map of numbers",
|
|
varType: "map(number)",
|
|
override: `{"foo": "oops", "bar": 2}`,
|
|
// in lieu of something like ErrorMatches, this is the best single phrase
|
|
wantErrorMsg: "from JSON",
|
|
},
|
|
{
|
|
name: "tuple",
|
|
varType: "tuple([number,string])",
|
|
override: `[99, "bottles"]`,
|
|
argValue: `format("%d %s", FOO[0], FOO[1])`,
|
|
wantValue: "99 bottles",
|
|
},
|
|
{
|
|
name: "tuple elements with wrong type",
|
|
varType: "tuple([number,string])",
|
|
override: `[99, 100]`,
|
|
wantErrorMsg: unsuitableValueType,
|
|
},
|
|
{
|
|
name: "JSON object",
|
|
varType: `object({messages: list(string)})`,
|
|
override: `{"messages": ["hi", "there"]}`,
|
|
argValue: `join("-", FOO["messages"])`,
|
|
wantValue: "hi-there",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
argValue := tt.argValue
|
|
if argValue == "" {
|
|
argValue = "FOO"
|
|
}
|
|
dt := fmt.Sprintf(`
|
|
variable "FOO" {
|
|
type = %s
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = %s
|
|
}
|
|
}`, tt.varType, argValue)
|
|
t.Setenv("FOO_JSON", tt.override)
|
|
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
|
|
if tt.wantErrorMsg != "" {
|
|
require.ErrorContains(t, err, tt.wantErrorMsg)
|
|
} else {
|
|
require.NoError(t, err)
|
|
if tt.wantValue != "" {
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, tt.wantValue, *c.Targets[0].Args["foo"])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSONOverridePriority(t *testing.T) {
|
|
t.Run("JSON override ignored when same user var exists", func(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
type = list(number)
|
|
}
|
|
variable "FOO_JSON" {
|
|
type = list(number)
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = FOO
|
|
}
|
|
}`)
|
|
// env FOO_JSON is the CSV override of var FOO_JSON, not a JSON override of FOO
|
|
t.Setenv("FOO", "[1,2]")
|
|
t.Setenv("FOO_JSON", "[3,4]")
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "failed to convert")
|
|
require.ErrorContains(t, err, "from CSV")
|
|
})
|
|
|
|
t.Run("JSON override ignored when same builtin var exists", func(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
type = list(number)
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = length(FOO)
|
|
}
|
|
}`)
|
|
t.Setenv("FOO", "1,2")
|
|
t.Setenv("FOO_JSON", "[3,4,5]")
|
|
c, _, err := ParseFiles(
|
|
[]File{{Name: "docker-bake.hcl", Data: dt}},
|
|
map[string]string{"FOO_JSON": "whatever"},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "2", *c.Targets[0].Args["foo"])
|
|
})
|
|
|
|
// this is implied/exercised in other tests, but repeated for completeness
|
|
t.Run("JSON override ignored if var is untyped", func(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
default = [1, 2]
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = length(FOO)
|
|
}
|
|
}`)
|
|
t.Setenv("FOO_JSON", "[3,4]")
|
|
_, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "unsupported type")
|
|
})
|
|
|
|
t.Run("override-ish variable has regular CSV override", func(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO_JSON" {
|
|
type = list(number)
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = length(FOO_JSON)
|
|
}
|
|
}`)
|
|
// despite the name, it's still CSV
|
|
t.Setenv("FOO_JSON", "10,11,12")
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "3", *c.Targets[0].Args["foo"])
|
|
|
|
t.Setenv("FOO_JSON", "[10,11,12]")
|
|
_, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.ErrorContains(t, err, "from CSV")
|
|
})
|
|
|
|
t.Run("override-ish variable has own JSON override", func(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO_JSON" {
|
|
type = list(number)
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = length(FOO_JSON)
|
|
}
|
|
}`)
|
|
t.Setenv("FOO_JSON_JSON", "[4,5,6]")
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "3", *c.Targets[0].Args["foo"])
|
|
})
|
|
|
|
t.Run("JSON override trumps CSV when no var name conflict", func(t *testing.T) {
|
|
dt := []byte(`
|
|
variable "FOO" {
|
|
type = list(number)
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
foo = length(FOO)
|
|
}
|
|
}`)
|
|
t.Setenv("FOO", "1,2")
|
|
t.Setenv("FOO_JSON", "[3,4,5]")
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "3", *c.Targets[0].Args["foo"])
|
|
})
|
|
|
|
t.Run("JSON override works with lowercase vars", func(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Windows case-insensitivity")
|
|
}
|
|
dt := []byte(`
|
|
variable "foo" {
|
|
type = number
|
|
default = 101
|
|
}
|
|
|
|
target "default" {
|
|
args = {
|
|
bar = foo
|
|
}
|
|
}`)
|
|
// may seem reasonable, but not supported (on case-sensitive systems)
|
|
t.Setenv("foo_json", "9000")
|
|
c, err := ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "101", *c.Targets[0].Args["bar"])
|
|
|
|
t.Setenv("foo_JSON", "42")
|
|
c, err = ParseFile(dt, "docker-bake.hcl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(c.Targets))
|
|
require.Equal(t, "42", *c.Targets[0].Args["bar"])
|
|
})
|
|
}
|
|
|
|
func ptrstr(s any) *string {
|
|
var n *string
|
|
if reflect.ValueOf(s).Kind() == reflect.String {
|
|
ss := s.(string)
|
|
n = &ss
|
|
}
|
|
return n
|
|
}
|