Files
buildx/bake/hcl_test.go
Tonis Tiigi c5322ca727 bake: add --var flag for setting variable values
This provides alternative way to set values for Bake
variables without adding them the global environment variables.
This can also be used then environment variable access
is disabled with BUILDX_BAKE_DISABLE_VARS_ENV_LOOKUP.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-01-15 23:17:28 -08:00

2466 lines
56 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, 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, 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, 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, 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 TestHCLTypedValuelessVariablesUsedAsNull(t *testing.T) {
// Omitting complex types since they can't (shouldn't) be used as direct build arg values.
// Complex types can be used directly for other bake attributes, but that'll conflate HCL
// evaluation with whether the attribute accepts null in lieu of empty list/object, etc.
// This usage probably hits the 80/20 rule for usage of null variables.
types := []string{
"any",
"string", "number", "bool",
}
t.Run("assignment", func(t *testing.T) {
for _, varType := range types {
tName := fmt.Sprintf("value-less var typed %q", varType)
t.Run(tName, func(t *testing.T) {
dt := fmt.Sprintf(`
variable "FOO" {
type = %s
}
target "default" {
args = {
foo = FOO
}
}`, varType)
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
require.NoError(t, err)
require.Equal(t, 1, len(c.Targets))
require.Nil(t, c.Targets[0].Args["foo"])
})
}
})
t.Run("ternary", func(t *testing.T) {
for _, varType := range types {
tName := fmt.Sprintf("value-less var of %q on 'false' branch", varType)
t.Run(tName, func(t *testing.T) {
dt := fmt.Sprintf(`
variable "FOO" {
type = %s
}
target "default" {
args = {
foo = FOO == null ? "hi" : FOO
}
}`, varType)
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
require.NoError(t, err)
require.Equal(t, 1, len(c.Targets))
require.Equal(t, "hi", *c.Targets[0].Args["foo"])
})
}
for _, varType := range types {
tName := fmt.Sprintf("value-less var of %q on 'true' branch", varType)
t.Run(tName, func(t *testing.T) {
dt := fmt.Sprintf(`
variable "FOO" {
type = %s
}
target "default" {
args = {
foo = FOO == null ? FOO : "hi"
}
}`, varType)
c, err := ParseFile([]byte(dt), "docker-bake.hcl")
require.NoError(t, err)
require.Equal(t, 1, len(c.Targets))
require.Nil(t, 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, 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, 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, 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, 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"}, nil)
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, 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",
}, nil)
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, 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, 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 TestEmptyVariable(t *testing.T) {
dt := []byte(`
variable "FOO" {}
target "default" {
args = {
foo = equal(FOO, "")
}
}`)
c, err := ParseFile(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 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"},
nil,
)
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
}